@emartech/aggregate-metric-logger 6.1.1 → 6.5.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.
@@ -3,7 +3,8 @@ name: Aggregate Metric Logger Build
3
3
  on: push
4
4
 
5
5
  env:
6
- NPM_TOKEN: ${{ secrets.NPM_PUBLISHER_TOKEN }}
6
+ NPM_TOKEN: ${{ secrets.NPM_DEPLOYER_TOKEN }}
7
+ NPM_PUBLISHER_TOKEN: ${{ secrets.NPM_PUBLISHER_TOKEN }}
7
8
  GH_TOKEN: ${{ secrets.GH_TOKEN }}
8
9
  SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
9
10
 
@@ -13,61 +14,74 @@ jobs:
13
14
  runs-on: ubuntu-latest
14
15
  steps:
15
16
  - name: Check out repository
16
- uses: actions/checkout@v6
17
+ uses: actions/checkout@v7
17
18
  - name: Use correct Node version
18
- uses: actions/setup-node@v6
19
+ uses: actions/setup-node@v7
19
20
  with:
20
21
  node-version-file: '.nvmrc'
21
22
  - name: Cache node_modules
22
23
  id: cache_node_modules
23
- uses: actions/cache@v5
24
+ uses: actions/cache@v6
24
25
  with:
25
26
  path: node_modules
26
- key: node_modules-${{ hashFiles('**/package-lock.json') }}
27
+ key: node_modules-${{ hashFiles('**/package.json') }}
27
28
  - name: Install dependencies
28
29
  if: ${{ steps.cache_node_modules.outputs.cache-hit != 'true' }}
29
30
  continue-on-error: true
30
31
  run: |
32
+ npm install -g npm@latest
33
+ npm -v
31
34
  npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
32
35
  npm i
36
+
33
37
  - name: Run tests
34
38
  run: npm test
35
39
  - name: Slack notification
36
- uses: 8398a7/action-slack@v3
37
- if: ${{ failure() }}
40
+ uses: slackapi/slack-github-action@v4
41
+ if: failure()
38
42
  with:
39
- status: ${{ job.status }}
40
- fields: repo,message,author,job,ref
41
- job_name: Test 🚦
42
- env:
43
- SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
43
+ webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
44
+ webhook-type: incoming-webhook
45
+ payload: |
46
+ text: "*GitHub Action build result*: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}"
44
47
 
45
48
  deploy:
46
49
  name: Deploy 🚢
47
50
  needs: [test]
48
51
  runs-on: ubuntu-latest
52
+ permissions:
53
+ contents: write
49
54
  steps:
50
55
  - name: Check out repository
51
- uses: actions/checkout@v6
56
+ uses: actions/checkout@v7
52
57
  - name: Use correct Node version
53
- uses: actions/setup-node@v6
58
+ uses: actions/setup-node@v7
54
59
  with:
55
60
  node-version-file: '.nvmrc'
56
61
  - name: Cache node_modules
57
62
  id: cache_node_modules
58
- uses: actions/cache@v5
63
+ uses: actions/cache@v6
59
64
  with:
60
65
  path: node_modules
61
- key: node_modules-${{ hashFiles('**/package-lock.json') }}
66
+ key: node_modules-${{ hashFiles('**/package.json') }}
67
+ - name: Install dependencies
68
+ if: ${{ steps.cache_node_modules.outputs.cache-hit != 'true' }}
69
+ continue-on-error: true
70
+ run: |
71
+ npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
72
+ npm i
62
73
  - name: Release package
63
74
  if: github.ref_name == 'master'
64
- run: CI=true npm run semantic-release
65
- - name: Slack notification
66
- uses: 8398a7/action-slack@v3
67
- with:
68
- status: ${{ job.status }}
69
- fields: repo,message,author,job,ref
70
- job_name: Deploy 🚢
71
75
  env:
72
- SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
76
+ GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GH_TOKEN_CS }}
77
+ NPM_TOKEN: ${{ secrets.NPM_PUBLISHER_TOKEN }}
78
+ run:
79
+ CI=true npm run semantic-release
80
+ - name: Slack notification
81
+ uses: slackapi/slack-github-action@v4
73
82
  if: always()
