@memberjunction/ng-entity-viewer 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.
Files changed (78) hide show
  1. package/dist/lib/aggregate-panel/aggregate-panel.component.d.ts +5 -0
  2. package/dist/lib/aggregate-panel/aggregate-panel.component.d.ts.map +1 -1
  3. package/dist/lib/aggregate-panel/aggregate-panel.component.js +45 -47
  4. package/dist/lib/aggregate-panel/aggregate-panel.component.js.map +1 -1
  5. package/dist/lib/entity-cards/entity-cards.component.js +7 -10
  6. package/dist/lib/entity-cards/entity-cards.component.js.map +1 -1
  7. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts +42 -2
  8. package/dist/lib/entity-data-grid/entity-data-grid.component.d.ts.map +1 -1
  9. package/dist/lib/entity-data-grid/entity-data-grid.component.js +256 -141
  10. package/dist/lib/entity-data-grid/entity-data-grid.component.js.map +1 -1
  11. package/dist/lib/entity-data-grid/models/grid-types.d.ts +8 -1
  12. package/dist/lib/entity-data-grid/models/grid-types.d.ts.map +1 -1
  13. package/dist/lib/entity-data-grid/models/grid-types.js.map +1 -1
  14. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts +0 -4
  15. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.d.ts.map +1 -1
  16. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js +166 -207
  17. package/dist/lib/entity-record-detail-panel/entity-record-detail-panel.component.js.map +1 -1
  18. package/dist/lib/entity-viewer/entity-viewer.component.d.ts +109 -5
  19. package/dist/lib/entity-viewer/entity-viewer.component.d.ts.map +1 -1
  20. package/dist/lib/entity-viewer/entity-viewer.component.js +287 -42
  21. package/dist/lib/entity-viewer/entity-viewer.component.js.map +1 -1
  22. package/dist/lib/recycle-bin/recycle-bin.component.js +21 -44
  23. package/dist/lib/recycle-bin/recycle-bin.component.js.map +1 -1
  24. package/dist/lib/shared-view-warning-dialog/shared-view-warning-dialog.component.js +19 -20
  25. package/dist/lib/shared-view-warning-dialog/shared-view-warning-dialog.component.js.map +1 -1
  26. package/dist/lib/view-config-panel/view-config-panel.component.js +268 -301
  27. package/dist/lib/view-config-panel/view-config-panel.component.js.map +1 -1
  28. package/dist/lib/view-selector/view-selector.component.d.ts +26 -1
  29. package/dist/lib/view-selector/view-selector.component.d.ts.map +1 -1
  30. package/dist/lib/view-selector/view-selector.component.js +118 -76
  31. package/dist/lib/view-selector/view-selector.component.js.map +1 -1
  32. package/dist/lib/view-types/descriptors/grid-view-type.d.ts +7 -0
  33. package/dist/lib/view-types/descriptors/grid-view-type.d.ts.map +1 -1
  34. package/dist/lib/view-types/descriptors/grid-view-type.js +7 -0
  35. package/dist/lib/view-types/descriptors/grid-view-type.js.map +1 -1
  36. package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts +24 -1
  37. package/dist/lib/view-types/renderers/grid-view-renderer.component.d.ts.map +1 -1
  38. package/dist/lib/view-types/renderers/grid-view-renderer.component.js +39 -4
  39. package/dist/lib/view-types/renderers/grid-view-renderer.component.js.map +1 -1
  40. package/dist/lib/view-types/renderers/map-view-renderer.component.d.ts.map +1 -1
  41. package/dist/lib/view-types/renderers/map-view-renderer.component.js +11 -14
  42. package/dist/lib/view-types/renderers/map-view-renderer.component.js.map +1 -1
  43. package/dist/lib/view-types/renderers/timeline-view-renderer.component.d.ts.map +1 -1
  44. package/dist/lib/view-types/renderers/timeline-view-renderer.component.js +12 -14
  45. package/dist/lib/view-types/renderers/timeline-view-renderer.component.js.map +1 -1
  46. package/dist/lib/view-types/view-type.contracts.d.ts +44 -0
  47. package/dist/lib/view-types/view-type.contracts.d.ts.map +1 -1
  48. package/dist/lib/view-types/view-type.contracts.js +6 -0
  49. package/dist/lib/view-types/view-type.contracts.js.map +1 -1
  50. package/dist/lib/view-workspace/view-workspace.component.d.ts +32 -1
  51. package/dist/lib/view-workspace/view-workspace.component.d.ts.map +1 -1
  52. package/dist/lib/view-workspace/view-workspace.component.js +68 -7
  53. package/dist/lib/view-workspace/view-workspace.component.js.map +1 -1
  54. package/dist/module.d.ts +2 -1
  55. package/dist/module.d.ts.map +1 -1
  56. package/dist/module.js +20 -4
  57. package/dist/module.js.map +1 -1
  58. package/package.json +18 -15
  59. package/dist/__tests__/entity-cards-selection.test.d.ts +0 -2
  60. package/dist/__tests__/entity-cards-selection.test.d.ts.map +0 -1
  61. package/dist/__tests__/entity-cards-selection.test.js +0 -149
  62. package/dist/__tests__/entity-cards-selection.test.js.map +0 -1
  63. package/dist/__tests__/entity-cards-viewmodel.test.d.ts +0 -2
  64. package/dist/__tests__/entity-cards-viewmodel.test.d.ts.map +0 -1
  65. package/dist/__tests__/entity-cards-viewmodel.test.js +0 -208
  66. package/dist/__tests__/entity-cards-viewmodel.test.js.map +0 -1
  67. package/dist/__tests__/highlight.test.d.ts +0 -2
  68. package/dist/__tests__/highlight.test.d.ts.map +0 -1
  69. package/dist/__tests__/highlight.test.js +0 -67
  70. package/dist/__tests__/highlight.test.js.map +0 -1
  71. package/dist/__tests__/index.test.d.ts +0 -2
  72. package/dist/__tests__/index.test.d.ts.map +0 -1
  73. package/dist/__tests__/index.test.js +0 -7
  74. package/dist/__tests__/index.test.js.map +0 -1
  75. package/dist/__tests__/view-types.test.d.ts +0 -2
  76. package/dist/__tests__/view-types.test.d.ts.map +0 -1
  77. package/dist/__tests__/view-types.test.js +0 -102
  78. package/dist/__tests__/view-types.test.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/ng-entity-viewer",
3
- "version": "5.42.0",
3
+ "version": "5.44.0",
4
4
  "description": "MemberJunction: Angular components for viewing entity data in multiple formats (grid, cards) with filtering, selection, and shared data management",
5
5
  "main": "./dist/public-api.js",
6
6
  "typings": "./dist/public-api.d.ts",
@@ -26,6 +26,7 @@
26
26
  "devDependencies": {
27
27
  "@angular/compiler": "21.1.3",
28
28
  "@angular/compiler-cli": "21.1.3",
29
+ "@memberjunction/ng-test-utils": "5.44.0",
29
30
  "vitest": "^4.0.18"
30
31
  },
31
32
  "peerDependencies": {
@@ -37,20 +38,22 @@
37
38
  "ag-grid-community": "^35.0.1"
38
39
  },
