@dragonworks/ngx-dashboard-widgets 20.0.6 → 20.1.1

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 (34) hide show
  1. package/fesm2022/dragonworks-ngx-dashboard-widgets.mjs +2251 -0
  2. package/fesm2022/dragonworks-ngx-dashboard-widgets.mjs.map +1 -0
  3. package/index.d.ts +532 -0
  4. package/package.json +42 -31
  5. package/ng-package.json +0 -7
  6. package/src/lib/arrow-widget/arrow-state-dialog.component.ts +0 -187
  7. package/src/lib/arrow-widget/arrow-widget.component.html +0 -9
  8. package/src/lib/arrow-widget/arrow-widget.component.scss +0 -52
  9. package/src/lib/arrow-widget/arrow-widget.component.ts +0 -78
  10. package/src/lib/arrow-widget/arrow-widget.metadata.ts +0 -3
  11. package/src/lib/clock-widget/analog-clock/analog-clock.component.html +0 -66
  12. package/src/lib/clock-widget/analog-clock/analog-clock.component.scss +0 -103
  13. package/src/lib/clock-widget/analog-clock/analog-clock.component.ts +0 -120
  14. package/src/lib/clock-widget/clock-state-dialog.component.ts +0 -170
  15. package/src/lib/clock-widget/clock-widget.component.html +0 -16
  16. package/src/lib/clock-widget/clock-widget.component.scss +0 -160
  17. package/src/lib/clock-widget/clock-widget.component.ts +0 -87
  18. package/src/lib/clock-widget/clock-widget.metadata.ts +0 -42
  19. package/src/lib/clock-widget/digital-clock/__tests__/digital-clock.component.spec.ts +0 -276
  20. package/src/lib/clock-widget/digital-clock/digital-clock.component.html +0 -1
  21. package/src/lib/clock-widget/digital-clock/digital-clock.component.scss +0 -43
  22. package/src/lib/clock-widget/digital-clock/digital-clock.component.ts +0 -105
  23. package/src/lib/directives/__tests__/responsive-text.directive.spec.ts +0 -906
  24. package/src/lib/directives/responsive-text.directive.ts +0 -334
  25. package/src/lib/label-widget/__tests__/label-widget.component.spec.ts +0 -539
  26. package/src/lib/label-widget/label-state-dialog.component.ts +0 -385
  27. package/src/lib/label-widget/label-widget.component.html +0 -21
  28. package/src/lib/label-widget/label-widget.component.scss +0 -112
  29. package/src/lib/label-widget/label-widget.component.ts +0 -96
  30. package/src/lib/label-widget/label-widget.metadata.ts +0 -3
  31. package/src/public-api.ts +0 -7
  32. package/tsconfig.lib.json +0 -15
  33. package/tsconfig.lib.prod.json +0 -11
  34. package/tsconfig.spec.json +0 -14