83
+ with:
84
+ webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
85
+ webhook-type: incoming-webhook
86
+ payload: |
87
+ text: "*GitHub Action build result*: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}"
package/.nvmrc CHANGED
@@ -1 +1 @@
1
- 24.16.0
1
+ 24.19.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emartech/aggregate-metric-logger",
3
- "version": "6.1.1",
3
+ "version": "6.5.0",
4
4
  "description": "aggregate metrics in memory for logging",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -30,19 +30,19 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "@emartech/json-logger": "^9.0.0",
34
- "@google-cloud/monitoring": "^5.0.0",
35
- "json-stable-stringify": "^1.1.1",
36
- "lodash": "^4.17.15",
37
- "ms": "^2.1.3"
33
+ "@emartech/json-logger": "9.2.5",
34
+ "@google-cloud/monitoring": "6.0.0",
35
+ "json-stable-stringify": "1.3.0",
36
+ "lodash": "4.18.1",
37
+ "ms": "2.1.3"
38
38
  },
39
39
  "devDependencies": {
40
- "@emartech/rti-server-tools": "^10.0.0",
41
- "@types/jest": "^30.0.0",
42
- "dotenv-haphap": "^3.1.2",
43
- "eslint": "^10.0.0",
44
- "globals": "17.6.0",
45
- "jest": "^30.0.0",
46
- "semantic-release": "^25.0.0"
40
+ "@emartech/rti-server-tools": "11.5.0",
41
+ "@types/jest": "30.0.0",
42
+ "dotenv-haphap": "3.1.2",
43
+ "eslint": "10.9.0",
44
+ "globals": "17.11.0",
45
+ "jest": "30.4.2",
46
+ "semantic-release": "25.0.9"
47
47
  }
48
48
  }
@@ -5,8 +5,15 @@ const { createGcpTimeSeriesPublisher } = require('../gcp-time-series-publisher/g
5
5
  const { createGcpTimeSeriesBuilder } = require('../gcp-time-series-builder/gcp-time-series-builder');
6
6
  const { createCounter } = require('../metric-types/counter/counter');
7
7
  const { createDistribution } = require('../metric-types/distribution/distribution');
8
+ const { createGauge } = require('../metric-types/gauge/gauge');
8
9
  const { isNumber, isPlainObject } = require('lodash');
9
10
 
11
+ const aggregatorFactories = {
12
+ counter: createCounter,
13
+ distribution: createDistribution,
14
+ gauge: createGauge
15
+ };
16
+
10
17
  class GcpMetricCollector {
11
18
  metrics = {};
12
19
  initialized = false;
@@ -33,6 +40,18 @@ class GcpMetricCollector {
33
40
  return this.addToMetric(metricName, labels, 'distribution', value);
34
41
  }
35
42
 
43
+ gauge(metricName, labels = {}, value) {
44
+ if (!isNumber(value)) {
45
+ throw new Error(`${ value } is not a number`);
46
+ }
47
+
48
+ if (!isPlainObject(labels)) {
49
+ throw new Error('labels is not a plain object');
50
+ }
51
+
52
+ this.addToMetric(metricName, labels, 'gauge', value);
53
+ }
54
+
36
55
  start(metricName, labels = {}) {
37
56
  return { metricName, labels, start: Date.now() };
38
57
  }
@@ -55,7 +74,11 @@ class GcpMetricCollector {
55
74
  }
56
75
  const key = `${metricName}_${stringify(labels)}`;
57
76
  if (!this.metrics[key]) {
58
- const aggregator = type === 'counter' ? createCounter() : createDistribution();
77
+ const createAggregator = aggregatorFactories[type];
78
+ if (!createAggregator) {
79
+ throw new Error(`unknown metric type: ${type}`);
80
+ }
81
+ const aggregator = createAggregator();
59
82
  this.metrics[key] = { name: metricName, labels, aggregator };
60
83
  }
61
84
  this.metrics[key].aggregator.add(value);
@@ -72,12 +95,22 @@ class GcpMetricCollector {
72
95
  }
73
96
 
74
97
  async flush() {
98
+ await this.publishPending();
99
+ this.setupNextFlush();
100
+ }
101
+
102
+ async shutdown() {
103
+ clearTimeout(this.timeoutId);
104
+ this.initialized = false;
105
+ await this.publishPending();
106
+ }
107
+
108
+ async publishPending() {
75
109
  const timeSeries = createGcpTimeSeriesBuilder(this.projectId).buildTimeSeries(this.metrics);
76
110
  for (const [, metric] of Object.entries(this.metrics)) {
77
111
  metric.aggregator.advanceTimer();
78
112
  }
79
113
  await createGcpTimeSeriesPublisher(this.projectId).publishTimeSeries(timeSeries);
80
- this.setupNextFlush();
81
114
  }
82
115
  }
83
116
 
@@ -89,10 +122,15 @@ exports.count = (metricName, labels = {}, value = 1) =>
89
122
  exports.measure = (metricName, labels = {}, value = 1) =>
90
123
  collector.measure(metricName, labels, value);
91
124
 
125
+ exports.gauge = (metricName, labels = {}, value) =>
126
+ collector.gauge(metricName, labels, value);
127
+
92
128
  exports.start = (metricName, labels = {}) => collector.start(metricName, labels);
93
129
 
94
130
  exports.stop = measurement => collector.stop(measurement);
95
131
 
96
132
  exports.reset = () => collector.reset();
97
133
 
134
+ exports.shutdown = () => collector.shutdown();
135
+
98
136
  exports.createGcpMetricsCollector = projectId => new GcpMetricCollector(projectId);
@@ -343,6 +343,160 @@ describe('gcpMetricCollector', () => {
343
343
  });
344
344
  });