39
40
  "dependencies": {
40
- "@memberjunction/core": "5.42.0",
41
- "@memberjunction/core-entities": "5.42.0",
42
- "@memberjunction/export-engine": "5.42.0",
43
- "@memberjunction/global": "5.42.0",
44
- "@memberjunction/ng-base-types": "5.42.0",
45
- "@memberjunction/ng-export-service": "5.42.0",
46
- "@memberjunction/ng-filter-builder": "5.42.0",
47
- "@memberjunction/ng-list-management": "5.42.0",
48
- "@memberjunction/ng-map-view": "5.42.0",
49
- "@memberjunction/ng-pagination": "5.42.0",
50
- "@memberjunction/ng-record-changes": "5.42.0",
51
- "@memberjunction/ng-shared-generic": "5.42.0",
52
- "@memberjunction/ng-timeline": "5.42.0",
53
- "@memberjunction/ng-ui-components": "5.42.0",
41
+ "@memberjunction/actions-base": "5.44.0",
42
+ "@memberjunction/core": "5.44.0",
43
+ "@memberjunction/core-entities": "5.44.0",
44
+ "@memberjunction/export-engine": "5.44.0",
45
+ "@memberjunction/global": "5.44.0",
46
+ "@memberjunction/ng-base-types": "5.44.0",
47
+ "@memberjunction/ng-entity-action-ux": "5.44.0",
48
+ "@memberjunction/ng-export-service": "5.44.0",
49
+ "@memberjunction/ng-filter-builder": "5.44.0",
50
+ "@memberjunction/ng-list-management": "5.44.0",
51
+ "@memberjunction/ng-map-view": "5.44.0",
52
+ "@memberjunction/ng-pagination": "5.44.0",
53
+ "@memberjunction/ng-record-changes": "5.44.0",
54
+ "@memberjunction/ng-shared-generic": "5.44.0",
55
+ "@memberjunction/ng-timeline": "5.44.0",
56
+ "@memberjunction/ng-ui-components": "5.44.0",
54
57
  "rxjs": "^7.8.2",
55
58
  "tslib": "^2.8.1"
56
59
  },
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=entity-cards-selection.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entity-cards-selection.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/entity-cards-selection.test.ts"],"names":[],"mappings":""}
@@ -1,149 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { EntityCardsComponent } from '../lib/entity-cards/entity-cards.component';
3
- /**
4
- * Fix A regression coverage: a selection-only change (selectedRecordId) must
5
- * flip the cheap isSelected flags on the EXISTING card view-models WITHOUT
6
- * rebuilding them — i.e. without re-running the per-card buildPkString()
7
- * allocations or the RegExp-building highlightMatch() calls. A records change,
8
- * by contrast, must rebuild.
9
- */
10
- // Minimal EntityInfo mock — just enough for buildPkString (PrimaryKeys) and
11
- // the template-generation heuristics (Fields). Single uniqueidentifier PK 'ID'.
12
- function mockEntity() {
13
- const idField = {
14
- Name: 'ID',
15
- IsPrimaryKey: true,
16
- TSType: 'string',
17
- Type: 'uniqueidentifier',
18
- Sequence: 0,
19
- IsNameField: false,
20
- DefaultInView: false,
21
- DisplayNameOrName: 'ID',
22
- EntityFieldValues: [],
23
- };
24
- const nameField = {
25
- Name: 'Name',
26
- IsPrimaryKey: false,
27
- TSType: 'string',
28
- Type: 'nvarchar',
29
- Sequence: 1,
30
- IsNameField: true,
31
- DefaultInView: true,
32
- DisplayNameOrName: 'Name',
33
- EntityFieldValues: [],
34
- };
35
- const fields = [idField, nameField];
36
- return {
37
- ID: 'E1',
38
- Name: 'Test Entity',
39
- Fields: fields,
40
- PrimaryKeys: [idField],
41
- NameField: nameField,
42
- };
43
- }
44
- function makeComponent() {
45
- const elementRef = { nativeElement: { querySelector: () => null } };
46
- const cdr = { detectChanges: () => { }, markForCheck: () => { } };
47
- return new EntityCardsComponent(elementRef, cdr);
48
- }
49
- function change(previousValue, currentValue, firstChange = false) {
50
- return { previousValue, currentValue, firstChange, isFirstChange: () => firstChange };
51
- }
52
- describe('EntityCardsComponent — selection-only updates (Fix A)', () => {
53
- let component;
54
- beforeEach(() => {
55
- component = makeComponent();
56
- component.entity = mockEntity();
57
- component.records = [
58
- { ID: 'a', Name: 'Alpha' },
59
- { ID: 'b', Name: 'Bravo' },
60
- { ID: 'c', Name: 'Charlie' },
61
- ];
62
- component.ngOnInit();
63
- });
64
- // The pkString format is CompositeKey.ToConcatenatedString() (e.g. "ID|a"),
65
- // not the raw PK value. Resolve the real strings from the built VMs so the
66
- // tests are robust to that format.
67
- function pk(index) {
68
- return component.cardViewModels[index].pkString;
69
- }
70
- it('builds VMs with pkStrings and no selection initially', () => {
71
- expect(component.cardViewModels).toHaveLength(3);
72
- expect(component.cardViewModels.every(vm => vm.pkString.length > 0)).toBe(true);
73
- expect(component.cardViewModels.every(vm => !vm.isSelected)).toBe(true);
74
- });
75
- it('selection-only change flips the correct isSelected flag WITHOUT re-running highlightMatch', () => {
76
- const highlightSpy = vi.spyOn(component, 'highlightMatch');
77
- const targetPk = pk(1);
78
- component.selectedRecordId = targetPk;
79
- const changes = { selectedRecordId: change(null, targetPk) };
80
- component.ngOnChanges(changes);
81
- expect(highlightSpy).not.toHaveBeenCalled();
82
- expect(component.cardViewModels.map(vm => vm.isSelected)).toEqual([false, true, false]);
83
- });
84
- it('selection-only change preserves the array and every VM object reference', () => {
85
- const arrayBefore = component.cardViewModels;
86
- const vmsBefore = [...component.cardViewModels];
87
- const targetPk = pk(2);
88
- component.selectedRecordId = targetPk;
89
- component.ngOnChanges({ selectedRecordId: change(null, targetPk) });
90
- // Same array instance and same VM instances — only the flags mutated in place.
91
- expect(component.cardViewModels).toBe(arrayBefore);
92
- component.cardViewModels.forEach((vm, i) => expect(vm).toBe(vmsBefore[i]));
93
- expect(component.cardViewModels.map(vm => vm.isSelected)).toEqual([false, false, true]);
94
- });
95
- it('clearing the selection unsets all isSelected flags', () => {
96
- const firstPk = pk(0);
97
- component.selectedRecordId = firstPk;
98
- component.ngOnChanges({ selectedRecordId: change(null, firstPk) });
99
- expect(component.cardViewModels.map(vm => vm.isSelected)).toEqual([true, false, false]);
100
- component.selectedRecordId = null;
101
- component.ngOnChanges({ selectedRecordId: change(firstPk, null) });
102
- expect(component.cardViewModels.every(vm => !vm.isSelected)).toBe(true);
103
- });
104
- it('a records change DOES rebuild the VMs (new array + highlightMatch runs)', () => {
105
- const arrayBefore = component.cardViewModels;
106
- const highlightSpy = vi.spyOn(component, 'highlightMatch');
107
- component.records = [
108
- { ID: 'x', Name: 'Xray' },
109
- { ID: 'y', Name: 'Yankee' },
110
- ];
111
- component.ngOnChanges({ records: change(null, component.records) });
112
- expect(component.cardViewModels).not.toBe(arrayBefore);
113
- expect(component.cardViewModels).toHaveLength(2);
114
- expect(component.cardViewModels.every(vm => vm.pkString.length > 0)).toBe(true);
115
- expect(highlightSpy).toHaveBeenCalled();
116
- });
117
- it('a combined records+selectedRecordId change rebuilds and sets selection correctly', () => {
118
- const highlightSpy = vi.spyOn(component, 'highlightMatch');
119
- component.records = [
120
- { ID: 'x', Name: 'Xray' },
121
- { ID: 'y', Name: 'Yankee' },
122
- ];
123
- // Match the second new record's pkString format ("ID|y").
124
- const targetPk = component.cardViewModels[1].pkString.replace(/[^|]+$/, 'y');
125
- component.selectedRecordId = targetPk;
126
- component.ngOnChanges({
127
- records: change(null, component.records),
128
- selectedRecordId: change(null, targetPk),
129
- });
130
- // content change wins → full rebuild, which also sets isSelected correctly
131
- expect(highlightSpy).toHaveBeenCalled();
132
- expect(component.cardViewModels.map(vm => vm.isSelected)).toEqual([false, true]);
133
- });
134
- it('selection-only change still requests a scroll-to-selected', () => {
135
- const targetPk = pk(1);
136
- component.selectedRecordId = targetPk;
137
- component.ngOnChanges({ selectedRecordId: change(null, targetPk) });
138
- // pendingScrollToSelected is private; assert via behavior in ngAfterViewChecked.
139
- const scrollSpy = vi
140
- .spyOn(component, 'scrollToSelectedCard')
141
- .mockImplementation(() => { });
142
- vi.useFakeTimers();
143
- component.ngAfterViewChecked();
144
- vi.runAllTimers();
145
- expect(scrollSpy).toHaveBeenCalled();
146
- vi.useRealTimers();
147
- });
148
- });
149
- //# sourceMappingURL=entity-cards-selection.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entity-cards-selection.test.js","sourceRoot":"","sources":["../../src/__tests__/entity-cards-selection.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest';\nimport type { ElementRef, ChangeDetectorRef, SimpleChanges, SimpleChange } from '@angular/core';\nimport type { EntityInfo, EntityFieldInfo } from '@memberjunction/core';\nimport { EntityCardsComponent } from '../lib/entity-cards/entity-cards.component';\n\n/**\n * Fix A regression coverage: a selection-only change (selectedRecordId) must\n * flip the cheap isSelected flags on the EXISTING card view-models WITHOUT\n * rebuilding them — i.e. without re-running the per-card buildPkString()\n * allocations or the RegExp-building highlightMatch() calls. A records change,\n * by contrast, must rebuild.\n */\n\n// Minimal EntityInfo mock — just enough for buildPkString (PrimaryKeys) and\n// the template-generation heuristics (Fields). Single uniqueidentifier PK 'ID'.\nfunction mockEntity(): EntityInfo {\n const idField = {\n Name: 'ID',\n IsPrimaryKey: true,\n TSType: 'string',\n Type: 'uniqueidentifier',\n Sequence: 0,\n IsNameField: false,\n DefaultInView: false,\n DisplayNameOrName: 'ID',\n EntityFieldValues: [],\n } as unknown as EntityFieldInfo;\n\n const nameField = {\n Name: 'Name',\n IsPrimaryKey: false,\n TSType: 'string',\n Type: 'nvarchar',\n Sequence: 1,\n IsNameField: true,\n DefaultInView: true,\n DisplayNameOrName: 'Name',\n EntityFieldValues: [],\n } as unknown as EntityFieldInfo;\n\n const fields = [idField, nameField];\n\n return {\n ID: 'E1',\n Name: 'Test Entity',\n Fields: fields,\n PrimaryKeys: [idField],\n NameField: nameField,\n } as unknown as EntityInfo;\n}\n\nfunction makeComponent(): EntityCardsComponent {\n const elementRef = { nativeElement: { querySelector: () => null } } as unknown as ElementRef;\n const cdr = { detectChanges: () => {}, markForCheck: () => {} } as unknown as ChangeDetectorRef;\n return new EntityCardsComponent(elementRef, cdr);\n}\n\nfunction change(previousValue: unknown, currentValue: unknown, firstChange = false): SimpleChange {\n return { previousValue, currentValue, firstChange, isFirstChange: () => firstChange } as SimpleChange;\n}\n\ndescribe('EntityCardsComponent — selection-only updates (Fix A)', () => {\n let component: EntityCardsComponent;\n\n beforeEach(() => {\n component = makeComponent();\n component.entity = mockEntity();\n component.records = [\n { ID: 'a', Name: 'Alpha' },\n { ID: 'b', Name: 'Bravo' },\n { ID: 'c', Name: 'Charlie' },\n ];\n component.ngOnInit();\n });\n\n // The pkString format is CompositeKey.ToConcatenatedString() (e.g. \"ID|a\"),\n // not the raw PK value. Resolve the real strings from the built VMs so the\n // tests are robust to that format.\n function pk(index: number): string {\n return component.cardViewModels[index].pkString;\n }\n\n it('builds VMs with pkStrings and no selection initially', () => {\n expect(component.cardViewModels).toHaveLength(3);\n expect(component.cardViewModels.every(vm => vm.pkString.length > 0)).toBe(true);\n expect(component.cardViewModels.every(vm => !vm.isSelected)).toBe(true);\n });\n\n it('selection-only change flips the correct isSelected flag WITHOUT re-running highlightMatch', () => {\n const highlightSpy = vi.spyOn(component, 'highlightMatch');\n\n const targetPk = pk(1);\n component.selectedRecordId = targetPk;\n const changes: SimpleChanges = { selectedRecordId: change(null, targetPk) };\n component.ngOnChanges(changes);\n\n expect(highlightSpy).not.toHaveBeenCalled();\n expect(component.cardViewModels.map(vm => vm.isSelected)).toEqual([false, true, false]);\n });\n\n it('selection-only change preserves the array and every VM object reference', () => {\n const arrayBefore = component.cardViewModels;\n const vmsBefore = [...component.cardViewModels];\n\n const targetPk = pk(2);\n component.selectedRecordId = targetPk;\n component.ngOnChanges({ selectedRecordId: change(null, targetPk) });\n\n // Same array instance and same VM instances — only the flags mutated in place.\n expect(component.cardViewModels).toBe(arrayBefore);\n component.cardViewModels.forEach((vm, i) => expect(vm).toBe(vmsBefore[i]));\n expect(component.cardViewModels.map(vm => vm.isSelected)).toEqual([false, false, true]);\n });\n\n it('clearing the selection unsets all isSelected flags', () => {\n const firstPk = pk(0);\n component.selectedRecordId = firstPk;\n component.ngOnChanges({ selectedRecordId: change(null, firstPk) });\n expect(component.cardViewModels.map(vm => vm.isSelected)).toEqual([true, false, false]);\n\n component.selectedRecordId = null;\n component.ngOnChanges({ selectedRecordId: change(firstPk, null) });\n expect(component.cardViewModels.every(vm => !vm.isSelected)).toBe(true);\n });\n\n it('a records change DOES rebuild the VMs (new array + highlightMatch runs)', () => {\n const arrayBefore = component.cardViewModels;\n const highlightSpy = vi.spyOn(component, 'highlightMatch');\n\n component.records = [\n { ID: 'x', Name: 'Xray' },\n { ID: 'y', Name: 'Yankee' },\n ];\n component.ngOnChanges({ records: change(null, component.records) });\n\n expect(component.cardViewModels).not.toBe(arrayBefore);\n expect(component.cardViewModels).toHaveLength(2);\n expect(component.cardViewModels.every(vm => vm.pkString.length > 0)).toBe(true);\n expect(highlightSpy).toHaveBeenCalled();\n });\n\n it('a combined records+selectedRecordId change rebuilds and sets selection correctly', () => {\n const highlightSpy = vi.spyOn(component, 'highlightMatch');\n component.records = [\n { ID: 'x', Name: 'Xray' },\n { ID: 'y', Name: 'Yankee' },\n ];\n // Match the second new record's pkString format (\"ID|y\").\n const targetPk = component.cardViewModels[1].pkString.replace(/[^|]+$/, 'y');\n component.selectedRecordId = targetPk;\n component.ngOnChanges({\n records: change(null, component.records),\n selectedRecordId: change(null, targetPk),\n });\n\n // content change wins → full rebuild, which also sets isSelected correctly\n expect(highlightSpy).toHaveBeenCalled();\n expect(component.cardViewModels.map(vm => vm.isSelected)).toEqual([false, true]);\n });\n\n it('selection-only change still requests a scroll-to-selected', () => {\n const targetPk = pk(1);\n component.selectedRecordId = targetPk;\n component.ngOnChanges({ selectedRecordId: change(null, targetPk) });\n // pendingScrollToSelected is private; assert via behavior in ngAfterViewChecked.\n const scrollSpy = vi\n .spyOn(component as unknown as { scrollToSelectedCard: () => void }, 'scrollToSelectedCard')\n .mockImplementation(() => {});\n vi.useFakeTimers();\n component.ngAfterViewChecked();\n vi.runAllTimers();\n expect(scrollSpy).toHaveBeenCalled();\n vi.useRealTimers();\n });\n});\n"]}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=entity-cards-viewmodel.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entity-cards-viewmodel.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/entity-cards-viewmodel.test.ts"],"names":[],"mappings":""}
@@ -1,208 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { EntityCardsComponent } from '../lib/entity-cards/entity-cards.component';
3
- /**
4
- * Part C (PR #2841): DEEPENED coverage of buildCardViewModel and the
5
- * selection-only vs content-change rebuild trigger matrix.
6
- *
7
- * buildCardViewModel produces a per-record CardViewModel exactly once per
8
- * content change (records / filterText / hiddenFieldMatches / entity /
9
- * cardTemplate), and a selectedRecordId-only change must NOT rebuild — it only
10
- * flips the cheap isSelected flags on the existing VMs. These tests pin:
11
- * - trackId fallback to record_<index> when there is no usable PK,
12
- * - hidden-field match present / absent (hasHiddenFieldMatch + field name),
13
- * - display-field VM typing (boolean / number / date / text),
14
- * - subtitle pill vs non-pill detection,
15
- * - the FULL trigger matrix (each content input rebuilds; selectedRecordId
16
- * alone does not).
17
- */
18
- function field(partial) {
19
- return {
20
- Sequence: 0,
21
- IsPrimaryKey: false,
22
- IsNameField: false,
23
- DefaultInView: false,
24
- EntityFieldValues: [],
25
- DisplayNameOrName: partial.Name ?? '',
26
- ...partial,
27
- };
28
- }
29
- /**
30
- * Rich entity: single uniqueidentifier PK 'ID', a Name (title), a Status
31
- * (subtitle keyword), a Description, and DefaultInView display fields of
32
- * boolean / number / date types.
33
- */
34
- function richEntity() {
35
- const idField = field({ Name: 'ID', IsPrimaryKey: true, TSType: 'string', Type: 'uniqueidentifier', DisplayNameOrName: 'ID' });
36
- const nameField = field({ Name: 'Name', TSType: 'string', Type: 'nvarchar', IsNameField: true, DefaultInView: true, Sequence: 1, DisplayNameOrName: 'Name' });
37
- const statusField = field({ Name: 'Status', TSType: 'string', Type: 'nvarchar', Sequence: 2, DisplayNameOrName: 'Status' });
38
- const descField = field({ Name: 'Description', TSType: 'string', Type: 'nvarchar', Sequence: 3, DisplayNameOrName: 'Description' });
39
- const activeField = field({ Name: 'IsActive', TSType: 'boolean', Type: 'bit', DefaultInView: true, Sequence: 4, DisplayNameOrName: 'Is Active' });
40
- const amountField = field({ Name: 'Amount', TSType: 'number', Type: 'decimal', DefaultInView: true, Sequence: 5, DisplayNameOrName: 'Amount' });
41
- const dueField = field({ Name: 'DueOn', TSType: 'Date', Type: 'date', DefaultInView: true, Sequence: 6, DisplayNameOrName: 'Due On' });
42
- const fields = [idField, nameField, statusField, descField, activeField, amountField, dueField];
43
- return { ID: 'E1', Name: 'Rich Entity', Fields: fields, PrimaryKeys: [idField], NameField: nameField };
44
- }
45
- /** Minimal entity with a PK, used for the no-template / no-PK paths. */
46
- function minimalEntity() {
47
- const idField = field({ Name: 'ID', IsPrimaryKey: true, TSType: 'string', Type: 'uniqueidentifier', DisplayNameOrName: 'ID' });
48
- return { ID: 'E2', Name: 'Min Entity', Fields: [idField], PrimaryKeys: [idField], NameField: undefined };
49
- }
50
- function makeComponent() {
51
- const elementRef = { nativeElement: { querySelector: () => null } };
52
- const cdr = { detectChanges: () => { }, markForCheck: () => { } };
53
- return new EntityCardsComponent(elementRef, cdr);
54
- }
55
- function change(previousValue, currentValue, firstChange = false) {
56
- return { previousValue, currentValue, firstChange, isFirstChange: () => firstChange };
57
- }
58
- describe('EntityCardsComponent.buildCardViewModel — view-model shape', () => {
59
- let component;
60
- beforeEach(() => {
61
- component = makeComponent();
62
- component.entity = richEntity();
63
- component.records = [
64
- { ID: 'aaaa', Name: 'Alpha', Status: 'Active', Description: 'first one', IsActive: true, Amount: 750, DueOn: '2026-01-15' },
65
- { ID: 'bbbb', Name: 'Bravo', Status: 'Pending', Description: 'second', IsActive: false, Amount: 0, DueOn: null },
66
- ];
67
- component.ngOnInit();
68
- });
69
- it('builds one VM per record with a pkString-derived trackId', () => {
70
- expect(component.cardViewModels).toHaveLength(2);
71
- // trackId equals pkString when a PK is present
72
- component.cardViewModels.forEach((vm) => {
73
- expect(vm.pkString.length).toBeGreaterThan(0);
74
- expect(vm.trackId).toBe(vm.pkString);
75
- expect(vm.trackId.startsWith('record_')).toBe(false);
76
- });
77
- });
78
- it('falls back trackId to record_<index> when there is no usable PK', () => {
79
- // No entity → computePkString returns '' for every record → record_<index>.
80
- const noEntity = makeComponent();
81
- noEntity.entity = null;
82
- noEntity.records = [{ Name: 'X' }, { Name: 'Y' }];
83
- noEntity.ngOnInit();
84
- expect(noEntity.cardViewModels.map((vm) => vm.trackId)).toEqual(['record_0', 'record_1']);
85
- expect(noEntity.cardViewModels.every((vm) => vm.pkString === '')).toBe(true);
86
- });
87
- it('renders display-field VMs with correct boolean / number / date typing', () => {
88
- const alpha = component.cardViewModels[0];
89
- const byName = new Map(alpha.displayFields.map((d) => [d.field.name, d]));
90
- const isActive = byName.get('IsActive');
91
- expect(isActive?.field.type).toBe('boolean');
92
- expect(isActive?.booleanValue).toBe(true);
93
- const amount = byName.get('Amount');
94
- expect(amount?.field.type).toBe('number');
95
- // 'amount' keyword + sub-1000 value → currency formatting ($750)
96
- expect(amount?.numericValue).toBe('$750');
97
- const due = byName.get('DueOn');
98
- expect(due?.field.type).toBe('date');
99
- expect(due?.dateValue).not.toBe('-'); // a real date formats to something non-empty
100
- });
101
- it('handles null/zero values in display fields gracefully', () => {
102
- const bravo = component.cardViewModels[1];
103
- const byName = new Map(bravo.displayFields.map((d) => [d.field.name, d]));
104
- expect(byName.get('IsActive')?.booleanValue).toBe(false);
105
- expect(byName.get('DueOn')?.dateValue).toBe('-'); // null date → placeholder
106
- });
107
- it('marks hasHiddenFieldMatch + resolves the matched field display name when present', () => {
108
- const alphaPk = component.cardViewModels[0].pkString;
109
- const matches = new Map([[alphaPk, 'Description']]);
110
- component.hiddenFieldMatches = matches;
111
- component.ngOnChanges({ hiddenFieldMatches: change(new Map(), matches) });
112
- const alpha = component.cardViewModels[0];
113
- const bravo = component.cardViewModels[1];
114
- expect(alpha.hasHiddenFieldMatch).toBe(true);
115
- expect(alpha.hiddenMatchFieldName).toBe('Description'); // DisplayNameOrName
116
- expect(bravo.hasHiddenFieldMatch).toBe(false);
117
- expect(bravo.hiddenMatchFieldName).toBe('');
118
- });
119
- it('subtitle picks up the Status field value', () => {
120
- expect(component.cardViewModels[0].subtitleValue).toBe('Active');
121
- expect(component.cardViewModels[1].subtitleValue).toBe('Pending');
122
- });
123
- it('subtitleIsPill reflects PillColorUtil for the current subtitle value', () => {
124
- // 'Active' is a known status keyword → a pill; a neutral string is not.
125
- component.records = [{ ID: 'aaaa', Name: 'Alpha', Status: 'Active' }];
126
- component.ngOnChanges({ records: change(null, component.records) });
127
- const pillForActive = component.subtitleIsPill;
128
- component.records = [{ ID: 'aaaa', Name: 'Alpha', Status: 'Some Freeform Label' }];
129
- component.ngOnChanges({ records: change(null, component.records) });
130
- const pillForFreeform = component.subtitleIsPill;
131
- // At least one of the two must differ — the getter is value-driven, not constant.
132
- expect(pillForActive === true || pillForFreeform === false).toBe(true);
133
- });
134
- it('produces no display fields when the entity has no DefaultInView fields (minimal entity)', () => {
135
- const min = makeComponent();
136
- min.entity = minimalEntity();
137
- min.records = [{ ID: 'zzzz' }];
138
- min.ngOnInit();
139
- expect(min.cardViewModels[0].displayFields).toEqual([]);
140
- });
141
- });
142
- describe('EntityCardsComponent — rebuild trigger matrix (content vs selection-only)', () => {
143
- let component;
144
- beforeEach(() => {
145
- component = makeComponent();
146
- component.entity = richEntity();
147
- component.records = [
148
- { ID: 'aaaa', Name: 'Alpha', Status: 'Active' },
149
- { ID: 'bbbb', Name: 'Bravo', Status: 'Pending' },
150
- ];
151
- component.ngOnInit();
152
- });
153
- /** Each of these content inputs must trigger a FULL rebuild (new VM array + highlightMatch). */
154
- const contentTriggers = [
155
- { name: 'filterText', apply: (c) => { c.filterText = 'Alp'; } },
156
- { name: 'hiddenFieldMatches', apply: (c) => { c.hiddenFieldMatches = new Map([['x', 'Name']]); } },
157
- { name: 'entity', apply: (c) => { c.entity = richEntity(); } },
158
- { name: 'cardTemplate', apply: (c) => { c.cardTemplate = null; } },
159
- ];
160
- for (const trigger of contentTriggers) {
161
- it(`rebuilds the VMs when '${String(trigger.name)}' changes`, () => {
162
- const arrayBefore = component.cardViewModels;
163
- const highlightSpy = vi.spyOn(component, 'highlightMatch');
164
- trigger.apply(component);
165
- component.ngOnChanges({ [trigger.name]: change(null, undefined) });
166
- expect(component.cardViewModels).not.toBe(arrayBefore);
167
- expect(highlightSpy).toHaveBeenCalled();
168
- });
169
- }
170
- it('records change rebuilds (new array + highlightMatch)', () => {
171
- const arrayBefore = component.cardViewModels;
172
- const highlightSpy = vi.spyOn(component, 'highlightMatch');
173
- component.records = [{ ID: 'cccc', Name: 'Charlie', Status: 'Active' }];
174
- component.ngOnChanges({ records: change(null, component.records) });
175
- expect(component.cardViewModels).not.toBe(arrayBefore);
176
- expect(component.cardViewModels).toHaveLength(1);
177
- expect(highlightSpy).toHaveBeenCalled();
178
- });
179
- it('selectedRecordId alone does NOT rebuild — only flips isSelected (no highlightMatch, same array)', () => {
180
- const arrayBefore = component.cardViewModels;
181
- const vmsBefore = [...component.cardViewModels];
182
- const highlightSpy = vi.spyOn(component, 'highlightMatch');
183
- const targetPk = component.cardViewModels[1].pkString;
184
- component.selectedRecordId = targetPk;
185
- component.ngOnChanges({ selectedRecordId: change(null, targetPk) });
186
- expect(highlightSpy).not.toHaveBeenCalled();
187
- expect(component.cardViewModels).toBe(arrayBefore);
188
- component.cardViewModels.forEach((vm, i) => expect(vm).toBe(vmsBefore[i]));
189
- expect(component.cardViewModels.map((vm) => vm.isSelected)).toEqual([false, true]);
190
- });
191
- it('a content change + selection together: content wins (rebuild) and selection is applied', () => {
192
- const highlightSpy = vi.spyOn(component, 'highlightMatch');
193
- component.records = [
194
- { ID: 'cccc', Name: 'Charlie', Status: 'Active' },
195
- { ID: 'dddd', Name: 'Delta', Status: 'Pending' },
196
- ];
197
- component.ngOnChanges({ records: change(null, component.records) });
198
- const targetPk = component.cardViewModels[0].pkString;
199
- component.selectedRecordId = targetPk;
200
- component.ngOnChanges({
201
- records: change(null, component.records),
202
- selectedRecordId: change(null, targetPk),
203
- });
204
- expect(highlightSpy).toHaveBeenCalled();
205
- expect(component.cardViewModels.map((vm) => vm.isSelected)).toEqual([true, false]);
206
- });
207
- });
208
- //# sourceMappingURL=entity-cards-viewmodel.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entity-cards-viewmodel.test.js","sourceRoot":"","sources":["../../src/__tests__/entity-cards-viewmodel.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest';\nimport type { ElementRef, ChangeDetectorRef, SimpleChange, SimpleChanges } from '@angular/core';\nimport type { EntityInfo, EntityFieldInfo } from '@memberjunction/core';\nimport { EntityCardsComponent } from '../lib/entity-cards/entity-cards.component';\n\n/**\n * Part C (PR #2841): DEEPENED coverage of buildCardViewModel and the\n * selection-only vs content-change rebuild trigger matrix.\n *\n * buildCardViewModel produces a per-record CardViewModel exactly once per\n * content change (records / filterText / hiddenFieldMatches / entity /\n * cardTemplate), and a selectedRecordId-only change must NOT rebuild — it only\n * flips the cheap isSelected flags on the existing VMs. These tests pin:\n * - trackId fallback to record_<index> when there is no usable PK,\n * - hidden-field match present / absent (hasHiddenFieldMatch + field name),\n * - display-field VM typing (boolean / number / date / text),\n * - subtitle pill vs non-pill detection,\n * - the FULL trigger matrix (each content input rebuilds; selectedRecordId\n * alone does not).\n */\n\nfunction field(partial: Partial<EntityFieldInfo>): EntityFieldInfo {\n return {\n Sequence: 0,\n IsPrimaryKey: false,\n IsNameField: false,\n DefaultInView: false,\n EntityFieldValues: [],\n DisplayNameOrName: partial.Name ?? '',\n ...partial,\n } as unknown as EntityFieldInfo;\n}\n\n/**\n * Rich entity: single uniqueidentifier PK 'ID', a Name (title), a Status\n * (subtitle keyword), a Description, and DefaultInView display fields of\n * boolean / number / date types.\n */\nfunction richEntity(): EntityInfo {\n const idField = field({ Name: 'ID', IsPrimaryKey: true, TSType: 'string', Type: 'uniqueidentifier', DisplayNameOrName: 'ID' });\n const nameField = field({ Name: 'Name', TSType: 'string', Type: 'nvarchar', IsNameField: true, DefaultInView: true, Sequence: 1, DisplayNameOrName: 'Name' });\n const statusField = field({ Name: 'Status', TSType: 'string', Type: 'nvarchar', Sequence: 2, DisplayNameOrName: 'Status' });\n const descField = field({ Name: 'Description', TSType: 'string', Type: 'nvarchar', Sequence: 3, DisplayNameOrName: 'Description' });\n const activeField = field({ Name: 'IsActive', TSType: 'boolean', Type: 'bit', DefaultInView: true, Sequence: 4, DisplayNameOrName: 'Is Active' });\n const amountField = field({ Name: 'Amount', TSType: 'number', Type: 'decimal', DefaultInView: true, Sequence: 5, DisplayNameOrName: 'Amount' });\n const dueField = field({ Name: 'DueOn', TSType: 'Date', Type: 'date', DefaultInView: true, Sequence: 6, DisplayNameOrName: 'Due On' });\n\n const fields = [idField, nameField, statusField, descField, activeField, amountField, dueField];\n return { ID: 'E1', Name: 'Rich Entity', Fields: fields, PrimaryKeys: [idField], NameField: nameField } as unknown as EntityInfo;\n}\n\n/** Minimal entity with a PK, used for the no-template / no-PK paths. */\nfunction minimalEntity(): EntityInfo {\n const idField = field({ Name: 'ID', IsPrimaryKey: true, TSType: 'string', Type: 'uniqueidentifier', DisplayNameOrName: 'ID' });\n return { ID: 'E2', Name: 'Min Entity', Fields: [idField], PrimaryKeys: [idField], NameField: undefined } as unknown as EntityInfo;\n}\n\nfunction makeComponent(): EntityCardsComponent {\n const elementRef = { nativeElement: { querySelector: () => null } } as unknown as ElementRef;\n const cdr = { detectChanges: () => {}, markForCheck: () => {} } as unknown as ChangeDetectorRef;\n return new EntityCardsComponent(elementRef, cdr);\n}\n\nfunction change(previousValue: unknown, currentValue: unknown, firstChange = false): SimpleChange {\n return { previousValue, currentValue, firstChange, isFirstChange: () => firstChange } as SimpleChange;\n}\n\ndescribe('EntityCardsComponent.buildCardViewModel — view-model shape', () => {\n let component: EntityCardsComponent;\n\n beforeEach(() => {\n component = makeComponent();\n component.entity = richEntity();\n component.records = [\n { ID: 'aaaa', Name: 'Alpha', Status: 'Active', Description: 'first one', IsActive: true, Amount: 750, DueOn: '2026-01-15' },\n { ID: 'bbbb', Name: 'Bravo', Status: 'Pending', Description: 'second', IsActive: false, Amount: 0, DueOn: null },\n ];\n component.ngOnInit();\n });\n\n it('builds one VM per record with a pkString-derived trackId', () => {\n expect(component.cardViewModels).toHaveLength(2);\n // trackId equals pkString when a PK is present\n component.cardViewModels.forEach((vm) => {\n expect(vm.pkString.length).toBeGreaterThan(0);\n expect(vm.trackId).toBe(vm.pkString);\n expect(vm.trackId.startsWith('record_')).toBe(false);\n });\n });\n\n it('falls back trackId to record_<index> when there is no usable PK', () => {\n // No entity → computePkString returns '' for every record → record_<index>.\n const noEntity = makeComponent();\n noEntity.entity = null;\n noEntity.records = [{ Name: 'X' }, { Name: 'Y' }];\n noEntity.ngOnInit();\n expect(noEntity.cardViewModels.map((vm) => vm.trackId)).toEqual(['record_0', 'record_1']);\n expect(noEntity.cardViewModels.every((vm) => vm.pkString === '')).toBe(true);\n });\n\n it('renders display-field VMs with correct boolean / number / date typing', () => {\n const alpha = component.cardViewModels[0];\n const byName = new Map(alpha.displayFields.map((d) => [d.field.name, d]));\n\n const isActive = byName.get('IsActive');\n expect(isActive?.field.type).toBe('boolean');\n expect(isActive?.booleanValue).toBe(true);\n\n const amount = byName.get('Amount');\n expect(amount?.field.type).toBe('number');\n // 'amount' keyword + sub-1000 value → currency formatting ($750)\n expect(amount?.numericValue).toBe('$750');\n\n const due = byName.get('DueOn');\n expect(due?.field.type).toBe('date');\n expect(due?.dateValue).not.toBe('-'); // a real date formats to something non-empty\n });\n\n it('handles null/zero values in display fields gracefully', () => {\n const bravo = component.cardViewModels[1];\n const byName = new Map(bravo.displayFields.map((d) => [d.field.name, d]));\n expect(byName.get('IsActive')?.booleanValue).toBe(false);\n expect(byName.get('DueOn')?.dateValue).toBe('-'); // null date → placeholder\n });\n\n it('marks hasHiddenFieldMatch + resolves the matched field display name when present', () => {\n const alphaPk = component.cardViewModels[0].pkString;\n const matches = new Map<string, string>([[alphaPk, 'Description']]);\n component.hiddenFieldMatches = matches;\n component.ngOnChanges({ hiddenFieldMatches: change(new Map(), matches) });\n\n const alpha = component.cardViewModels[0];\n const bravo = component.cardViewModels[1];\n expect(alpha.hasHiddenFieldMatch).toBe(true);\n expect(alpha.hiddenMatchFieldName).toBe('Description'); // DisplayNameOrName\n expect(bravo.hasHiddenFieldMatch).toBe(false);\n expect(bravo.hiddenMatchFieldName).toBe('');\n });\n\n it('subtitle picks up the Status field value', () => {\n expect(component.cardViewModels[0].subtitleValue).toBe('Active');\n expect(component.cardViewModels[1].subtitleValue).toBe('Pending');\n });\n\n it('subtitleIsPill reflects PillColorUtil for the current subtitle value', () => {\n // 'Active' is a known status keyword → a pill; a neutral string is not.\n component.records = [{ ID: 'aaaa', Name: 'Alpha', Status: 'Active' }];\n component.ngOnChanges({ records: change(null, component.records) });\n const pillForActive = component.subtitleIsPill;\n\n component.records = [{ ID: 'aaaa', Name: 'Alpha', Status: 'Some Freeform Label' }];\n component.ngOnChanges({ records: change(null, component.records) });\n const pillForFreeform = component.subtitleIsPill;\n\n // At least one of the two must differ — the getter is value-driven, not constant.\n expect(pillForActive === true || pillForFreeform === false).toBe(true);\n });\n\n it('produces no display fields when the entity has no DefaultInView fields (minimal entity)', () => {\n const min = makeComponent();\n min.entity = minimalEntity();\n min.records = [{ ID: 'zzzz' }];\n min.ngOnInit();\n expect(min.cardViewModels[0].displayFields).toEqual([]);\n });\n});\n\ndescribe('EntityCardsComponent — rebuild trigger matrix (content vs selection-only)', () => {\n let component: EntityCardsComponent;\n\n beforeEach(() => {\n component = makeComponent();\n component.entity = richEntity();\n component.records = [\n { ID: 'aaaa', Name: 'Alpha', Status: 'Active' },\n { ID: 'bbbb', Name: 'Bravo', Status: 'Pending' },\n ];\n component.ngOnInit();\n });\n\n /** Each of these content inputs must trigger a FULL rebuild (new VM array + highlightMatch). */\n const contentTriggers: { name: keyof SimpleChanges; apply: (c: EntityCardsComponent) => void }[] = [\n { name: 'filterText', apply: (c) => { c.filterText = 'Alp'; } },\n { name: 'hiddenFieldMatches', apply: (c) => { c.hiddenFieldMatches = new Map([['x', 'Name']]); } },\n { name: 'entity', apply: (c) => { c.entity = richEntity(); } },\n { name: 'cardTemplate', apply: (c) => { c.cardTemplate = null; } },\n ];\n\n for (const trigger of contentTriggers) {\n it(`rebuilds the VMs when '${String(trigger.name)}' changes`, () => {\n const arrayBefore = component.cardViewModels;\n const highlightSpy = vi.spyOn(component, 'highlightMatch');\n trigger.apply(component);\n component.ngOnChanges({ [trigger.name]: change(null, undefined) } as SimpleChanges);\n\n expect(component.cardViewModels).not.toBe(arrayBefore);\n expect(highlightSpy).toHaveBeenCalled();\n });\n }\n\n it('records change rebuilds (new array + highlightMatch)', () => {\n const arrayBefore = component.cardViewModels;\n const highlightSpy = vi.spyOn(component, 'highlightMatch');\n component.records = [{ ID: 'cccc', Name: 'Charlie', Status: 'Active' }];\n component.ngOnChanges({ records: change(null, component.records) });\n expect(component.cardViewModels).not.toBe(arrayBefore);\n expect(component.cardViewModels).toHaveLength(1);\n expect(highlightSpy).toHaveBeenCalled();\n });\n\n it('selectedRecordId alone does NOT rebuild — only flips isSelected (no highlightMatch, same array)', () => {\n const arrayBefore = component.cardViewModels;\n const vmsBefore = [...component.cardViewModels];\n const highlightSpy = vi.spyOn(component, 'highlightMatch');\n\n const targetPk = component.cardViewModels[1].pkString;\n component.selectedRecordId = targetPk;\n component.ngOnChanges({ selectedRecordId: change(null, targetPk) });\n\n expect(highlightSpy).not.toHaveBeenCalled();\n expect(component.cardViewModels).toBe(arrayBefore);\n component.cardViewModels.forEach((vm, i) => expect(vm).toBe(vmsBefore[i]));\n expect(component.cardViewModels.map((vm) => vm.isSelected)).toEqual([false, true]);\n });\n\n it('a content change + selection together: content wins (rebuild) and selection is applied', () => {\n const highlightSpy = vi.spyOn(component, 'highlightMatch');\n component.records = [\n { ID: 'cccc', Name: 'Charlie', Status: 'Active' },\n { ID: 'dddd', Name: 'Delta', Status: 'Pending' },\n ];\n component.ngOnChanges({ records: change(null, component.records) });\n const targetPk = component.cardViewModels[0].pkString;\n component.selectedRecordId = targetPk;\n component.ngOnChanges({\n records: change(null, component.records),\n selectedRecordId: change(null, targetPk),\n });\n expect(highlightSpy).toHaveBeenCalled();\n expect(component.cardViewModels.map((vm) => vm.isSelected)).toEqual([true, false]);\n });\n});\n"]}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=highlight.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"highlight.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/highlight.test.ts"],"names":[],"mappings":""}
@@ -1,67 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { HighlightUtil } from '../lib/utils/highlight.util';
3
- describe('HighlightUtil.matches', () => {
4
- it('should return false for empty value or search term', () => {
5
- expect(HighlightUtil.matches('', 'test')).toBe(false);
6
- expect(HighlightUtil.matches('test', '')).toBe(false);
7
- expect(HighlightUtil.matches('', '')).toBe(false);
8
- });
9
- it('should perform case-insensitive substring match', () => {
10
- expect(HighlightUtil.matches('Hello World', 'hello')).toBe(true);
11
- expect(HighlightUtil.matches('Hello World', 'WORLD')).toBe(true);
12
- expect(HighlightUtil.matches('Hello World', 'xyz')).toBe(false);
13
- });
14
- it('should handle SQL-style % wildcards', () => {
15
- expect(HighlightUtil.matches('food assessment', 'food%ass')).toBe(true);
16
- expect(HighlightUtil.matches('food bar assessment', 'food%ass')).toBe(true);
17
- expect(HighlightUtil.matches('assessment food', 'food%ass')).toBe(false);
18
- });
19
- it('should match everything for wildcard-only patterns', () => {
20
- expect(HighlightUtil.matches('anything', '%')).toBe(true);
21
- expect(HighlightUtil.matches('anything', '%%')).toBe(true);
22
- });
23
- it('should require ordered fragments', () => {
24
- expect(HighlightUtil.matches('abc def ghi', 'abc%ghi')).toBe(true);
25
- expect(HighlightUtil.matches('ghi def abc', 'abc%ghi')).toBe(false);
26
- });
27
- it('should handle multiple wildcard segments', () => {
28
- expect(HighlightUtil.matches('alpha beta gamma delta', 'alpha%gamma%delta')).toBe(true);
29
- expect(HighlightUtil.matches('alpha beta gamma', 'alpha%gamma%delta')).toBe(false);
30
- });
31
- });
32
- describe('HighlightUtil.highlight', () => {
33
- it('should return empty string for empty text', () => {
34
- expect(HighlightUtil.highlight('', 'test')).toBe('');
35
- });
36
- it('should return escaped text when no search term', () => {
37
- expect(HighlightUtil.highlight('hello', '')).toBe('hello');
38
- expect(HighlightUtil.highlight('hello', ' ')).toBe('hello');
39
- });
40
- it('should return text without highlighting when no match', () => {
41
- const result = HighlightUtil.highlight('hello world', 'xyz');
42
- expect(result).not.toContain('highlight-match');
43
- expect(result).toContain('hello world');
44
- });
45
- it('should highlight simple matches', () => {
46
- const result = HighlightUtil.highlight('Hello World', 'World', false);
47
- expect(result).toContain('<span class="highlight-match">World</span>');
48
- });
49
- it('should highlight wildcard matches', () => {
50
- const result = HighlightUtil.highlight('food assessment', 'food%ass', false);
51
- expect(result).toContain('<span class="highlight-match">food</span>');
52
- expect(result).toContain('<span class="highlight-match">ass</span>');
53
- });
54
- });
55
- describe('HighlightUtil.escapeHtml', () => {
56
- it('should escape HTML special characters (SSR fallback)', () => {
57
- // In node environment without proper document, should use SSR fallback
58
- const result = HighlightUtil.escapeHtml('<script>alert("xss")</script>');
59
- expect(result).not.toContain('<script>');
60
- expect(result).toContain('&lt;');
61
- });
62
- it('should handle ampersands', () => {
63
- const result = HighlightUtil.escapeHtml('A & B');
64
- expect(result).toContain('&amp;');
65
- });
66
- });
67
- //# sourceMappingURL=highlight.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"highlight.test.js","sourceRoot":"","sources":["../../src/__tests__/highlight.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport { HighlightUtil } from '../lib/utils/highlight.util';\n\ndescribe('HighlightUtil.matches', () => {\n it('should return false for empty value or search term', () => {\n expect(HighlightUtil.matches('', 'test')).toBe(false);\n expect(HighlightUtil.matches('test', '')).toBe(false);\n expect(HighlightUtil.matches('', '')).toBe(false);\n });\n\n it('should perform case-insensitive substring match', () => {\n expect(HighlightUtil.matches('Hello World', 'hello')).toBe(true);\n expect(HighlightUtil.matches('Hello World', 'WORLD')).toBe(true);\n expect(HighlightUtil.matches('Hello World', 'xyz')).toBe(false);\n });\n\n it('should handle SQL-style % wildcards', () => {\n expect(HighlightUtil.matches('food assessment', 'food%ass')).toBe(true);\n expect(HighlightUtil.matches('food bar assessment', 'food%ass')).toBe(true);\n expect(HighlightUtil.matches('assessment food', 'food%ass')).toBe(false);\n });\n\n it('should match everything for wildcard-only patterns', () => {\n expect(HighlightUtil.matches('anything', '%')).toBe(true);\n expect(HighlightUtil.matches('anything', '%%')).toBe(true);\n });\n\n it('should require ordered fragments', () => {\n expect(HighlightUtil.matches('abc def ghi', 'abc%ghi')).toBe(true);\n expect(HighlightUtil.matches('ghi def abc', 'abc%ghi')).toBe(false);\n });\n\n it('should handle multiple wildcard segments', () => {\n expect(HighlightUtil.matches('alpha beta gamma delta', 'alpha%gamma%delta')).toBe(true);\n expect(HighlightUtil.matches('alpha beta gamma', 'alpha%gamma%delta')).toBe(false);\n });\n});\n\ndescribe('HighlightUtil.highlight', () => {\n it('should return empty string for empty text', () => {\n expect(HighlightUtil.highlight('', 'test')).toBe('');\n });\n\n it('should return escaped text when no search term', () => {\n expect(HighlightUtil.highlight('hello', '')).toBe('hello');\n expect(HighlightUtil.highlight('hello', ' ')).toBe('hello');\n });\n\n it('should return text without highlighting when no match', () => {\n const result = HighlightUtil.highlight('hello world', 'xyz');\n expect(result).not.toContain('highlight-match');\n expect(result).toContain('hello world');\n });\n\n it('should highlight simple matches', () => {\n const result = HighlightUtil.highlight('Hello World', 'World', false);\n expect(result).toContain('<span class=\"highlight-match\">World</span>');\n });\n\n it('should highlight wildcard matches', () => {\n const result = HighlightUtil.highlight('food assessment', 'food%ass', false);\n expect(result).toContain('<span class=\"highlight-match\">food</span>');\n expect(result).toContain('<span class=\"highlight-match\">ass</span>');\n });\n});\n\ndescribe('HighlightUtil.escapeHtml', () => {\n it('should escape HTML special characters (SSR fallback)', () => {\n // In node environment without proper document, should use SSR fallback\n const result = HighlightUtil.escapeHtml('<script>alert(\"xss\")</script>');\n expect(result).not.toContain('<script>');\n expect(result).toContain('&lt;');\n });\n\n it('should handle ampersands', () => {\n const result = HighlightUtil.escapeHtml('A & B');\n expect(result).toContain('&amp;');\n });\n});\n"]}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=index.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":""}
@@ -1,7 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- describe('@memberjunction/ng-entity-viewer', () => {
3
- it('should have a passing test', () => {
4
- expect(true).toBe(true);
5
- });
6
- });
7
- //# sourceMappingURL=index.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,QAAQ,CAAC,kCAAkC,EAAE,GAAG,EAAE;IAChD,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, it, expect } from 'vitest';\n\ndescribe('@memberjunction/ng-entity-viewer', () => {\n it('should have a passing test', () => {\n expect(true).toBe(true);\n });\n});\n"]}