@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.
- package/fesm2022/dragonworks-ngx-dashboard-widgets.mjs +2251 -0
- package/fesm2022/dragonworks-ngx-dashboard-widgets.mjs.map +1 -0
- package/index.d.ts +532 -0
- package/package.json +42 -31
- package/ng-package.json +0 -7
- package/src/lib/arrow-widget/arrow-state-dialog.component.ts +0 -187
- package/src/lib/arrow-widget/arrow-widget.component.html +0 -9
- package/src/lib/arrow-widget/arrow-widget.component.scss +0 -52
- package/src/lib/arrow-widget/arrow-widget.component.ts +0 -78
- package/src/lib/arrow-widget/arrow-widget.metadata.ts +0 -3
- package/src/lib/clock-widget/analog-clock/analog-clock.component.html +0 -66
- package/src/lib/clock-widget/analog-clock/analog-clock.component.scss +0 -103
- package/src/lib/clock-widget/analog-clock/analog-clock.component.ts +0 -120
- package/src/lib/clock-widget/clock-state-dialog.component.ts +0 -170
- package/src/lib/clock-widget/clock-widget.component.html +0 -16
- package/src/lib/clock-widget/clock-widget.component.scss +0 -160
- package/src/lib/clock-widget/clock-widget.component.ts +0 -87
- package/src/lib/clock-widget/clock-widget.metadata.ts +0 -42
- package/src/lib/clock-widget/digital-clock/__tests__/digital-clock.component.spec.ts +0 -276
- package/src/lib/clock-widget/digital-clock/digital-clock.component.html +0 -1
- package/src/lib/clock-widget/digital-clock/digital-clock.component.scss +0 -43
- package/src/lib/clock-widget/digital-clock/digital-clock.component.ts +0 -105
- package/src/lib/directives/__tests__/responsive-text.directive.spec.ts +0 -906
- package/src/lib/directives/responsive-text.directive.ts +0 -334
- package/src/lib/label-widget/__tests__/label-widget.component.spec.ts +0 -539
- package/src/lib/label-widget/label-state-dialog.component.ts +0 -385
- package/src/lib/label-widget/label-widget.component.html +0 -21
- package/src/lib/label-widget/label-widget.component.scss +0 -112
- package/src/lib/label-widget/label-widget.component.ts +0 -96
- package/src/lib/label-widget/label-widget.metadata.ts +0 -3
- package/src/public-api.ts +0 -7
- package/tsconfig.lib.json +0 -15
- package/tsconfig.lib.prod.json +0 -11
- 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
|
-
});
|