345
345
 
346
+ describe('gauge', () => {
347
+ it('should send a single gauge value in next flush period', () => {
348
+ const now = new Date('2026-08-25T12:03:17+00:00');
349
+ const oneMinuteInMillis = 60 * 1000;
350
+ jest.useFakeTimers().setSystemTime(now);
351
+ stubCreateTimeSeries();
352
+
353
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 42);
354
+
355
+ expect(getPublishedMetrics()).toEqual([]);
356
+
357
+ jest.advanceTimersByTime(oneMinuteInMillis);
358
+
359
+ expect(getPublishedMetrics()).toEqual([
360
+ [
361
+ {
362
+ resource: {
363
+ labels: {
364
+ project_id: 'dummy-project'
365
+ },
366
+ type: 'global'
367
+ },
368
+ name: 'job_delay',
369
+ labels: { programId: 'A' },
370
+ value: { doubleValue: 42 },
371
+ startTime: now.getTime() + oneMinuteInMillis,
372
+ endTime: now.getTime() + oneMinuteInMillis,
373
+ metricKind: 'GAUGE',
374
+ valueType: 'doubleValue'
375
+ }
376
+ ]
377
+ ]);
378
+ });
379
+
380
+ it('should keep only the last value within a flush period', async () => {
381
+ stubCreateTimeSeries();
382
+
383
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 5);
384
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 9);
385
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 3);
386
+
387
+ jest.advanceTimersByTime(60 * 1000 + 1);
388
+ await flushPromises();
389
+
390
+ expect(getPublishedMetrics()).toMatchObject([
391
+ [{ name: 'job_delay', labels: { programId: 'A' }, value: { doubleValue: 3 } }]
392
+ ]);
393
+ });
394
+
395
+ it('should send a separate metric for each label combo', async () => {
396
+ stubCreateTimeSeries();
397
+
398
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 1);
399
+ gcpMetricCollector.gauge('job_delay', { programId: 'B' }, 2);
400
+
401
+ jest.advanceTimersByTime(60 * 1000 + 1);
402
+ await flushPromises();
403
+
404
+ expect(getPublishedMetrics()).toMatchObject([
405
+ [
406
+ { name: 'job_delay', labels: { programId: 'A' }, value: { doubleValue: 1 } },
407
+ { name: 'job_delay', labels: { programId: 'B' }, value: { doubleValue: 2 } }
408
+ ]
409
+ ]);
410
+ });
411
+
412
+ it('should send metric for given gcp project id', () => {
413
+ stubCreateTimeSeries();
414
+
415
+ gcpMetricCollector
416
+ .createGcpMetricsCollector('test-project')
417
+ .gauge('job_delay', { programId: 'A' }, 42);
418
+
419
+ jest.advanceTimersByTime(60 * 1000 + 1);
420
+
421
+ expect(getPublishedMetrics()).toEqual([
422
+ [
423
+ expect.objectContaining({
424
+ resource: {
425
+ labels: {
426
+ project_id: 'test-project'
427
+ },
428
+ type: 'global'
429
+ }
430
+ })
431
+ ]
432
+ ]);
433
+ });
434
+
435
+ it('should go silent and not republish a stale value when no new value is set', async () => {
436
+ stubCreateTimeSeries();
437
+
438
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 42);
439
+
440
+ jest.advanceTimersByTime(60 * 1000 + 1);
441
+ await flushPromises();
442
+
443
+ jest.advanceTimersByTime(60 * 1000 + 1);
444
+ await flushPromises();
445
+
446
+ expect(getPublishedMetrics()).toMatchObject([
447
+ [{ name: 'job_delay', labels: { programId: 'A' }, value: { doubleValue: 42 } }]
448
+ ]);
449
+ });
450
+
451
+ it('should publish a fresh value after a silent period', async () => {
452
+ stubCreateTimeSeries();
453
+
454
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 42);
455
+
456
+ jest.advanceTimersByTime(60 * 1000 + 1);
457
+ await flushPromises();
458
+
459
+ jest.advanceTimersByTime(60 * 1000 + 1);
460
+ await flushPromises();
461
+
462
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 9);
463
+
464
+ jest.advanceTimersByTime(60 * 1000 + 1);
465
+ await flushPromises();
466
+
467
+ expect(getPublishedMetrics()).toMatchObject([
468
+ [{ value: { doubleValue: 42 } }],
469
+ [{ value: { doubleValue: 9 } }]
470
+ ]);
471
+ });
472
+
473
+ it('should not publish metrics if metric logger is not enabled', async () => {
474
+ process.env.METRIC_LOGGER_ENABLED = 'false';
475
+ stubCreateTimeSeries();
476
+
477
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 42);
478
+ jest.advanceTimersByTime(60 * 1000 + 1);
479
+
480
+ expect(getPublishedMetrics()).toEqual([]);
481
+ });
482
+
483
+ it('should validate labels parameter whether it is an object', async () => {
484
+ process.env.METRIC_LOGGER_ENABLED = 'false';
485
+
486
+ expect(() => {
487
+ gcpMetricCollector.gauge('irrelevant', new Error('not a plain object'), 42);
488
+ }).toThrow(new Error('labels is not a plain object'));
489
+ });
490
+
491
+ it('should validate value parameter whether it is a number', async () => {
492
+ process.env.METRIC_LOGGER_ENABLED = 'false';
493
+
494
+ expect(() => {
495
+ gcpMetricCollector.gauge('irrelevant', {}, '42');
496
+ }).toThrow(new Error('42 is not a number'));
497
+ });
498
+ });
499
+
346
500
  describe('start', () => {
347
501
  it('should return a measurement object with the given tag, params and start time', () => {
348
502
  const fakeCurrentTimestamp = 2323434543;
@@ -383,4 +537,50 @@ describe('gcpMetricCollector', () => {
383
537
  ]);
384
538
  });
385
539
  });
