@memberjunction/ng-record-changes 5.42.0 → 5.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/ng-record-changes.component.js +102 -120
- package/dist/lib/ng-record-changes.component.js.map +1 -1
- package/dist/lib/ng-record-changes.module.d.ts +1 -1
- package/dist/lib/ng-record-changes.module.d.ts.map +1 -1
- package/dist/lib/ng-record-changes.module.js +7 -4
- package/dist/lib/ng-record-changes.module.js.map +1 -1
- package/dist/lib/restore-preview-panel/restore-preview-panel.component.js +41 -45
- package/dist/lib/restore-preview-panel/restore-preview-panel.component.js.map +1 -1
- package/package.json +14 -11
- package/dist/__tests__/restore-version.test.d.ts +0 -2
- package/dist/__tests__/restore-version.test.d.ts.map +0 -1
- package/dist/__tests__/restore-version.test.js +0 -123
- package/dist/__tests__/restore-version.test.js.map +0 -1
- package/dist/lib/__tests__/record-changes.test.d.ts +0 -2
- package/dist/lib/__tests__/record-changes.test.d.ts.map +0 -1
- package/dist/lib/__tests__/record-changes.test.js +0 -308
- package/dist/lib/__tests__/record-changes.test.js.map +0 -1
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for record-changes package:
|
|
3
|
-
* - RecordChangesComponent utility methods (pure functions)
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
-
// Mock Angular
|
|
7
|
-
vi.mock('@angular/core', () => ({
|
|
8
|
-
Component: () => (target) => target,
|
|
9
|
-
Directive: () => (target) => target,
|
|
10
|
-
Injectable: () => (target) => target,
|
|
11
|
-
Input: () => () => { },
|
|
12
|
-
Output: () => () => { },
|
|
13
|
-
EventEmitter: class {
|
|
14
|
-
emit() { }
|
|
15
|
-
},
|
|
16
|
-
ChangeDetectorRef: class {
|
|
17
|
-
detectChanges() { }
|
|
18
|
-
markForCheck() { }
|
|
19
|
-
},
|
|
20
|
-
ChangeDetectionStrategy: { OnPush: 1 },
|
|
21
|
-
ViewEncapsulation: { None: 2 },
|
|
22
|
-
OnInit: class {
|
|
23
|
-
},
|
|
24
|
-
NgZone: class {
|
|
25
|
-
run(fn) { return fn(); }
|
|
26
|
-
},
|
|
27
|
-
}));
|
|
28
|
-
vi.mock('@memberjunction/ng-base-types', () => {
|
|
29
|
-
class MockBaseAngularComponent {
|
|
30
|
-
Provider = null;
|
|
31
|
-
get ProviderToUse() { return this.Provider; }
|
|
32
|
-
}
|
|
33
|
-
return { BaseAngularComponent: MockBaseAngularComponent };
|
|
34
|
-
});
|
|
35
|
-
vi.mock('@angular/platform-browser', () => ({
|
|
36
|
-
DomSanitizer: class {
|
|
37
|
-
bypassSecurityTrustHtml(html) { return html; }
|
|
38
|
-
},
|
|
39
|
-
}));
|
|
40
|
-
vi.mock('@memberjunction/core', () => ({
|
|
41
|
-
BaseEntity: class {
|
|
42
|
-
},
|
|
43
|
-
CompositeKey: class {
|
|
44
|
-
},
|
|
45
|
-
EntityFieldInfo: class {
|
|
46
|
-
},
|
|
47
|
-
EntityFieldTSType: { Boolean: 'boolean', Date: 'Date', Number: 'number', String: 'string' },
|
|
48
|
-
Metadata: class {
|
|
49
|
-
},
|
|
50
|
-
RunView: class {
|
|
51
|
-
RunView = vi.fn().mockResolvedValue({ Success: true, Results: [] });
|
|
52
|
-
},
|
|
53
|
-
}));
|
|
54
|
-
vi.mock('@memberjunction/core-entities', () => ({
|
|
55
|
-
MJRecordChangeEntity: class {
|
|
56
|
-
},
|
|
57
|
-
}));
|
|
58
|
-
vi.mock('@memberjunction/ng-notifications', () => ({
|
|
59
|
-
MJNotificationService: class {
|
|
60
|
-
CreateSimpleNotification = vi.fn();
|
|
61
|
-
},
|
|
62
|
-
}));
|
|
63
|
-
vi.mock('diff', () => ({
|
|
64
|
-
diffChars: vi.fn(() => []),
|
|
65
|
-
diffWords: vi.fn(() => []),
|
|
66
|
-
}));
|
|
67
|
-
describe('RecordChangesComponent utility methods', () => {
|
|
68
|
-
let component;
|
|
69
|
-
beforeEach(async () => {
|
|
70
|
-
vi.clearAllMocks();
|
|
71
|
-
const mod = await import('../ng-record-changes.component');
|
|
72
|
-
const { ChangeDetectorRef } = await import('@angular/core');
|
|
73
|
-
const { MJNotificationService } = await import('@memberjunction/ng-notifications');
|
|
74
|
-
const { DomSanitizer } = await import('@angular/platform-browser');
|
|
75
|
-
const { NgZone } = await import('@angular/core');
|
|
76
|
-
component = new mod.RecordChangesComponent({ detectChanges: vi.fn(), markForCheck: vi.fn() }, { run: (fn) => fn() }, {}, { bypassSecurityTrustHtml: (v) => v });
|
|
77
|
-
});
|
|
78
|
-
describe('getChangeTypeCardClass', () => {
|
|
79
|
-
it('should return correct class for Create', () => {
|
|
80
|
-
expect(component.getChangeTypeCardClass('Create')).toBe('type-create');
|
|
81
|
-
});
|
|
82
|
-
it('should return correct class for Update', () => {
|
|
83
|
-
expect(component.getChangeTypeCardClass('Update')).toBe('type-update');
|
|
84
|
-
});
|
|
85
|
-
it('should return correct class for Delete', () => {
|
|
86
|
-
expect(component.getChangeTypeCardClass('Delete')).toBe('type-delete');
|
|
87
|
-
});
|
|
88
|
-
it('should return type-update for unknown type', () => {
|
|
89
|
-
expect(component.getChangeTypeCardClass('Other')).toBe('type-update');
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
describe('getChangeTypeBadgeText', () => {
|
|
93
|
-
it('should return Created for Create', () => {
|
|
94
|
-
expect(component.getChangeTypeBadgeText('Create')).toBe('Created');
|
|
95
|
-
});
|
|
96
|
-
it('should return Update for Update', () => {
|
|
97
|
-
expect(component.getChangeTypeBadgeText('Update')).toBe('Update');
|
|
98
|
-
});
|
|
99
|
-
it('should return Deleted for Delete', () => {
|
|
100
|
-
expect(component.getChangeTypeBadgeText('Delete')).toBe('Deleted');
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
describe('getSourceClass', () => {
|
|
104
|
-
it('should return source-internal for Internal', () => {
|
|
105
|
-
expect(component.getSourceClass('Internal')).toBe('source-internal');
|
|
106
|
-
});
|
|
107
|
-
it('should return source-external for External', () => {
|
|
108
|
-
expect(component.getSourceClass('External')).toBe('source-external');
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
describe('getStatusClass', () => {
|
|
112
|
-
it('should return status-complete for Complete', () => {
|
|
113
|
-
expect(component.getStatusClass('Complete')).toBe('status-complete');
|
|
114
|
-
});
|
|
115
|
-
it('should return status-pending for Pending', () => {
|
|
116
|
-
expect(component.getStatusClass('Pending')).toBe('status-pending');
|
|
117
|
-
});
|
|
118
|
-
it('should return status-error for Error', () => {
|
|
119
|
-
expect(component.getStatusClass('Error')).toBe('status-error');
|
|
120
|
-
});
|
|
121
|
-
it('should return status-unknown for unknown status', () => {
|
|
122
|
-
expect(component.getStatusClass('Other')).toBe('status-unknown');
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
describe('getLabelStatusClass', () => {
|
|
126
|
-
it('should return correct classes for Active/Archived/Restored', () => {
|
|
127
|
-
expect(component.getLabelStatusClass('Active')).toBe('label-status-active');
|
|
128
|
-
expect(component.getLabelStatusClass('Archived')).toBe('label-status-archived');
|
|
129
|
-
expect(component.getLabelStatusClass('Restored')).toBe('label-status-restored');
|
|
130
|
-
expect(component.getLabelStatusClass('Unknown')).toBe('');
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
describe('getUserInitials', () => {
|
|
134
|
-
it('should extract initials from email', () => {
|
|
135
|
-
expect(component.getUserInitials('amith@bluecypress.io')).toBe('AM');
|
|
136
|
-
});
|
|
137
|
-
it('should extract initials from name', () => {
|
|
138
|
-
expect(component.getUserInitials('John Doe')).toBe('JD');
|
|
139
|
-
});
|
|
140
|
-
it('should handle single word', () => {
|
|
141
|
-
expect(component.getUserInitials('admin')).toBe('AD');
|
|
142
|
-
});
|
|
143
|
-
it('should handle null', () => {
|
|
144
|
-
expect(component.getUserInitials(null)).toBe('?');
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
describe('getUserDisplayName', () => {
|
|
148
|
-
it('should extract local part from email', () => {
|
|
149
|
-
expect(component.getUserDisplayName('amith@bluecypress.io')).toBe('amith');
|
|
150
|
-
});
|
|
151
|
-
it('should return name as-is', () => {
|
|
152
|
-
expect(component.getUserDisplayName('John Doe')).toBe('John Doe');
|
|
153
|
-
});
|
|
154
|
-
it('should return Unknown for null', () => {
|
|
155
|
-
expect(component.getUserDisplayName(null)).toBe('Unknown');
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
describe('Conditional pill filters', () => {
|
|
159
|
-
it('starts in "All" mode (no conditional pills selected)', () => {
|
|
160
|
-
component.viewData = [];
|
|
161
|
-
expect(component.IsAllSelected).toBe(true);
|
|
162
|
-
});
|
|
163
|
-
it('TogglePill toggles a pill on then off', () => {
|
|
164
|
-
component.viewData = [];
|
|
165
|
-
component.ChipSelections = { Update: false };
|
|
166
|
-
component.TogglePill('Update');
|
|
167
|
-
expect(component.ChipSelections['Update']).toBe(true);
|
|
168
|
-
component.TogglePill('Update');
|
|
169
|
-
expect(component.ChipSelections['Update']).toBe(false);
|
|
170
|
-
});
|
|
171
|
-
it('SelectAllPill clears every conditional selection', () => {
|
|
172
|
-
component.viewData = [];
|
|
173
|
-
component.ChipSelections = { Update: true, Create: true };
|
|
174
|
-
component.SelectAllPill();
|
|
175
|
-
expect(component.ChipSelections['Update']).toBe(false);
|
|
176
|
-
expect(component.ChipSelections['Create']).toBe(false);
|
|
177
|
-
expect(component.IsAllSelected).toBe(true);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
describe('formatRelativeTime', () => {
|
|
181
|
-
it('should return "Just now" for very recent dates', () => {
|
|
182
|
-
const now = new Date();
|
|
183
|
-
expect(component.formatRelativeTime(now)).toBe('Just now');
|
|
184
|
-
});
|
|
185
|
-
it('should return minutes for recent dates', () => {
|
|
186
|
-
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
187
|
-
expect(component.formatRelativeTime(fiveMinutesAgo)).toBe('5 mins ago');
|
|
188
|
-
});
|
|
189
|
-
it('should return hours for older dates', () => {
|
|
190
|
-
const threeHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000);
|
|
191
|
-
expect(component.formatRelativeTime(threeHoursAgo)).toBe('3 hours ago');
|
|
192
|
-
});
|
|
193
|
-
it('should return days for dates within a week', () => {
|
|
194
|
-
const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
|
|
195
|
-
expect(component.formatRelativeTime(twoDaysAgo)).toBe('2 days ago');
|
|
196
|
-
});
|
|
197
|
-
it('should return formatted date for older dates', () => {
|
|
198
|
-
const oldDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
199
|
-
const result = component.formatRelativeTime(oldDate);
|
|
200
|
-
expect(result).not.toContain('ago');
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
describe('formatFullDateTime', () => {
|
|
204
|
-
it('should format date with all parts', () => {
|
|
205
|
-
const date = new Date('2025-03-15T14:30:00Z');
|
|
206
|
-
const result = component.formatFullDateTime(date);
|
|
207
|
-
expect(result).toContain('2025');
|
|
208
|
-
expect(result).toContain('15');
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
describe('formatTime', () => {
|
|
212
|
-
it('should format time in 12-hour format', () => {
|
|
213
|
-
const date = new Date('2025-03-15T14:30:00');
|
|
214
|
-
const result = component.formatTime(date);
|
|
215
|
-
expect(result).toContain('2:30');
|
|
216
|
-
expect(result).toContain('PM');
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
describe('toggleExpansion', () => {
|
|
220
|
-
it('should add ID to expanded set', () => {
|
|
221
|
-
component.toggleExpansion('change-1');
|
|
222
|
-
expect(component.expandedItems.has('change-1')).toBe(true);
|
|
223
|
-
});
|
|
224
|
-
it('should remove ID from expanded set on second toggle', () => {
|
|
225
|
-
component.toggleExpansion('change-1');
|
|
226
|
-
component.toggleExpansion('change-1');
|
|
227
|
-
expect(component.expandedItems.has('change-1')).toBe(false);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
describe('ClearFilters', () => {
|
|
231
|
-
it('should reset all filter state', () => {
|
|
232
|
-
component.searchTerm = 'test';
|
|
233
|
-
component.ChipSelections = { Update: true, Restore: true };
|
|
234
|
-
component.viewData = [];
|
|
235
|
-
component.ClearFilters();
|
|
236
|
-
expect(component.searchTerm).toBe('');
|
|
237
|
-
expect(component.ChipSelections['Update']).toBe(false);
|
|
238
|
-
expect(component.ChipSelections['Restore']).toBe(false);
|
|
239
|
-
expect(component.IsAllSelected).toBe(true);
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
describe('getChangeSummary', () => {
|
|
243
|
-
it('should return "Record created" for Create type', () => {
|
|
244
|
-
const change = { Type: 'Create', ChangesJSON: '{}', ChangesDescription: '' };
|
|
245
|
-
expect(component.getChangeSummary(change)).toBe('Record created');
|
|
246
|
-
});
|
|
247
|
-
it('should return "Record deleted" for Delete type', () => {
|
|
248
|
-
const change = { Type: 'Delete', ChangesJSON: '{}', ChangesDescription: '' };
|
|
249
|
-
expect(component.getChangeSummary(change)).toBe('Record deleted');
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
// PR #2841 precompute: viewDataById is rebuilt on each data load so the lineage
|
|
253
|
-
// chip's source lookup is an O(1) NormalizeUUID-keyed Map read instead of an
|
|
254
|
-
// O(N) scan per CD pass. These tests pin the rebuild trigger + the case-variant
|
|
255
|
-
// (SQL upper vs Postgres lower) UUID resolution that NormalizeUUID guarantees.
|
|
256
|
-
describe('viewDataById / getRestoredFromSourceChange (lineage lookup precompute)', () => {
|
|
257
|
-
/** Call the private rebuild — the real component invokes it inside LoadRecordChanges. */
|
|
258
|
-
function rebuild() {
|
|
259
|
-
component.rebuildViewDataIndex();
|
|
260
|
-
}
|
|
261
|
-
function setData(rows) {
|
|
262
|
-
component.viewData = rows;
|
|
263
|
-
rebuild();
|
|
264
|
-
}
|
|
265
|
-
it('resolves a restore row to its source change (O(1) Map read)', () => {
|
|
266
|
-
const source = { ID: 'AAAAAAAA-1111-2222-3333-444444444444' };
|
|
267
|
-
const restore = { ID: 'BBBBBBBB-0000-0000-0000-000000000000', RestoredFromID: source.ID };
|
|
268
|
-
setData([source, restore]);
|
|
269
|
-
expect(component.getRestoredFromSourceChange(restore)).toBe(source);
|
|
270
|
-
});
|
|
271
|
-
it('returns null when this row is NOT a restore (no RestoredFromID)', () => {
|
|
272
|
-
const plain = { ID: 'CCCCCCCC-1111-2222-3333-444444444444' };
|
|
273
|
-
setData([plain]);
|
|
274
|
-
expect(component.getRestoredFromSourceChange(plain)).toBeNull();
|
|
275
|
-
});
|
|
276
|
-
it('returns null when the source change is not in the loaded history (pruned)', () => {
|
|
277
|
-
const restore = { ID: 'DDDDDDDD-0000-0000-0000-000000000000', RestoredFromID: 'EEEEEEEE-9999-9999-9999-999999999999' };
|
|
278
|
-
setData([restore]); // the referenced source row is absent
|
|
279
|
-
expect(component.getRestoredFromSourceChange(restore)).toBeNull();
|
|
280
|
-
});
|
|
281
|
-
it('resolves across UUID case variance (SQL upper RestoredFromID → Postgres lower stored ID)', () => {
|
|
282
|
-
// Source stored lowercase (Postgres style); restore references it uppercase (SQL style).
|
|
283
|
-
const source = { ID: 'ff112233-aabb-ccdd-eeff-001122334455' };
|
|
284
|
-
const restore = { ID: 'AB000000-0000-0000-0000-000000000001', RestoredFromID: 'FF112233-AABB-CCDD-EEFF-001122334455' };
|
|
285
|
-
setData([source, restore]);
|
|
286
|
-
// Without NormalizeUUID keying this would miss; with it, the case difference is erased.
|
|
287
|
-
expect(component.getRestoredFromSourceChange(restore)).toBe(source);
|
|
288
|
-
});
|
|
289
|
-
it('rebuilds the index on each data load — a stale source from a prior load does not leak', () => {
|
|
290
|
-
const oldSource = { ID: '11111111-1111-1111-1111-111111111111' };
|
|
291
|
-
const restore = { ID: '22222222-2222-2222-2222-222222222222', RestoredFromID: oldSource.ID };
|
|
292
|
-
setData([oldSource, restore]);
|
|
293
|
-
expect(component.getRestoredFromSourceChange(restore)).toBe(oldSource);
|
|
294
|
-
// New load WITHOUT the old source row — the rebuilt index must forget it.
|
|
295
|
-
setData([restore]);
|
|
296
|
-
expect(component.getRestoredFromSourceChange(restore)).toBeNull();
|
|
297
|
-
});
|
|
298
|
-
it('last-writer-wins on duplicate IDs in the loaded set (Map semantics)', () => {
|
|
299
|
-
const first = { ID: 'DEAD0000-0000-0000-0000-000000000000', Source: 'A' };
|
|
300
|
-
const second = { ID: 'dead0000-0000-0000-0000-000000000000', Source: 'B' };
|
|
301
|
-
const restore = { ID: '99999999-9999-9999-9999-999999999999', RestoredFromID: 'DEAD0000-0000-0000-0000-000000000000' };
|
|
302
|
-
setData([first, second, restore]);
|
|
303
|
-
// both normalize to the same key — the later insertion wins
|
|
304
|
-
expect(component.getRestoredFromSourceChange(restore)).toBe(second);
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
//# sourceMappingURL=record-changes.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"record-changes.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/record-changes.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Tests for record-changes package:\n * - RecordChangesComponent utility methods (pure functions)\n */\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\n\n// Mock Angular\nvi.mock('@angular/core', () => ({\n Component: () => (target: Function) => target,\n Directive: () => (target: Function) => target,\n Injectable: () => (target: Function) => target,\n Input: () => () => {},\n Output: () => () => {},\n EventEmitter: class { emit() {} },\n ChangeDetectorRef: class { detectChanges() {} markForCheck() {} },\n ChangeDetectionStrategy: { OnPush: 1 },\n ViewEncapsulation: { None: 2 },\n OnInit: class {},\n NgZone: class { run(fn: Function) { return fn(); } },\n}));\n\nvi.mock('@memberjunction/ng-base-types', () => {\n class MockBaseAngularComponent {\n Provider: unknown = null;\n get ProviderToUse() { return this.Provider; }\n }\n return { BaseAngularComponent: MockBaseAngularComponent };\n});\n\nvi.mock('@angular/platform-browser', () => ({\n DomSanitizer: class {\n bypassSecurityTrustHtml(html: string) { return html; }\n },\n}));\n\nvi.mock('@memberjunction/core', () => ({\n BaseEntity: class {},\n CompositeKey: class {},\n EntityFieldInfo: class {},\n EntityFieldTSType: { Boolean: 'boolean', Date: 'Date', Number: 'number', String: 'string' },\n Metadata: class {},\n RunView: class {\n RunView = vi.fn().mockResolvedValue({ Success: true, Results: [] });\n },\n}));\n\nvi.mock('@memberjunction/core-entities', () => ({\n MJRecordChangeEntity: class {},\n}));\n\nvi.mock('@memberjunction/ng-notifications', () => ({\n MJNotificationService: class {\n CreateSimpleNotification = vi.fn();\n },\n}));\n\nvi.mock('diff', () => ({\n diffChars: vi.fn(() => []),\n diffWords: vi.fn(() => []),\n}));\n\ndescribe('RecordChangesComponent utility methods', () => {\n let component: InstanceType<typeof import('../ng-record-changes.component').RecordChangesComponent>;\n\n beforeEach(async () => {\n vi.clearAllMocks();\n const mod = await import('../ng-record-changes.component');\n const { ChangeDetectorRef } = await import('@angular/core');\n const { MJNotificationService } = await import('@memberjunction/ng-notifications');\n const { DomSanitizer } = await import('@angular/platform-browser');\n const { NgZone } = await import('@angular/core');\n component = new mod.RecordChangesComponent(\n { detectChanges: vi.fn(), markForCheck: vi.fn() } as unknown as InstanceType<typeof ChangeDetectorRef>,\n { run: (fn: () => void) => fn() } as unknown as InstanceType<typeof NgZone>,\n {} as InstanceType<typeof MJNotificationService>,\n { bypassSecurityTrustHtml: (v: string) => v } as unknown as InstanceType<typeof DomSanitizer>,\n );\n });\n\n describe('getChangeTypeCardClass', () => {\n it('should return correct class for Create', () => {\n expect(component.getChangeTypeCardClass('Create')).toBe('type-create');\n });\n\n it('should return correct class for Update', () => {\n expect(component.getChangeTypeCardClass('Update')).toBe('type-update');\n });\n\n it('should return correct class for Delete', () => {\n expect(component.getChangeTypeCardClass('Delete')).toBe('type-delete');\n });\n\n it('should return type-update for unknown type', () => {\n expect(component.getChangeTypeCardClass('Other')).toBe('type-update');\n });\n });\n\n describe('getChangeTypeBadgeText', () => {\n it('should return Created for Create', () => {\n expect(component.getChangeTypeBadgeText('Create')).toBe('Created');\n });\n\n it('should return Update for Update', () => {\n expect(component.getChangeTypeBadgeText('Update')).toBe('Update');\n });\n\n it('should return Deleted for Delete', () => {\n expect(component.getChangeTypeBadgeText('Delete')).toBe('Deleted');\n });\n });\n\n describe('getSourceClass', () => {\n it('should return source-internal for Internal', () => {\n expect(component.getSourceClass('Internal')).toBe('source-internal');\n });\n\n it('should return source-external for External', () => {\n expect(component.getSourceClass('External')).toBe('source-external');\n });\n });\n\n describe('getStatusClass', () => {\n it('should return status-complete for Complete', () => {\n expect(component.getStatusClass('Complete')).toBe('status-complete');\n });\n\n it('should return status-pending for Pending', () => {\n expect(component.getStatusClass('Pending')).toBe('status-pending');\n });\n\n it('should return status-error for Error', () => {\n expect(component.getStatusClass('Error')).toBe('status-error');\n });\n\n it('should return status-unknown for unknown status', () => {\n expect(component.getStatusClass('Other')).toBe('status-unknown');\n });\n });\n\n describe('getLabelStatusClass', () => {\n it('should return correct classes for Active/Archived/Restored', () => {\n expect(component.getLabelStatusClass('Active')).toBe('label-status-active');\n expect(component.getLabelStatusClass('Archived')).toBe('label-status-archived');\n expect(component.getLabelStatusClass('Restored')).toBe('label-status-restored');\n expect(component.getLabelStatusClass('Unknown')).toBe('');\n });\n });\n\n describe('getUserInitials', () => {\n it('should extract initials from email', () => {\n expect(component.getUserInitials('amith@bluecypress.io')).toBe('AM');\n });\n\n it('should extract initials from name', () => {\n expect(component.getUserInitials('John Doe')).toBe('JD');\n });\n\n it('should handle single word', () => {\n expect(component.getUserInitials('admin')).toBe('AD');\n });\n\n it('should handle null', () => {\n expect(component.getUserInitials(null)).toBe('?');\n });\n });\n\n describe('getUserDisplayName', () => {\n it('should extract local part from email', () => {\n expect(component.getUserDisplayName('amith@bluecypress.io')).toBe('amith');\n });\n\n it('should return name as-is', () => {\n expect(component.getUserDisplayName('John Doe')).toBe('John Doe');\n });\n\n it('should return Unknown for null', () => {\n expect(component.getUserDisplayName(null)).toBe('Unknown');\n });\n });\n\n describe('Conditional pill filters', () => {\n it('starts in \"All\" mode (no conditional pills selected)', () => {\n component.viewData = [];\n expect(component.IsAllSelected).toBe(true);\n });\n\n it('TogglePill toggles a pill on then off', () => {\n component.viewData = [];\n component.ChipSelections = { Update: false };\n component.TogglePill('Update');\n expect(component.ChipSelections['Update']).toBe(true);\n component.TogglePill('Update');\n expect(component.ChipSelections['Update']).toBe(false);\n });\n\n it('SelectAllPill clears every conditional selection', () => {\n component.viewData = [];\n component.ChipSelections = { Update: true, Create: true };\n component.SelectAllPill();\n expect(component.ChipSelections['Update']).toBe(false);\n expect(component.ChipSelections['Create']).toBe(false);\n expect(component.IsAllSelected).toBe(true);\n });\n });\n\n describe('formatRelativeTime', () => {\n it('should return \"Just now\" for very recent dates', () => {\n const now = new Date();\n expect(component.formatRelativeTime(now)).toBe('Just now');\n });\n\n it('should return minutes for recent dates', () => {\n const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);\n expect(component.formatRelativeTime(fiveMinutesAgo)).toBe('5 mins ago');\n });\n\n it('should return hours for older dates', () => {\n const threeHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000);\n expect(component.formatRelativeTime(threeHoursAgo)).toBe('3 hours ago');\n });\n\n it('should return days for dates within a week', () => {\n const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);\n expect(component.formatRelativeTime(twoDaysAgo)).toBe('2 days ago');\n });\n\n it('should return formatted date for older dates', () => {\n const oldDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);\n const result = component.formatRelativeTime(oldDate);\n expect(result).not.toContain('ago');\n });\n });\n\n describe('formatFullDateTime', () => {\n it('should format date with all parts', () => {\n const date = new Date('2025-03-15T14:30:00Z');\n const result = component.formatFullDateTime(date);\n expect(result).toContain('2025');\n expect(result).toContain('15');\n });\n });\n\n describe('formatTime', () => {\n it('should format time in 12-hour format', () => {\n const date = new Date('2025-03-15T14:30:00');\n const result = component.formatTime(date);\n expect(result).toContain('2:30');\n expect(result).toContain('PM');\n });\n });\n\n describe('toggleExpansion', () => {\n it('should add ID to expanded set', () => {\n component.toggleExpansion('change-1');\n expect(component.expandedItems.has('change-1')).toBe(true);\n });\n\n it('should remove ID from expanded set on second toggle', () => {\n component.toggleExpansion('change-1');\n component.toggleExpansion('change-1');\n expect(component.expandedItems.has('change-1')).toBe(false);\n });\n });\n\n describe('ClearFilters', () => {\n it('should reset all filter state', () => {\n component.searchTerm = 'test';\n component.ChipSelections = { Update: true, Restore: true };\n component.viewData = [];\n\n component.ClearFilters();\n\n expect(component.searchTerm).toBe('');\n expect(component.ChipSelections['Update']).toBe(false);\n expect(component.ChipSelections['Restore']).toBe(false);\n expect(component.IsAllSelected).toBe(true);\n });\n });\n\n describe('getChangeSummary', () => {\n it('should return \"Record created\" for Create type', () => {\n const change = { Type: 'Create', ChangesJSON: '{}', ChangesDescription: '' };\n expect(component.getChangeSummary(change as never)).toBe('Record created');\n });\n\n it('should return \"Record deleted\" for Delete type', () => {\n const change = { Type: 'Delete', ChangesJSON: '{}', ChangesDescription: '' };\n expect(component.getChangeSummary(change as never)).toBe('Record deleted');\n });\n });\n\n // PR #2841 precompute: viewDataById is rebuilt on each data load so the lineage\n // chip's source lookup is an O(1) NormalizeUUID-keyed Map read instead of an\n // O(N) scan per CD pass. These tests pin the rebuild trigger + the case-variant\n // (SQL upper vs Postgres lower) UUID resolution that NormalizeUUID guarantees.\n describe('viewDataById / getRestoredFromSourceChange (lineage lookup precompute)', () => {\n type Change = { ID: string; RestoredFromID?: string | null; Source?: string };\n\n /** Call the private rebuild — the real component invokes it inside LoadRecordChanges. */\n function rebuild(): void {\n (component as unknown as { rebuildViewDataIndex: () => void }).rebuildViewDataIndex();\n }\n\n function setData(rows: Change[]): void {\n component.viewData = rows as never;\n rebuild();\n }\n\n it('resolves a restore row to its source change (O(1) Map read)', () => {\n const source: Change = { ID: 'AAAAAAAA-1111-2222-3333-444444444444' };\n const restore: Change = { ID: 'BBBBBBBB-0000-0000-0000-000000000000', RestoredFromID: source.ID };\n setData([source, restore]);\n\n expect(component.getRestoredFromSourceChange(restore as never)).toBe(source as never);\n });\n\n it('returns null when this row is NOT a restore (no RestoredFromID)', () => {\n const plain: Change = { ID: 'CCCCCCCC-1111-2222-3333-444444444444' };\n setData([plain]);\n expect(component.getRestoredFromSourceChange(plain as never)).toBeNull();\n });\n\n it('returns null when the source change is not in the loaded history (pruned)', () => {\n const restore: Change = { ID: 'DDDDDDDD-0000-0000-0000-000000000000', RestoredFromID: 'EEEEEEEE-9999-9999-9999-999999999999' };\n setData([restore]); // the referenced source row is absent\n expect(component.getRestoredFromSourceChange(restore as never)).toBeNull();\n });\n\n it('resolves across UUID case variance (SQL upper RestoredFromID → Postgres lower stored ID)', () => {\n // Source stored lowercase (Postgres style); restore references it uppercase (SQL style).\n const source: Change = { ID: 'ff112233-aabb-ccdd-eeff-001122334455' };\n const restore: Change = { ID: 'AB000000-0000-0000-0000-000000000001', RestoredFromID: 'FF112233-AABB-CCDD-EEFF-001122334455' };\n setData([source, restore]);\n\n // Without NormalizeUUID keying this would miss; with it, the case difference is erased.\n expect(component.getRestoredFromSourceChange(restore as never)).toBe(source as never);\n });\n\n it('rebuilds the index on each data load — a stale source from a prior load does not leak', () => {\n const oldSource: Change = { ID: '11111111-1111-1111-1111-111111111111' };\n const restore: Change = { ID: '22222222-2222-2222-2222-222222222222', RestoredFromID: oldSource.ID };\n setData([oldSource, restore]);\n expect(component.getRestoredFromSourceChange(restore as never)).toBe(oldSource as never);\n\n // New load WITHOUT the old source row — the rebuilt index must forget it.\n setData([restore]);\n expect(component.getRestoredFromSourceChange(restore as never)).toBeNull();\n });\n\n it('last-writer-wins on duplicate IDs in the loaded set (Map semantics)', () => {\n const first: Change = { ID: 'DEAD0000-0000-0000-0000-000000000000', Source: 'A' };\n const second: Change = { ID: 'dead0000-0000-0000-0000-000000000000', Source: 'B' };\n const restore: Change = { ID: '99999999-9999-9999-9999-999999999999', RestoredFromID: 'DEAD0000-0000-0000-0000-000000000000' };\n setData([first, second, restore]);\n // both normalize to the same key — the later insertion wins\n expect(component.getRestoredFromSourceChange(restore as never)).toBe(second as never);\n });\n });\n});\n"]}
|