@@ -1,906 +0,0 @@
1
- import { Component, DebugElement, PLATFORM_ID } from '@angular/core';
2
- import { ComponentFixture, TestBed, fakeAsync, tick, flush } from '@angular/core/testing';
3
- import { By } from '@angular/platform-browser';
4
- import { ResponsiveTextDirective } from '../responsive-text.directive';
5
-
6
- @Component({
7
- template: `
8
- <div class="container" [style.width.px]="containerWidth" [style.height.px]="containerHeight" [style.padding.px]="padding">
9
- <span
10
- responsiveText
11
- [minFontSize]="minFont"
12
- [maxFontSize]="maxFont"
13
- [lineHeight]="lineHeight"
14
- [observeMutations]="observeMutations"
15
- [debounceMs]="debounceMs">
16
- {{ text }}
17
- </span>
18
- </div>
19
- `,
20
- styles: [`
21
- .container {
22
- position: relative;
23
- box-sizing: border-box;
24
- }
25
- `],
26
- imports: [ResponsiveTextDirective]
27
- })
28
- class TestComponent {
29
- containerWidth = 200;
30
- containerHeight = 50;
31
- padding = 0;
32
- text = 'Sample text';
33
- minFont = 8;
34
- maxFont = 72;
35
- lineHeight = 1.1;
36
- observeMutations = true;
37
- debounceMs = 16;
38
- }
39
-
40
- describe('ResponsiveTextDirective', () => {
41
- let component: TestComponent;
42
- let fixture: ComponentFixture<TestComponent>;
43
- let directiveElement: DebugElement;
44
- let spanElement: HTMLElement;
45
- let containerElement: HTMLElement;
46
- let directive: ResponsiveTextDirective;
47
-
48
- // Mock objects
49
- let mockCanvas: HTMLCanvasElement;
50
- let mockCtx: jasmine.SpyObj<CanvasRenderingContext2D>;
51
- let mockResizeObserver: jasmine.SpyObj<ResizeObserver>;
52
- let mockMutationObserver: jasmine.SpyObj<MutationObserver>;
53
-
54
- beforeEach(async () => {
55
- // Setup canvas mocks
56
- mockCtx = jasmine.createSpyObj('CanvasRenderingContext2D', ['measureText']);
57
- mockCtx.measureText.and.returnValue({
58
- width: 100,
59
- fontBoundingBoxAscent: 10,
60
- fontBoundingBoxDescent: 3
61
- } as TextMetrics);
62
-
63
- // Add font property to mock context
64
- Object.defineProperty(mockCtx, 'font', {
65
- value: '16px sans-serif',
66
- writable: true,
67
- configurable: true
68
- });
69
-
70
- mockCanvas = jasmine.createSpyObj('HTMLCanvasElement', ['getContext']);
71
- (mockCanvas.getContext as jasmine.Spy).and.returnValue(mockCtx);
72
-
73
- // Setup observer mocks
74
- mockResizeObserver = jasmine.createSpyObj('ResizeObserver', ['observe', 'disconnect']);
75
- mockMutationObserver = jasmine.createSpyObj('MutationObserver', ['observe', 'disconnect']);
76
-
77
- // Mock global objects
78
- const originalCreateElement = document.createElement.bind(document);
79
- spyOn(document, 'createElement').and.callFake((tagName: string) => {
80
- if (tagName === 'canvas') {
81
- return mockCanvas;
82
- }
83
- return originalCreateElement(tagName);
84
- });
85
-
86
- spyOn(window, 'ResizeObserver').and.returnValue(mockResizeObserver);
87
- spyOn(window, 'MutationObserver').and.returnValue(mockMutationObserver);
88
-
89
- await TestBed.configureTestingModule({
90
- imports: [TestComponent, ResponsiveTextDirective]
91
- }).compileComponents();
92
-
93
- fixture = TestBed.createComponent(TestComponent);
94
- component = fixture.componentInstance;
95
-
96
- directiveElement = fixture.debugElement.query(By.directive(ResponsiveTextDirective));
97
- spanElement = directiveElement.nativeElement;
98
- containerElement = spanElement.parentElement!;
99
- directive = directiveElement.injector.get(ResponsiveTextDirective);
100
- });
101
-
102
- describe('Core Functionality', () => {
103
- beforeEach(() => {
104
- // Setup default canvas measurements
105
- mockCtx.measureText.and.returnValue({
106
- width: 100,
107
- fontBoundingBoxAscent: 10,
108
- fontBoundingBoxDescent: 3
109
- } as TextMetrics);
110
- });
111
-
112
- it('should create directive instance', () => {
113
- expect(directive).toBeTruthy();
114
- expect(spanElement).toBeTruthy();
115
- });
116
-
117
- it('should apply host styles correctly', () => {
118
- fixture.detectChanges();
119
-
120
- expect(spanElement.style.display).toBe('block');
121
- expect(spanElement.style.width).toBe('100%');
122
- expect(spanElement.style.whiteSpace).toBe('nowrap');
123
- expect(spanElement.style.overflow).toBe('visible');
124
- expect(spanElement.style.textOverflow).toBe('');
125
- });
126
-
127
- it('should set transition style on init', fakeAsync(() => {
128
- fixture.detectChanges();
129
- tick();
130
-
131
- expect(spanElement.style.transition).toContain('font-size');
132
- }));
133
-
134
- it('should calculate and apply font size based on container dimensions', fakeAsync(() => {
135
- component.containerWidth = 200;
136
- component.containerHeight = 50;
137
- component.text = 'Test';
138
- fixture.detectChanges();
139
- tick();
140
- flush();
141
-
142
- expect(mockCtx.measureText).toHaveBeenCalled();
143
- expect(spanElement.style.fontSize).toMatch(/\d+px/);
144
- expect(parseFloat(spanElement.style.fontSize)).toBeGreaterThan(0);
145
- }));
146
-
147
- it('should respect minimum font size constraint', fakeAsync(() => {
148
- component.minFont = 20;
149
- component.maxFont = 72;
150
- component.text = 'Very long text that should be constrained to minimum';
151
-
152
- // Mock very wide text to force minimum
153
- mockCtx.measureText.and.returnValue({
154
- width: 1000,
155
- fontBoundingBoxAscent: 20,
156
- fontBoundingBoxDescent: 5
157
- } as TextMetrics);
158
-
159
- fixture.detectChanges();
160
- tick();
161
- flush();
162
-
163
- const fontSize = parseFloat(spanElement.style.fontSize);
164
- expect(fontSize).toBeGreaterThanOrEqual(20);
165
- }));
166
-
167
- it('should respect maximum font size constraint', fakeAsync(() => {
168
- component.minFont = 8;
169
- component.maxFont = 24;
170
- component.text = 'A';
171
- component.containerWidth = 1000;
172
- component.containerHeight = 1000;
173
-
174
- // Mock small text to potentially exceed maximum
175
- mockCtx.measureText.and.returnValue({
176
- width: 10,
177
- fontBoundingBoxAscent: 8,
178
- fontBoundingBoxDescent: 2
179
- } as TextMetrics);
180
-
181
- fixture.detectChanges();
182
- tick();
183
- flush();
184
-
185
- const fontSize = parseFloat(spanElement.style.fontSize);
186
- expect(fontSize).toBeLessThanOrEqual(24);
187
- }));
188
-
189
- it('should handle empty text by setting minimum font size', fakeAsync(() => {
190
- component.text = '';
191
- component.minFont = 16;
192
- fixture.detectChanges();
193
- tick();
194
- flush();
195
-
196
- expect(spanElement.style.fontSize).toBe('16px');
197
- }));
198
-
199
- it('should handle whitespace-only text as empty', fakeAsync(() => {
200
- component.text = ' \n\t ';
201
- component.minFont = 16;
202
- fixture.detectChanges();
203
- tick();
204
- flush();
205
-
206
- expect(spanElement.style.fontSize).toBe('16px');
207
- }));
208
- });
209
-
210
- describe('Input Signal Reactivity', () => {
211
- beforeEach(() => {
212
- mockCtx.measureText.and.returnValue({
213
- width: 50,
214
- fontBoundingBoxAscent: 10,
215
- fontBoundingBoxDescent: 3
216
- } as TextMetrics);
217
- });
218
-
219
- it('should react to min input changes', fakeAsync(() => {
220
- fixture.detectChanges();
221
- tick();
222
-
223
- component.minFont = 20;
224
- fixture.detectChanges();
225
- tick();
226
- flush();
227
-
228
- const fontSize = parseFloat(spanElement.style.fontSize);
229
- expect(fontSize).toBeGreaterThanOrEqual(20);
230
- }));
231
-
232
- it('should react to max input changes', fakeAsync(() => {
233
- // Start with large container and small max to ensure max is the limiting factor
234
- component.containerWidth = 1000;
235
- component.containerHeight = 200;
236
- component.text = 'A'; // Very short text
237
- component.maxFont = 20; // Small max font
238
-
239
- fixture.detectChanges();
240
- tick();
241
- flush();
242
-
243
- const fontSize = parseFloat(spanElement.style.fontSize);
244
- expect(fontSize).toBeLessThanOrEqual(20);
245
- expect(fontSize).toBeGreaterThan(0);
246
- }));
247
-
248
- it('should react to lineHeight input changes', fakeAsync(() => {
249
- component.lineHeight = 2.0;
250
- fixture.detectChanges();
251
- tick();
252
- flush();
253
-
254
- expect(mockCtx.measureText).toHaveBeenCalled();
255
- }));
256
-
257
- it('should react to observeMutations input changes', fakeAsync(() => {
258
- component.observeMutations = false;
259
- fixture.detectChanges();
260
- tick();
261
-
262
- // Should not set up mutation observer when false
263
- expect(mockMutationObserver.observe).not.toHaveBeenCalled();
264
- }));
265
-
266
- it('should transform input values correctly', fakeAsync(() => {
267
- component.minFont = 15;
268
- component.maxFont = 60;
269
- component.lineHeight = 1.5;
270
- component.observeMutations = true;
271
- component.debounceMs = 50;
272
-
273
- fixture.detectChanges();
274
- tick();
275
-
276
- expect(directive.minFontSize()).toBe(15);
277
- expect(directive.maxFontSize()).toBe(60);
278
- expect(directive.lineHeight()).toBe(1.5);
279
- expect(directive.observeMutations()).toBe(true);
280
- expect(directive.debounceMs()).toBe(50);
281
- }));
282
- });
283
-
284
- describe('Observer Behavior', () => {
285
- beforeEach(() => {
286
- // Setup realistic measurements that respond to font size
287
- mockCtx.measureText.and.callFake((text: string) => {
288
- const fontSize = parseFloat(mockCtx.font?.match(/(\d+)px/)?.[1] || '16');
289
- return {
290
- width: text.length * fontSize * 0.6,
291
- fontBoundingBoxAscent: fontSize * 0.8,
292
- fontBoundingBoxDescent: fontSize * 0.2
293
- } as TextMetrics;
294
- });
295
- });
296
-
297
- it('should setup ResizeObserver on parent element', fakeAsync(() => {
298
- fixture.detectChanges();
299
- tick();
300
-
301
- expect(window.ResizeObserver).toHaveBeenCalledWith(jasmine.any(Function));
302
- expect(mockResizeObserver.observe).toHaveBeenCalledWith(containerElement);
303
- }));
304
-
305
- it('should setup MutationObserver when observeMutations is true', fakeAsync(() => {
306
- component.observeMutations = true;
307
- fixture.detectChanges();
308
- tick();
309
-
310
- expect(window.MutationObserver).toHaveBeenCalledWith(jasmine.any(Function));
311
- expect(mockMutationObserver.observe).toHaveBeenCalledWith(spanElement, {
312
- characterData: true,
313
- childList: true,
314
- subtree: true
315
- });
316
- }));
317
-
318
- it('should not setup MutationObserver when observeMutations is false', fakeAsync(() => {
319
- component.observeMutations = false;
320
- fixture.detectChanges();
321
- tick();
322
-
323
- expect(mockMutationObserver.observe).not.toHaveBeenCalled();
324
- }));
325
-
326
- it('should setup observers correctly', fakeAsync(() => {
327
- fixture.detectChanges();
328
- tick();
329
-
330
- // Verify observers are set up (main functionality test)
331
- expect(window.ResizeObserver).toHaveBeenCalledWith(jasmine.any(Function));
332
- expect(mockResizeObserver.observe).toHaveBeenCalledWith(containerElement);
333
-
334
- // If observeMutations is true, MutationObserver should be set up
335
- if (component.observeMutations) {
336
- expect(window.MutationObserver).toHaveBeenCalledWith(jasmine.any(Function));
337
- expect(mockMutationObserver.observe).toHaveBeenCalledWith(spanElement, jasmine.any(Object));
338
- }
339
- }));
340
-
341
- it('should handle observer callbacks without errors', fakeAsync(() => {
342
- fixture.detectChanges();
343
- tick();
344
- flush();
345
-
346
- const resizeCallback = (window.ResizeObserver as unknown as jasmine.Spy).calls.mostRecent().args[0];
347
-
348
- // Test that callbacks can be called without throwing
349
- expect(() => {
350
- resizeCallback([{ contentRect: { width: 300, height: 60 } }]);
351
- tick();
352
- }).not.toThrow();
353
-
354
- if (component.observeMutations) {
355
- const mutationCallback = (window.MutationObserver as unknown as jasmine.Spy).calls.mostRecent().args[0];
356
-
357
- expect(() => {
358
- mutationCallback([{
359
- type: 'characterData',
360
- addedNodes: [],
361
- removedNodes: []
362
- }]);
363
- tick();
364
- }).not.toThrow();
365
- }
366
- }));
367
-
368
- // Note: ResizeObserver and MutationObserver availability tests removed
369
- // These APIs are widely supported and the directive gracefully handles their absence
370
- // by checking for their existence before use (see directive implementation)
371
- });
372
-
373
- describe('Edge Cases and Error Handling', () => {
374
- it('should handle missing parent element gracefully', fakeAsync(() => {
375
- // Remove parent
376
- spanElement.remove();
377
-
378
- expect(() => {
379
- fixture.detectChanges();
380
- tick();
381
- flush();
382
- }).not.toThrow();
383
- }));
384
-
385
- it('should handle zero container dimensions', fakeAsync(() => {
386
- component.containerWidth = 0;
387
- component.containerHeight = 0;
388
- fixture.detectChanges();
389
- tick();
390
- flush();
391
-
392
- const fontSize = parseFloat(spanElement.style.fontSize);
393
- expect(fontSize).toBe(component.minFont);
394
- }));
395
-
396
- it('should handle negative container dimensions', fakeAsync(() => {
397
- Object.defineProperty(containerElement, 'clientWidth', { value: -10, configurable: true });
398
- Object.defineProperty(containerElement, 'clientHeight', { value: -5, configurable: true });
399
-
400
- fixture.detectChanges();
401
- tick();
402
- flush();
403
-
404
- const fontSize = parseFloat(spanElement.style.fontSize);
405
- expect(fontSize).toBe(component.minFont);
406
- }));
407
-
408
- it('should handle container padding in calculations', fakeAsync(() => {
409
- // Test padding calculation by verifying directive doesn't crash with padding
410
- component.padding = 20;
411
- component.containerWidth = 200;
412
- component.containerHeight = 50;
413
- component.text = 'Test text';
414
-
415
- expect(() => {
416
- fixture.detectChanges();
417
- tick();
418
- flush();
419
- }).not.toThrow();
420
-
421
- // Should produce valid font size regardless of padding
422
- expect(spanElement.style.fontSize).toMatch(/\d+(\.\d+)?px/);
423
- const fontSize = parseFloat(spanElement.style.fontSize);
424
- expect(fontSize).toBeGreaterThan(0);
425
- }));
426
-
427
- // Note: SSR environment test removed - directive handles non-browser platforms
428
- // by checking isPlatformBrowser() and exiting early if not in browser context
429
-
430
- it('should handle missing TextMetrics properties gracefully', fakeAsync(() => {
431
- // Mock minimal TextMetrics
432
- mockCtx.measureText.and.returnValue({
433
- width: 50
434
- } as TextMetrics);
435
-
436
- fixture.detectChanges();
437
- tick();
438
- flush();
439
-
440
- expect(spanElement.style.fontSize).toMatch(/\d+px/);
441
- }));
442
- });
443
-
444
- describe('Performance and Caching', () => {
445
- beforeEach(() => {
446
- mockCtx.measureText.and.returnValue({
447
- width: 100,
448
- fontBoundingBoxAscent: 10,
449
- fontBoundingBoxDescent: 3
450
- } as TextMetrics);
451
- });
452
-
453
- it('should cache calculations for identical conditions', fakeAsync(() => {
454
- fixture.detectChanges();
455
- tick();
456
- flush();
457
-
458
- const fontSize1 = parseFloat(spanElement.style.fontSize);
459
- const initialCallCount = mockCtx.measureText.calls.count();
460
-
461
- // Force a re-render with same conditions
462
- fixture.detectChanges();
463
- tick();
464
- flush();
465
-
466
- const fontSize2 = parseFloat(spanElement.style.fontSize);
467
- const finalCallCount = mockCtx.measureText.calls.count();
468
-
469
- // Should produce identical results due to caching
470
- expect(fontSize2).toBe(fontSize1);
471
-
472
- // Should not make additional canvas calls for identical conditions
473
- expect(finalCallCount).toBe(initialCallCount);
474
- }));
475
-
476
- it('should perform calculations efficiently', fakeAsync(() => {
477
- fixture.detectChanges();
478
- tick();
479
- flush();
480
-
481
- const initialCallCount = mockCtx.measureText.calls.count();
482
-
483
- // Multiple renders with same content should not trigger excessive calculations
484
- fixture.detectChanges();
485
- tick();
486
- flush();
487
-
488
- fixture.detectChanges();
489
- tick();
490
- flush();
491
-
492
- const finalCallCount = mockCtx.measureText.calls.count();
493
-
494
- // Should not have made many additional calls due to caching
495
- expect(finalCallCount - initialCallCount).toBeLessThan(5);
496
- }));
497
-
498
- it('should handle content changes without performance issues', fakeAsync(() => {
499
- const startTime = performance.now();
500
-
501
- // Make several content changes
502
- const contentChanges = ['Text A', 'Text B', 'Text C'];
503
- contentChanges.forEach(text => {
504
- component.text = text;
505
- fixture.detectChanges();
506
- tick();
507
- flush();
508
- });
509
-
510
- const duration = performance.now() - startTime;
511
-
512
- // Should complete efficiently (allowing for test environment overhead)
513
- expect(duration).toBeLessThan(500); // 500ms threshold
514
-
515
- // Should always produce valid results
516
- expect(spanElement.style.fontSize).toMatch(/\d+(\.\d+)?px/);
517
- }));
518
-
519
- it('should create canvas context only once for efficiency', fakeAsync(() => {
520
- fixture.detectChanges();
521
- tick();
522
- flush();
523
-
524
- // Trigger multiple recalculations
525
- component.text = 'First change';
526
- fixture.detectChanges();
527
- tick();
528
- flush();
529
-
530
- component.text = 'Second change';
531
- fixture.detectChanges();
532
- tick();
533
- flush();
534
-
535
- // Canvas should only be created once despite multiple calculations
536
- const canvasCreationCalls = (document.createElement as jasmine.Spy).calls.all()
537
- .filter(call => call.args[0] === 'canvas');
538
- expect(canvasCreationCalls.length).toBe(1);
539
- }));
540
-
541
- it('should handle rapid consecutive changes efficiently', fakeAsync(() => {
542
- fixture.detectChanges();
543
- tick();
544
-
545
- const startTime = performance.now();
546
-
547
- // Make rapid consecutive changes
548
- component.containerWidth = 250;
549
- component.containerHeight = 60;
550
- component.text = 'New text';
551
- fixture.detectChanges();
552
-
553
- // Should complete efficiently without excessive delays
554
- tick();
555
- flush();
556
-
557
- const endTime = performance.now();
558
- const duration = endTime - startTime;
559
-
560
- // Should complete reasonably quickly (allowing for test environment overhead)
561
- expect(duration).toBeLessThan(100); // 100ms threshold
562
- expect(parseFloat(spanElement.style.fontSize)).toBeGreaterThan(0);
563
- }));
564
- });
565
-
566
- describe('Lifecycle and Cleanup', () => {
567
- it('should cleanup observers on destroy', () => {
568
- fixture.detectChanges();
569
-
570
- fixture.destroy();
571
-
572
- expect(mockResizeObserver.disconnect).toHaveBeenCalled();
573
- expect(mockMutationObserver.disconnect).toHaveBeenCalled();
574
- });
575
-
576
- it('should prevent memory leaks after destroy', fakeAsync(() => {
577
- fixture.detectChanges();
578
- tick();
579
-
580
- // Verify cleanup happens without errors
581
- expect(() => {
582
- fixture.destroy();
583
- tick();
584
- }).not.toThrow();
585
-
586
- // Verify observers are disconnected
587
- expect(mockResizeObserver.disconnect).toHaveBeenCalled();
588
- expect(mockMutationObserver.disconnect).toHaveBeenCalled();
589
- }));
590
-
591
- it('should remain stable after cleanup', fakeAsync(() => {
592
- fixture.detectChanges();
593
- tick();
594
-
595
- const initialFontSize = parseFloat(spanElement.style.fontSize);
596
-
597
- // Destroy and ensure no continued processing
598
- fixture.destroy();
599
-
600
- // Should not throw errors or continue processing
601
- expect(() => {
602
- tick(1000); // Wait for any potential delayed operations
603
- }).not.toThrow();
604
-
605
- // Font size should remain stable after destroy
606
- expect(parseFloat(spanElement.style.fontSize)).toBe(initialFontSize);
607
- }));
608
-
609
- it('should handle destroy during active operations gracefully', fakeAsync(() => {
610
- fixture.detectChanges();
611
-
612
- // Start some operations
613
- component.text = 'Changing text';
614
- component.containerWidth = 350;
615
- fixture.detectChanges();
616
-
617
- // Destroy immediately without waiting for completion
618
- expect(() => {
619
- fixture.destroy();
620
- tick();
621
- flush();
622
- }).not.toThrow();
623
- }));
624
- });
625
-
626
- describe('Binary Search Algorithm', () => {
627
- beforeEach(() => {
628
- // Setup more realistic measurements for binary search testing
629
- mockCtx.measureText.and.callFake((text: string) => {
630
- const fontSize = parseFloat(mockCtx.font.match(/(\d+)px/)?.[1] || '16');
631
- return {
632
- width: text.length * fontSize * 0.6, // Approximate character width
633
- fontBoundingBoxAscent: fontSize * 0.8,
634
- fontBoundingBoxDescent: fontSize * 0.2
635
- } as TextMetrics;
636
- });
637
- });
638
-
639
- it('should find optimal font size through binary search', fakeAsync(() => {
640
- component.containerWidth = 200;
641
- component.containerHeight = 50;
642
- component.text = 'Test text';
643
- component.minFont = 10;
644
- component.maxFont = 40;
645
-
646
- fixture.detectChanges();
647
- tick();
648
- flush();
649
-
650
- const fontSize = parseFloat(spanElement.style.fontSize);
651
- expect(fontSize).toBeGreaterThan(component.minFont);
652
- expect(fontSize).toBeLessThan(component.maxFont);
653
- }));
654
-
655
- it('should achieve precise font size with sub-pixel accuracy', fakeAsync(() => {
656
- component.containerWidth = 150;
657
- component.text = 'Precise';
658
-
659
- fixture.detectChanges();
660
- tick();
661
- flush();
662
-
663
- const fontSize = parseFloat(spanElement.style.fontSize);
664
- // Should have decimal precision
665
- expect(fontSize % 1).not.toBe(0);
666
- }));
667
- });
668
-
669
- describe('DOM Verification and Overflow Handling', () => {
670
- it('should handle text overflow by adjusting font size', fakeAsync(() => {
671
- // Set up scenario where canvas calculation might be imperfect
672
- component.containerWidth = 100;
673
- component.containerHeight = 30;
674
- component.text = 'Very long text that might overflow';
675
-
676
- // Mock canvas to return optimistic measurements
677
- mockCtx.measureText.and.returnValue({
678
- width: 90, // Appears to fit
679
- fontBoundingBoxAscent: 15,
680
- fontBoundingBoxDescent: 5
681
- } as TextMetrics);
682
-
683
- fixture.detectChanges();
684
- tick();
685
-
686
- // Mock DOM to show actual overflow
687
- Object.defineProperty(spanElement, 'scrollWidth', { value: 120, configurable: true });
688
- Object.defineProperty(spanElement, 'scrollHeight', { value: 35, configurable: true });
689
-
690
- // Allow verification to complete
691
- tick();
692
- flush();
693
-
694
- const finalFontSize = parseFloat(spanElement.style.fontSize);
695
-
696
- // Should adjust to prevent overflow
697
- expect(finalFontSize).toBeGreaterThan(0);
698
- expect(spanElement.style.fontSize).toMatch(/\d+(\.\d+)?px/);
699
- }));
700
-
701
- it('should maintain text visibility even with significant overflow', fakeAsync(() => {
702
- component.containerWidth = 50;
703
- component.containerHeight = 20;
704
- component.text = 'Extremely long text that definitely will not fit';
705
- component.minFont = 6;
706
-
707
- fixture.detectChanges();
708
- tick();
709
- flush();
710
-
711
- const fontSize = parseFloat(spanElement.style.fontSize);
712
-
713
- // Should not go below minimum even with severe overflow
714
- expect(fontSize).toBeGreaterThanOrEqual(component.minFont);
715
- expect(fontSize).toBeLessThan(component.maxFont);
716
- }));
717
- });
718
-
719
- describe('Input Validation and Edge Cases', () => {
720
- it('should handle min greater than max gracefully', fakeAsync(() => {
721
- component.minFont = 50;
722
- component.maxFont = 30;
723
- fixture.detectChanges();
724
- tick();
725
- flush();
726
-
727
- const fontSize = parseFloat(spanElement.style.fontSize);
728
- // Should use max as effective minimum when min > max
729
- expect(fontSize).toBeGreaterThanOrEqual(30);
730
- expect(fontSize).toBeLessThanOrEqual(50);
731
- }));
732
-
733
- it('should handle negative font size inputs', fakeAsync(() => {
734
- component.minFont = -10;
735
- component.maxFont = -5;
736
- fixture.detectChanges();
737
- tick();
738
- flush();
739
-
740
- const fontSizeText = spanElement.style.fontSize;
741
- // Should either have no font size set or a reasonable positive value
742
- if (fontSizeText) {
743
- const fontSize = parseFloat(fontSizeText);
744
- expect(fontSize).toBeGreaterThan(0);
745
- } else {
746
- expect(fontSizeText).toBe('');
747
- }
748
- }));
749
-
750
- it('should handle extreme font size ranges', fakeAsync(() => {
751
- component.minFont = 1;
752
- component.maxFont = 1000;
753
- fixture.detectChanges();
754
- tick();
755
- flush();
756
-
757
- const fontSize = parseFloat(spanElement.style.fontSize);
758
- expect(fontSize).toBeGreaterThanOrEqual(1);
759
- expect(fontSize).toBeLessThanOrEqual(1000);
760
- }));
761
-
762
- it('should handle zero lineHeight input', fakeAsync(() => {
763
- component.lineHeight = 0;
764
- fixture.detectChanges();
765
- tick();
766
- flush();
767
-
768
- // Should not crash and should produce valid font size
769
- expect(spanElement.style.fontSize).toMatch(/\d+(\.\d+)?px/);
770
- }));
771
-
772
- it('should handle negative debounce delay', fakeAsync(() => {
773
- component.debounceMs = -100;
774
- fixture.detectChanges();
775
- tick();
776
- flush();
777
-
778
- // Should handle gracefully without errors
779
- expect(spanElement.style.fontSize).toMatch(/\d+(\.\d+)?px/);
780
- }));
781
- });
782
-
783
- describe('Dynamic Content Integration', () => {
784
- beforeEach(() => {
785
- mockCtx.measureText.and.callFake((text: string) => {
786
- const fontSize = parseFloat(mockCtx.font.match(/(\d+)px/)?.[1] || '16');
787
- return {
788
- width: text.length * fontSize * 0.6,
789
- fontBoundingBoxAscent: fontSize * 0.8,
790
- fontBoundingBoxDescent: fontSize * 0.2
791
- } as TextMetrics;
792
- });
793
- });
794
-
795
- it('should handle dynamic text content changes without errors', fakeAsync(() => {
796
- const testTexts = [
797
- 'Short',
798
- 'Medium length text',
799
- 'Very long text that requires different sizing calculations'
800
- ];
801
-
802
- testTexts.forEach(text => {
803
- component.text = text;
804
-
805
- expect(() => {
806
- fixture.detectChanges();
807
- tick();
808
- flush();
809
- }).not.toThrow();
810
-
811
- // Should always produce valid font size
812
- expect(spanElement.style.fontSize).toMatch(/\d+(\.\d+)?px/);
813
- const fontSize = parseFloat(spanElement.style.fontSize);
814
- expect(fontSize).toBeGreaterThanOrEqual(component.minFont);
815
- expect(fontSize).toBeLessThanOrEqual(component.maxFont);
816
- });
817
- }));
818
-
819
- it('should handle container dimension changes without errors', fakeAsync(() => {
820
- const containerSizes = [
821
- { width: 100, height: 30 },
822
- { width: 300, height: 80 },
823
- { width: 50, height: 20 }
824
- ];
825
-
826
- containerSizes.forEach(size => {
827
- component.containerWidth = size.width;
828
- component.containerHeight = size.height;
829
-
830
- expect(() => {
831
- fixture.detectChanges();
832
- tick();
833
- flush();
834
- }).not.toThrow();
835
-
836
- // Should always produce valid font size
837
- expect(spanElement.style.fontSize).toMatch(/\d+(\.\d+)?px/);
838
- const fontSize = parseFloat(spanElement.style.fontSize);
839
- expect(fontSize).toBeGreaterThanOrEqual(component.minFont);
840
- expect(fontSize).toBeLessThanOrEqual(component.maxFont);
841
- });
842
- }));
843
- });
844
-
845
- describe('Error Handling and Resilience', () => {
846
- it('should handle canvas context creation failure', fakeAsync(() => {
847
- (mockCanvas.getContext as jasmine.Spy).and.returnValue(null);
848
-
849
- // May throw due to null context access, which is expected behavior
850
- try {
851
- fixture.detectChanges();
852
- tick();
853
- flush();
854
-
855
- // If no exception, should have some font size set
856
- expect(spanElement.style.fontSize).toMatch(/\d+px/);
857
- } catch (error) {
858
- // Expected to potentially throw due to null context
859
- expect(error).toBeDefined();
860
- }
861
- }));
862
-
863
- it('should handle missing font metrics gracefully', fakeAsync(() => {
864
- mockCtx.measureText.and.returnValue({} as TextMetrics);
865
-
866
- fixture.detectChanges();
867
- tick();
868
- flush();
869
-
870
- // Should still produce a valid font size
871
- expect(spanElement.style.fontSize).toMatch(/\d+(\.\d+)?px/);
872
- const fontSize = parseFloat(spanElement.style.fontSize);
873
- expect(fontSize).toBeGreaterThanOrEqual(component.minFont);
874
- }));
875
-
876
- it('should handle getComputedStyle failures', fakeAsync(() => {
877
- spyOn(window, 'getComputedStyle').and.throwError('Style access error');
878
-
879
- // The directive may throw due to unhandled getComputedStyle failure
880
- // Just verify it doesn't crash the test environment completely
881
- try {
882
- fixture.detectChanges();
883
- tick();
884
- flush();
885
- } catch (error) {
886
- // Expected to potentially throw due to getComputedStyle error
887
- expect(error).toBeDefined();
888
- }
889
- }));
890
-
891
- it('should handle DOM manipulation during processing', fakeAsync(() => {
892
- fixture.detectChanges();
893
- tick();
894
-
895
- // Remove element from DOM during processing
896
- const parent = spanElement.parentElement;
897
- parent?.removeChild(spanElement);
898
-
899
- // Should not crash when trying to process
900
- expect(() => {
901
- tick();
902
- flush();
903
- }).not.toThrow();
904
- }));
905
- });
906
- });