540
+
541
+ describe('shutdown', () => {
542
+ it('should flush pending metrics immediately', async () => {
543
+ stubCreateTimeSeries();
544
+
545
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 42);
546
+
547
+ expect(getPublishedMetrics()).toEqual([]);
548
+
549
+ await gcpMetricCollector.shutdown();
550
+
551
+ expect(getPublishedMetrics()).toMatchObject([
552
+ [{ name: 'job_delay', labels: { programId: 'A' }, value: { doubleValue: 42 } }]
553
+ ]);
554
+ });
555
+
556
+ it('should not schedule another flush after shutdown', async () => {
557
+ stubCreateTimeSeries();
558
+ const collector = gcpMetricCollector.createGcpMetricsCollector('test-project');
559
+ const publishPendingSpy = jest.spyOn(collector, 'publishPending');
560
+
561
+ collector.gauge('job_delay', { programId: 'A' }, 42);
562
+ await collector.shutdown();
563
+
564
+ jest.advanceTimersByTime(60 * 1000 + 1);
565
+ await flushPromises();
566
+
567
+ expect(publishPendingSpy).toHaveBeenCalledTimes(1);
568
+ });
569
+
570
+ it('should resume flushing on the next metric after shutdown', async () => {
571
+ stubCreateTimeSeries();
572
+
573
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 42);
574
+ await gcpMetricCollector.shutdown();
575
+
576
+ gcpMetricCollector.gauge('job_delay', { programId: 'A' }, 9);
577
+ jest.advanceTimersByTime(60 * 1000 + 1);
578
+ await flushPromises();
579
+
580
+ expect(getPublishedMetrics()).toMatchObject([
581
+ [{ value: { doubleValue: 42 } }],
582
+ [{ value: { doubleValue: 9 } }]
583
+ ]);
584
+ });
585
+ });
386
586
  });
@@ -7,30 +7,35 @@ class GcpTimeSeriesBuilder {
7
7
  this.projectId = projectId ?? process.env.MONITORING_PROJECT_ID;
8
8
  }
9
9
  buildTimeSeries(metrics) {
10
- return Object.values(metrics).map(metric => {
11
- const state = metric.aggregator.getState();
12
- return {
13
- metric: {
14
- type: `custom.googleapis.com/${metric.name}`,
15
- labels: { ...metric.labels, hostname: process.env.HOSTNAME }
16
- },
17
- resource: {
18
- type: 'global',
19
- labels: { project_id: this.projectId }
20
- },
21
- points: [
22
- {
23
- interval: {
24
- startTime: { seconds: state.startTime / 1000 },
25
- endTime: { seconds: state.endTime / 1000 }
26
- },
27
- value: state.value
28
- }
29
- ],
30
- metricKind: state.metricKind,
31
- valueType: state.valueType
32
- };
33
- });
10
+ return Object.values(metrics)
11
+ .filter(
12
+ metric =>
13
+ typeof metric.aggregator.hasValue !== 'function' || metric.aggregator.hasValue()
14
+ )
15
+ .map(metric => {
16
+ const state = metric.aggregator.getState();
17
+ return {
18
+ metric: {
19
+ type: `custom.googleapis.com/${metric.name}`,
20
+ labels: { ...metric.labels, hostname: process.env.HOSTNAME }
21
+ },
22
+ resource: {
23
+ type: 'global',
24
+ labels: { project_id: this.projectId }
25
+ },
26
+ points: [
27
+ {
28
+ interval: {
29
+ startTime: { seconds: state.startTime / 1000 },
30
+ endTime: { seconds: state.endTime / 1000 }
31
+ },
32
+ value: state.value
33
+ }
34
+ ],
35
+ metricKind: state.metricKind,
36
+ valueType: state.valueType
37
+ };
38
+ });
34
39
  }
35
40
  }
36
41
 
@@ -52,6 +52,7 @@ class GcpTimeSeriesPublisher {
52
52
  };
53
53
  }
54
54
 
55
+
55
56
  exports.createGcpTimeSeriesPublisher = projectId => new GcpTimeSeriesPublisher(projectId);
56
57
 
57
58
  exports.publishTimeSeries = async timeSeries => {
package/src/index.d.ts CHANGED
@@ -4,6 +4,12 @@ export function count(
4
4
  value?: number
5
5
  ): void;
6
6
 
7
+ export function gauge(
8
+ metricName: string,
9
+ labels: MetricLabels,
10
+ value: number
11
+ ): void;
12
+
7
13
  export function measure(
8
14
  metricName: string,
9
15
  labels?: MetricLabels,
@@ -19,6 +25,8 @@ export function stop(
19
25
  measurement: MetricTimerObject
20
26
  ): void;
21
27
 
28
+ export function shutdown(): Promise<void>;
29
+
22
30
  export type MetricLabels = {
23
31
  [key: string]: string | number | null | undefined
24
32
  };
package/src/index.js CHANGED
@@ -4,8 +4,10 @@ const gcpMetricCollector = require('./gcp-metric-collector/gcp-metric-collector'
4
4
 
5
5
  module.exports = {
6
6
  count: gcpMetricCollector.count,
7
+ gauge: gcpMetricCollector.gauge,
7
8
  measure: gcpMetricCollector.measure,
8
9
  start: gcpMetricCollector.start,
9
10
  stop: gcpMetricCollector.stop,
11
+ shutdown: gcpMetricCollector.shutdown,
10
12
  createGcpMetricsCollector: gcpMetricCollector.createGcpMetricsCollector
11
13
  };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ exports.createGauge = () => {
4
+ let value = null;
5
+
6
+ return {
7
+ getState: () => ({
8
+ startTime: Date.now(),
9
+ endTime: Date.now(),
10
+ metricKind: 'GAUGE',
11
+ valueType: 'doubleValue',
12
+ value: { doubleValue: value }
13
+ }),
14
+ add: newValue => (value = newValue),
15
+ advanceTimer: () => {
16
+ value = null;
17
+ },
18
+ hasValue: () => value !== null
19
+ };
20
+ };
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const { createGauge } = require('./gauge');
4
+
5
+ describe('gauge', () => {
6
+ it('should return an empty gauge if no value was set', () => {
7
+ const now = new Date();
8
+ jest.useFakeTimers().setSystemTime(now);
9
+ const gauge = createGauge();
10
+
11
+ expect(gauge.getState()).toEqual({
12
+ startTime: now.getTime(),
13
+ endTime: now.getTime(),
14
+ metricKind: 'GAUGE',
15
+ valueType: 'doubleValue',
16
+ value: { doubleValue: null }
17
+ });
18
+ expect(gauge.hasValue()).toBe(false);
19
+ });
20
+
21
+ it('should return the same startTime and endTime as it is an instantaneous point', () => {
22
+ jest.useFakeTimers().setSystemTime(new Date(8880));
23
+ const gauge = createGauge();
24
+
25
+ expect(gauge.getState().startTime).toBe(gauge.getState().endTime);
26
+ });
27
+
28
+ it('should store the value when it is set', () => {
29
+ const gauge = createGauge();
30
+
31
+ gauge.add(42);
32
+
33
+ expect(gauge.getState()).toMatchObject({ value: { doubleValue: 42 } });
34
+ expect(gauge.hasValue()).toBe(true);
35
+ });
36
+
37
+ it('should keep only the last value when multiple values are set', () => {
38
+ const gauge = createGauge();
39
+
40
+ gauge.add(5);
41
+ gauge.add(9);
42
+ gauge.add(3);
43
+
44
+ expect(gauge.getState()).toMatchObject({ value: { doubleValue: 3 } });
45
+ });
46
+
47
+ it('should support fractional values', () => {
48
+ const gauge = createGauge();
49
+
50
+ gauge.add(1.5);
51
+
52
+ expect(gauge.getState()).toMatchObject({ value: { doubleValue: 1.5 } });
53
+ });
54
+
55
+ it('should reset the value on advanceTimer call', () => {
56
+ const gauge = createGauge();
57
+
58
+ gauge.add(7);
59
+ gauge.advanceTimer();
60
+
61
+ expect(gauge.getState()).toMatchObject({ value: { doubleValue: null } });
62
+ expect(gauge.hasValue()).toBe(false);
63
+ });
64
+
65
+ it('should store a new value after advanceTimer call', () => {
66
+ const gauge = createGauge();
67
+
68
+ gauge.add(7);
69
+ gauge.advanceTimer();
70
+ gauge.add(2);
71
+
72
+ expect(gauge.getState()).toMatchObject({ value: { doubleValue: 2 } });
73
+ expect(gauge.hasValue()).toBe(true);
74
+ });
75
+
76
+ it('should return current time as endTime', () => {
77
+ jest.useFakeTimers().setSystemTime(new Date(880));
78
+ const gauge = createGauge();
79
+
80
+ jest.advanceTimersByTime(20);
81
+
82
+ expect(gauge.getState()).toMatchObject({ endTime: 900 });
83
+ });
84
+ });