@openobserve/browser-core 0.2.12-beta.27 → 0.2.12-beta.28

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 (80) hide show
  1. package/cjs/boot/init.js +1 -1
  2. package/cjs/domain/configuration/endpointBuilder.js +1 -1
  3. package/cjs/domain/tags.js +1 -1
  4. package/cjs/domain/telemetry/telemetry.js +1 -1
  5. package/esm/boot/init.js +1 -1
  6. package/esm/domain/configuration/endpointBuilder.js +1 -1
  7. package/esm/domain/tags.js +1 -1
  8. package/esm/domain/telemetry/telemetry.js +1 -1
  9. package/package.json +2 -2
  10. package/src/boot/displayAlreadyInitializedError.spec.ts +18 -0
  11. package/src/boot/init.spec.ts +50 -0
  12. package/src/browser/addEventListener.spec.ts +141 -0
  13. package/src/browser/cookie.spec.ts +53 -0
  14. package/src/browser/fetchObservable.spec.ts +334 -0
  15. package/src/browser/pageMayExitObservable.spec.ts +56 -0
  16. package/src/browser/xhrObservable.spec.ts +405 -0
  17. package/src/domain/allowedTrackingOrigins.spec.ts +210 -0
  18. package/src/domain/bufferedData.spec.ts +34 -0
  19. package/src/domain/configuration/configuration.spec.ts +236 -0
  20. package/src/domain/configuration/endpointBuilder.spec.ts +173 -0
  21. package/src/domain/configuration/transportConfiguration.spec.ts +130 -0
  22. package/src/domain/connectivity/connectivity.spec.ts +50 -0
  23. package/src/domain/console/consoleObservable.spec.ts +151 -0
  24. package/src/domain/context/contextManager.spec.ts +152 -0
  25. package/src/domain/context/contextUtils.spec.ts +26 -0
  26. package/src/domain/context/storeContextManager.spec.ts +103 -0
  27. package/src/domain/contexts/accountContext.spec.ts +99 -0
  28. package/src/domain/contexts/globalContext.spec.ts +88 -0
  29. package/src/domain/contexts/userContext.spec.ts +156 -0
  30. package/src/domain/error/error.spec.ts +294 -0
  31. package/src/domain/error/trackRuntimeError.spec.ts +343 -0
  32. package/src/domain/eventRateLimiter/createEventRateLimiter.spec.ts +119 -0
  33. package/src/domain/extension/extensionUtils.spec.ts +47 -0
  34. package/src/domain/report/reportObservable.spec.ts +71 -0
  35. package/src/domain/session/oldCookiesMigration.spec.ts +76 -0
  36. package/src/domain/session/sessionManager.spec.ts +684 -0
  37. package/src/domain/session/sessionState.spec.ts +88 -0
  38. package/src/domain/session/sessionStore.spec.ts +647 -0
  39. package/src/domain/session/sessionStoreOperations.spec.ts +234 -0
  40. package/src/domain/session/storeStrategies/sessionInCookie.spec.ts +228 -0
  41. package/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts +75 -0
  42. package/src/domain/synthetics/syntheticsWorkerValues.spec.ts +82 -0
  43. package/src/domain/tags.spec.ts +74 -0
  44. package/src/domain/telemetry/telemetry.spec.ts +494 -0
  45. package/src/domain/trackingConsent.spec.ts +44 -0
  46. package/src/tools/abstractHooks.spec.ts +76 -0
  47. package/src/tools/abstractLifeCycle.spec.ts +46 -0
  48. package/src/tools/boundedBuffer.spec.ts +40 -0
  49. package/src/tools/catchUserErrors.spec.ts +20 -0
  50. package/src/tools/encoder.spec.ts +112 -0
  51. package/src/tools/experimentalFeatures.spec.ts +60 -0
  52. package/src/tools/getZoneJsOriginalValue.spec.ts +36 -0
  53. package/src/tools/instrumentMethod.spec.ts +381 -0
  54. package/src/tools/matchOption.spec.ts +41 -0
  55. package/src/tools/mergeInto.spec.ts +198 -0
  56. package/src/tools/monitor.spec.ts +172 -0
  57. package/src/tools/observable.spec.ts +259 -0
  58. package/src/tools/queueMicrotask.spec.ts +24 -0
  59. package/src/tools/readBytesFromStream.spec.ts +62 -0
  60. package/src/tools/requestIdleCallback.spec.ts +65 -0
  61. package/src/tools/serialisation/jsonStringify.spec.ts +76 -0
  62. package/src/tools/serialisation/sanitize.spec.ts +284 -0
  63. package/src/tools/stackTrace/capturedExceptions.specHelper.ts +316 -0
  64. package/src/tools/stackTrace/computeStackTrace.spec.ts +1007 -0
  65. package/src/tools/stackTrace/handlingStack.spec.ts +20 -0
  66. package/src/tools/taskQueue.spec.ts +60 -0
  67. package/src/tools/timer.spec.ts +76 -0
  68. package/src/tools/utils/browserDetection.spec.ts +120 -0
  69. package/src/tools/utils/byteUtils.spec.ts +29 -0
  70. package/src/tools/utils/functionUtils.spec.ts +229 -0
  71. package/src/tools/utils/numberUtils.spec.ts +27 -0
  72. package/src/tools/utils/stringUtils.spec.ts +74 -0
  73. package/src/tools/utils/typeUtils.spec.ts +25 -0
  74. package/src/tools/utils/urlPolyfill.spec.ts +55 -0
  75. package/src/tools/valueHistory.spec.ts +180 -0
  76. package/src/transport/batch.spec.ts +261 -0
  77. package/src/transport/eventBridge.spec.ts +90 -0
  78. package/src/transport/flushController.spec.ts +267 -0
  79. package/src/transport/httpRequest.spec.ts +400 -0
  80. package/src/transport/sendWithRetryStrategy.spec.ts +393 -0
@@ -0,0 +1,46 @@
1
+ import { AbstractLifeCycle } from './abstractLifeCycle'
2
+
3
+ describe('AbstractLifeCycle', () => {
4
+ const LifeCycle = AbstractLifeCycle<{
5
+ foo: 'bar'
6
+ no_data: void
7
+ }>
8
+
9
+ it('does nothing when notifying without subscribers', () => {
10
+ const lifeCycle = new LifeCycle()
11
+ expect(() => lifeCycle.notify('foo', 'bar')).not.toThrow()
12
+ })
13
+
14
+ it('notifies subscribers', () => {
15
+ const lifeCycle = new LifeCycle()
16
+ const subscriber1Spy = jasmine.createSpy()
17
+ const subscriber2Spy = jasmine.createSpy()
18
+ lifeCycle.subscribe('foo', subscriber1Spy)
19
+ lifeCycle.subscribe('foo', subscriber2Spy)
20
+
21
+ lifeCycle.notify('foo', 'bar')
22
+
23
+ expect(subscriber1Spy).toHaveBeenCalledOnceWith('bar')
24
+ expect(subscriber2Spy).toHaveBeenCalledOnceWith('bar')
25
+ })
26
+
27
+ it('notifies subscribers for events without data', () => {
28
+ const lifeCycle = new LifeCycle()
29
+ const subscriberSpy = jasmine.createSpy()
30
+ lifeCycle.subscribe('no_data', subscriberSpy)
31
+
32
+ lifeCycle.notify('no_data')
33
+
34
+ expect(subscriberSpy).toHaveBeenCalledOnceWith(undefined)
35
+ })
36
+
37
+ it('does not notify unsubscribed subscribers', () => {
38
+ const lifeCycle = new LifeCycle()
39
+ const subscriberSpy = jasmine.createSpy()
40
+ lifeCycle.subscribe('foo', subscriberSpy).unsubscribe()
41
+
42
+ lifeCycle.notify('foo', 'bar')
43
+
44
+ expect(subscriberSpy).not.toHaveBeenCalled()
45
+ })
46
+ })
@@ -0,0 +1,40 @@
1
+ import { createBoundedBuffer } from './boundedBuffer'
2
+
3
+ describe('BoundedBuffer', () => {
4
+ it('collect and drain the callbacks', () => {
5
+ const spy = jasmine.createSpy<() => void>()
6
+ const buffered = createBoundedBuffer()
7
+
8
+ buffered.add(spy)
9
+ expect(spy.calls.count()).toBe(0)
10
+
11
+ buffered.drain()
12
+ expect(spy.calls.count()).toBe(1)
13
+
14
+ buffered.drain()
15
+ expect(spy.calls.count()).toBe(1)
16
+ })
17
+
18
+ it('store at most 500 callbacks', () => {
19
+ const spy = jasmine.createSpy<() => void>()
20
+ const buffered = createBoundedBuffer()
21
+ const limit = 500
22
+
23
+ for (let i = 0; i < limit + 1; i += 1) {
24
+ buffered.add(spy)
25
+ }
26
+
27
+ buffered.drain()
28
+ expect(spy.calls.count()).toEqual(limit)
29
+ })
30
+
31
+ it('removes a callback', () => {
32
+ const spy = jasmine.createSpy<() => void>()
33
+ const buffered = createBoundedBuffer()
34
+
35
+ buffered.add(spy)
36
+ buffered.remove(spy)
37
+ buffered.drain()
38
+ expect(spy).not.toHaveBeenCalled()
39
+ })
40
+ })
@@ -0,0 +1,20 @@
1
+ import { catchUserErrors } from './catchUserErrors'
2
+ import { display } from './display'
3
+
4
+ describe('catchUserErrors', () => {
5
+ it('returns the same result as the original function', () => {
6
+ const wrappedFn = catchUserErrors((a: number, b: number) => a + b, 'Error during callback')
7
+ expect(wrappedFn(10, 2)).toBe(12)
8
+ })
9
+
10
+ it('logs errors using console.error and returns undefined', () => {
11
+ const displaySpy = spyOn(display, 'error')
12
+ const myError = 'Ooops!'
13
+ const wrappedFn = catchUserErrors(() => {
14
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
15
+ throw myError
16
+ }, 'Error during callback')
17
+ expect(wrappedFn()).toBe(undefined)
18
+ expect(displaySpy).toHaveBeenCalledWith('Error during callback', myError)
19
+ })
20
+ })
@@ -0,0 +1,112 @@
1
+ import { createIdentityEncoder } from './encoder'
2
+ import { noop } from './utils/functionUtils'
3
+
4
+ describe('createIdentityEncoder', () => {
5
+ it('creates an encoder with initial values', () => {
6
+ const encoder = createIdentityEncoder()
7
+
8
+ expect(encoder.isEmpty).toBe(true)
9
+ expect(encoder.finishSync()).toEqual({
10
+ output: '',
11
+ outputBytesCount: 0,
12
+ rawBytesCount: 0,
13
+ pendingData: '',
14
+ })
15
+ })
16
+
17
+ describe('write()', () => {
18
+ it('writes data to the encoder', () => {
19
+ const encoder = createIdentityEncoder()
20
+ const data = 'Hello, world!'
21
+
22
+ encoder.write(data)
23
+
24
+ expect(encoder.isEmpty).toBe(false)
25
+ })
26
+
27
+ it('calls the callback when writing data with a callback', () => {
28
+ const encoder = createIdentityEncoder()
29
+ const data = 'Callback test'
30
+ const callbackSpy = jasmine.createSpy()
31
+
32
+ encoder.write(data, callbackSpy)
33
+
34
+ expect(callbackSpy).toHaveBeenCalledOnceWith(data.length)
35
+ })
36
+ })
37
+
38
+ describe('finish()', () => {
39
+ it('calls the callback with the result', () => {
40
+ const encoder = createIdentityEncoder()
41
+ const data = 'Final data'
42
+ encoder.write(data)
43
+ const callbackSpy = jasmine.createSpy()
44
+
45
+ encoder.finish(callbackSpy)
46
+
47
+ expect(callbackSpy).toHaveBeenCalledWith({
48
+ output: data,
49
+ outputBytesCount: data.length,
50
+ rawBytesCount: data.length,
51
+ pendingData: '',
52
+ })
53
+ })
54
+
55
+ it('after calling finish(), the encoder should be considered empty', () => {
56
+ const encoder = createIdentityEncoder()
57
+ encoder.write('Some data')
58
+
59
+ encoder.finish(noop)
60
+
61
+ expect(encoder.isEmpty).toBe(true)
62
+ expect(encoder.finishSync()).toEqual({
63
+ output: '',
64
+ outputBytesCount: 0,
65
+ rawBytesCount: 0,
66
+ pendingData: '',
67
+ })
68
+ })
69
+ })
70
+
71
+ describe('finishSync()', () => {
72
+ it('returns the encoder result', () => {
73
+ const encoder = createIdentityEncoder()
74
+ const data = 'Hello, world!'
75
+
76
+ encoder.write(data)
77
+
78
+ expect(encoder.finishSync()).toEqual({
79
+ output: data,
80
+ outputBytesCount: data.length,
81
+ rawBytesCount: data.length,
82
+ pendingData: '',
83
+ })
84
+ })
85
+
86
+ it('after calling finish(), the encoder should be considered empty', () => {
87
+ const encoder = createIdentityEncoder()
88
+ encoder.write('Some data')
89
+
90
+ encoder.finishSync()
91
+
92
+ expect(encoder.isEmpty).toBe(true)
93
+ expect(encoder.finishSync()).toEqual({
94
+ output: '',
95
+ outputBytesCount: 0,
96
+ rawBytesCount: 0,
97
+ pendingData: '',
98
+ })
99
+ })
100
+ })
101
+
102
+ describe('estimateEncodedBytesCount()', () => {
103
+ it('estimates encoded bytes count accurately', () => {
104
+ const encoder = createIdentityEncoder()
105
+ const data = 'Estimation test'
106
+
107
+ const estimatedBytes = encoder.estimateEncodedBytesCount(data)
108
+
109
+ expect(estimatedBytes).toBe(data.length)
110
+ })
111
+ })
112
+ })
@@ -0,0 +1,60 @@
1
+ import {
2
+ ExperimentalFeature,
3
+ addExperimentalFeatures,
4
+ initFeatureFlags,
5
+ isExperimentalFeatureEnabled,
6
+ resetExperimentalFeatures,
7
+ } from './experimentalFeatures'
8
+
9
+ const TEST_FEATURE_FLAG_ONE = 'foo' as ExperimentalFeature
10
+ const TEST_FEATURE_FLAG_TWO = 'bar' as ExperimentalFeature
11
+
12
+ describe('experimentalFeatures', () => {
13
+ afterEach(() => {
14
+ resetExperimentalFeatures()
15
+ })
16
+
17
+ it('initial state is empty', () => {
18
+ expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBeFalse()
19
+ expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBeFalse()
20
+ })
21
+
22
+ it('should define enabled experimental features', () => {
23
+ addExperimentalFeatures([TEST_FEATURE_FLAG_ONE])
24
+ expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBeTrue()
25
+ expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBeFalse()
26
+ })
27
+
28
+ it('should allow to be shared between products', () => {
29
+ addExperimentalFeatures([TEST_FEATURE_FLAG_ONE])
30
+ addExperimentalFeatures([TEST_FEATURE_FLAG_TWO])
31
+
32
+ expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBeTrue()
33
+ expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBeTrue()
34
+ })
35
+ })
36
+
37
+ describe('initFeatureFlags', () => {
38
+ beforeEach(() => {
39
+ ;(ExperimentalFeature as any).FOO = TEST_FEATURE_FLAG_ONE
40
+ })
41
+
42
+ afterEach(() => {
43
+ delete (ExperimentalFeature as any).FOO
44
+ resetExperimentalFeatures()
45
+ })
46
+
47
+ it('ignores unknown experimental features', () => {
48
+ initFeatureFlags(['bar', undefined as any, null as any, 11 as any])
49
+
50
+ expect(isExperimentalFeatureEnabled('bar' as any)).toBeFalse()
51
+ expect(isExperimentalFeatureEnabled(undefined as any)).toBeFalse()
52
+ expect(isExperimentalFeatureEnabled(null as any)).toBeFalse()
53
+ expect(isExperimentalFeatureEnabled(11 as any)).toBeFalse()
54
+ })
55
+
56
+ it('updates experimental feature flags', () => {
57
+ initFeatureFlags(['foo'])
58
+ expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBeTrue()
59
+ })
60
+ })
@@ -0,0 +1,36 @@
1
+ import { mockZoneJs } from '../../test'
2
+
3
+ import type { BrowserWindowWithZoneJs } from './getZoneJsOriginalValue'
4
+ import { getZoneJsOriginalValue } from './getZoneJsOriginalValue'
5
+ import { noop } from './utils/functionUtils'
6
+
7
+ describe('getZoneJsOriginalValue', () => {
8
+ function originalValue() {
9
+ // just a function that does nothing but different than 'noop' as we'll want to differentiate
10
+ // them.
11
+ }
12
+ const object = {
13
+ name: originalValue,
14
+ }
15
+
16
+ it('returns the original value directly if Zone is not not defined', () => {
17
+ expect(getZoneJsOriginalValue(object, 'name')).toBe(originalValue)
18
+ })
19
+
20
+ it("returns the original value if Zone is defined but didn't patch that method", () => {
21
+ mockZoneJs()
22
+ expect(getZoneJsOriginalValue(object, 'name')).toBe(originalValue)
23
+ })
24
+
25
+ it('returns the original value if Zone is defined but does not define the __symbol__ function', () => {
26
+ mockZoneJs()
27
+ delete (window as BrowserWindowWithZoneJs).Zone!.__symbol__
28
+ expect(getZoneJsOriginalValue(object, 'name')).toBe(originalValue)
29
+ })
30
+
31
+ it('returns the original value if Zone did patch the method', () => {
32
+ const zoneJs = mockZoneJs()
33
+ zoneJs.replaceProperty(object, 'name', noop)
34
+ expect(getZoneJsOriginalValue(object, 'name')).toBe(originalValue)
35
+ })
36
+ })
@@ -0,0 +1,381 @@
1
+ import { mockClock, mockZoneJs } from '../../test'
2
+ import type { Clock, MockZoneJs } from '../../test'
3
+ import type { InstrumentedMethodCall } from './instrumentMethod'
4
+ import { instrumentMethod, instrumentSetter } from './instrumentMethod'
5
+ import { noop } from './utils/functionUtils'
6
+
7
+ describe('instrumentMethod', () => {
8
+ const THIRD_PARTY_RESULT = 42
9
+
10
+ it('replaces the original method', () => {
11
+ const original = () => 1
12
+ const object = { method: original }
13
+
14
+ instrumentMethod(object, 'method', noop)
15
+
16
+ expect(object.method).not.toBe(original)
17
+ })
18
+
19
+ it('calls the instrumentation before the original method', () => {
20
+ const originalSpy = jasmine.createSpy()
21
+ const instrumentationSpy = jasmine.createSpy()
22
+ const object = { method: originalSpy }
23
+
24
+ instrumentMethod(object, 'method', instrumentationSpy)
25
+
26
+ object.method()
27
+
28
+ expect(instrumentationSpy).toHaveBeenCalledBefore(originalSpy)
29
+ })
30
+
31
+ it('does not set a method originally undefined', () => {
32
+ const object: { method?: () => number } = {}
33
+
34
+ instrumentMethod(object, 'method', noop)
35
+
36
+ expect(object.method).toBeUndefined()
37
+ })
38
+
39
+ it('sets an event handler even if it was originally undefined', () => {
40
+ const object: { onevent?: () => void } = { onevent: undefined }
41
+
42
+ const instrumentationSpy = jasmine.createSpy()
43
+ instrumentMethod(object, 'onevent', instrumentationSpy)
44
+
45
+ expect(object.onevent).toBeDefined()
46
+
47
+ object.onevent!()
48
+ expect(instrumentationSpy).toHaveBeenCalled()
49
+ })
50
+
51
+ it('do not set an event handler even if the event is not supported (i.e. property does not exist on object)', () => {
52
+ const object: { onevent?: () => void } = {}
53
+
54
+ const instrumentationSpy = jasmine.createSpy()
55
+ instrumentMethod(object, 'onevent', instrumentationSpy)
56
+
57
+ expect('onevent' in object).toBeFalse()
58
+ })
59
+
60
+ it('calls the instrumentation with method target and parameters', () => {
61
+ const object = { method: (a: number, b: number) => a + b }
62
+ const instrumentationSpy = jasmine.createSpy<(call: InstrumentedMethodCall<typeof object, 'method'>) => void>()
63
+ instrumentMethod(object, 'method', instrumentationSpy)
64
+
65
+ object.method(2, 3)
66
+
67
+ expect(instrumentationSpy).toHaveBeenCalledOnceWith({
68
+ target: object,
69
+ parameters: jasmine.any(Object),
70
+ onPostCall: jasmine.any(Function),
71
+ handlingStack: undefined,
72
+ })
73
+ expect(instrumentationSpy.calls.mostRecent().args[0].parameters[0]).toBe(2)
74
+ expect(instrumentationSpy.calls.mostRecent().args[0].parameters[1]).toBe(3)
75
+ })
76
+
77
+ it('allows replacing a parameter', () => {
78
+ const object = { method: (a: number) => a }
79
+ instrumentMethod(object, 'method', ({ parameters }) => {
80
+ parameters[0] = 2
81
+ })
82
+
83
+ expect(object.method(1)).toBe(2)
84
+ })
85
+
86
+ it('allows adding a parameter', () => {
87
+ const object = { method: (a?: number) => a }
88
+ instrumentMethod(object, 'method', ({ parameters }) => {
89
+ parameters[0] = 2
90
+ })
91
+
92
+ expect(object.method()).toBe(2)
93
+ })
94
+
95
+ it('calls the "onPostCall" callback with the original method result', () => {
96
+ const object = { method: () => 1 }
97
+ const onPostCallSpy = jasmine.createSpy()
98
+ instrumentMethod(object, 'method', ({ onPostCall }) => onPostCall(onPostCallSpy))
99
+
100
+ object.method()
101
+
102
+ expect(onPostCallSpy).toHaveBeenCalledOnceWith(1)
103
+ })
104
+
105
+ it('allows other instrumentations from third parties', () => {
106
+ const object = { method: () => 1 }
107
+ const instrumentationSpy = jasmine.createSpy()
108
+ instrumentMethod(object, 'method', instrumentationSpy)
109
+
110
+ thirdPartyInstrumentation(object)
111
+
112
+ expect(object.method()).toBe(THIRD_PARTY_RESULT)
113
+ expect(instrumentationSpy).toHaveBeenCalled()
114
+ })
115
+
116
+ it('computes the handling stack', () => {
117
+ const object = { method: () => 1 }
118
+ const instrumentationSpy = jasmine.createSpy()
119
+ instrumentMethod(object, 'method', instrumentationSpy, { computeHandlingStack: true })
120
+
121
+ function foo() {
122
+ object.method()
123
+ }
124
+
125
+ foo()
126
+
127
+ expect(instrumentationSpy.calls.mostRecent().args[0].handlingStack).toEqual(
128
+ jasmine.stringMatching(/^HandlingStack: instrumented method\n {2}at foo @/)
129
+ )
130
+ })
131
+
132
+ describe('stop()', () => {
133
+ it('does not call the instrumentation anymore', () => {
134
+ const object = { method: () => 1 }
135
+ const instrumentationSpy = jasmine.createSpy()
136
+ const { stop } = instrumentMethod(object, 'method', () => instrumentationSpy)
137
+
138
+ stop()
139
+
140
+ object.method()
141
+ expect(instrumentationSpy).not.toHaveBeenCalled()
142
+ })
143
+
144
+ describe('when the method has been instrumented by a third party', () => {
145
+ it('should not break the third party instrumentation', () => {
146
+ const object = { method: () => 1 }
147
+ const { stop } = instrumentMethod(object, 'method', noop)
148
+
149
+ thirdPartyInstrumentation(object)
150
+ const instrumentedMethod = object.method
151
+
152
+ stop()
153
+
154
+ expect(object.method).toBe(instrumentedMethod)
155
+ })
156
+
157
+ it('does not call the instrumentation', () => {
158
+ const object = { method: () => 1 }
159
+ const instrumentationSpy = jasmine.createSpy()
160
+ const { stop } = instrumentMethod(object, 'method', instrumentationSpy)
161
+
162
+ thirdPartyInstrumentation(object)
163
+
164
+ stop()
165
+
166
+ expect(instrumentationSpy).not.toHaveBeenCalled()
167
+ })
168
+
169
+ it('should not throw errors if original method was undefined', () => {
170
+ const object: { onevent?: () => number } = {}
171
+ const instrumentationStub = () => 2
172
+ const { stop } = instrumentMethod(object, 'onevent', instrumentationStub)
173
+
174
+ thirdPartyInstrumentation(object)
175
+
176
+ stop()
177
+
178
+ expect(object.onevent).not.toThrow()
179
+ })
180
+ })
181
+ })
182
+
183
+ function thirdPartyInstrumentation(object: { method?: () => number; onevent?: () => void }) {
184
+ const originalMethod = object.method
185
+ if (typeof originalMethod === 'function') {
186
+ object.method = () => {
187
+ originalMethod()
188
+ return THIRD_PARTY_RESULT
189
+ }
190
+ }
191
+
192
+ const originalOnEvent = object.onevent
193
+ object.onevent = () => {
194
+ if (originalOnEvent) {
195
+ originalOnEvent()
196
+ }
197
+ }
198
+ }
199
+ })
200
+
201
+ describe('instrumentSetter', () => {
202
+ let clock: Clock
203
+ let zoneJs: MockZoneJs
204
+
205
+ beforeEach(() => {
206
+ clock = mockClock()
207
+ zoneJs = mockZoneJs()
208
+ })
209
+
210
+ it('replaces the original setter', () => {
211
+ const originalSetter = () => {
212
+ // do nothing particular, only used to test if this setter gets replaced
213
+ }
214
+ const object = {} as { foo: number }
215
+ Object.defineProperty(object, 'foo', { set: originalSetter, configurable: true })
216
+
217
+ instrumentSetter(object, 'foo', noop)
218
+
219
+ // eslint-disable-next-line @typescript-eslint/unbound-method
220
+ expect(Object.getOwnPropertyDescriptor(object, 'foo')!.set).not.toBe(originalSetter)
221
+ })
222
+
223
+ it('skips instrumentation if there is no original setter', () => {
224
+ const object = { foo: 1 }
225
+
226
+ instrumentSetter(object, 'foo', noop)
227
+ // eslint-disable-next-line @typescript-eslint/unbound-method
228
+ expect(Object.getOwnPropertyDescriptor(object, 'foo')!.set).toBeUndefined()
229
+ })
230
+
231
+ it('skips instrumentation if the descriptor is not configurable', () => {
232
+ const originalSetter = () => {
233
+ // do nothing particular, only used to test if this setter gets replaced
234
+ }
235
+ const object = {} as { foo: number }
236
+ Object.defineProperty(object, 'foo', { set: originalSetter, configurable: false })
237
+
238
+ instrumentSetter(object, 'foo', noop)
239
+ // eslint-disable-next-line @typescript-eslint/unbound-method
240
+ expect(Object.getOwnPropertyDescriptor(object, 'foo')!.set).toBe(originalSetter)
241
+ })
242
+
243
+ it('calls the original setter', () => {
244
+ const originalSetterSpy = jasmine.createSpy()
245
+ const object = {} as { foo: number }
246
+ Object.defineProperty(object, 'foo', { set: originalSetterSpy, configurable: true })
247
+
248
+ instrumentSetter(object, 'foo', noop)
249
+
250
+ object.foo = 1
251
+ expect(originalSetterSpy).toHaveBeenCalledOnceWith(1)
252
+ })
253
+
254
+ it('calls the instrumentation asynchronously', () => {
255
+ const instrumentationSetterSpy = jasmine.createSpy()
256
+ const object = {} as { foo: number }
257
+ Object.defineProperty(object, 'foo', { set: noop, configurable: true })
258
+
259
+ instrumentSetter(object, 'foo', instrumentationSetterSpy)
260
+
261
+ object.foo = 1
262
+ expect(instrumentationSetterSpy).not.toHaveBeenCalled()
263
+ clock.tick(0)
264
+ expect(instrumentationSetterSpy).toHaveBeenCalledOnceWith(object, 1)
265
+ })
266
+
267
+ it('does not use the Zone.js setTimeout function', () => {
268
+ const zoneJsSetTimeoutSpy = jasmine.createSpy()
269
+ zoneJs.replaceProperty(window, 'setTimeout', zoneJsSetTimeoutSpy)
270
+
271
+ const object = {} as { foo: number }
272
+ Object.defineProperty(object, 'foo', { set: noop, configurable: true })
273
+
274
+ instrumentSetter(object, 'foo', noop)
275
+ object.foo = 2
276
+
277
+ clock.tick(0)
278
+
279
+ expect(zoneJsSetTimeoutSpy).not.toHaveBeenCalled()
280
+ })
281
+
282
+ it('allows other instrumentations from third parties', () => {
283
+ const object = {} as { foo: number }
284
+ Object.defineProperty(object, 'foo', { set: noop, configurable: true })
285
+ const instrumentationSetterSpy = jasmine.createSpy()
286
+ instrumentSetter(object, 'foo', instrumentationSetterSpy)
287
+
288
+ const thirdPartyInstrumentationSpy = thirdPartyInstrumentation(object)
289
+
290
+ object.foo = 2
291
+ expect(thirdPartyInstrumentationSpy).toHaveBeenCalledOnceWith(2)
292
+ clock.tick(0)
293
+ expect(instrumentationSetterSpy).toHaveBeenCalledOnceWith(object, 2)
294
+ })
295
+
296
+ describe('stop()', () => {
297
+ it('restores the original behavior', () => {
298
+ const object = {} as { foo: number }
299
+ const originalSetter = () => {
300
+ // do nothing particular, only used to test if this setter gets replaced
301
+ }
302
+ Object.defineProperty(object, 'foo', { set: originalSetter, configurable: true })
303
+ const { stop } = instrumentSetter(object, 'foo', noop)
304
+
305
+ stop()
306
+
307
+ // eslint-disable-next-line @typescript-eslint/unbound-method
308
+ expect(Object.getOwnPropertyDescriptor(object, 'foo')!.set).toBe(originalSetter)
309
+ })
310
+
311
+ it('does not call the instrumentation anymore', () => {
312
+ const object = {} as { foo: number }
313
+ Object.defineProperty(object, 'foo', { set: noop, configurable: true })
314
+ const instrumentationSetterSpy = jasmine.createSpy()
315
+ const { stop } = instrumentSetter(object, 'foo', instrumentationSetterSpy)
316
+
317
+ stop()
318
+
319
+ object.foo = 2
320
+ clock.tick(0)
321
+
322
+ expect(instrumentationSetterSpy).not.toHaveBeenCalled()
323
+ })
324
+
325
+ it('does not call instrumentation pending in the event loop via setTimeout', () => {
326
+ const object = {} as { foo: number }
327
+ Object.defineProperty(object, 'foo', { set: noop, configurable: true })
328
+ const instrumentationSetterSpy = jasmine.createSpy()
329
+ const { stop } = instrumentSetter(object, 'foo', instrumentationSetterSpy)
330
+
331
+ object.foo = 2
332
+ stop()
333
+ clock.tick(0)
334
+
335
+ expect(instrumentationSetterSpy).not.toHaveBeenCalled()
336
+ })
337
+
338
+ describe('when the method has been instrumented by a third party', () => {
339
+ it('should not break the third party instrumentation', () => {
340
+ const object = {} as { foo: number }
341
+ Object.defineProperty(object, 'foo', { set: noop, configurable: true })
342
+ const { stop } = instrumentSetter(object, 'foo', noop)
343
+
344
+ const thirdPartyInstrumentationSpy = thirdPartyInstrumentation(object)
345
+
346
+ stop()
347
+
348
+ // eslint-disable-next-line @typescript-eslint/unbound-method
349
+ expect(Object.getOwnPropertyDescriptor(object, 'foo')!.set).toBe(thirdPartyInstrumentationSpy)
350
+ })
351
+
352
+ it('does not call the instrumentation', () => {
353
+ const object = {} as { foo: number }
354
+ Object.defineProperty(object, 'foo', { set: noop, configurable: true })
355
+ const instrumentationSetterSpy = jasmine.createSpy()
356
+ const { stop } = instrumentSetter(object, 'foo', instrumentationSetterSpy)
357
+
358
+ thirdPartyInstrumentation(object)
359
+
360
+ stop()
361
+
362
+ object.foo = 2
363
+ clock.tick(0)
364
+
365
+ expect(instrumentationSetterSpy).not.toHaveBeenCalled()
366
+ })
367
+ })
368
+ })
369
+
370
+ function thirdPartyInstrumentation(object: { foo: number }) {
371
+ // eslint-disable-next-line @typescript-eslint/unbound-method
372
+ const originalSetter = Object.getOwnPropertyDescriptor(object, 'foo')!.set
373
+ const thirdPartyInstrumentationSpy = jasmine.createSpy().and.callFake(function (this: any, value) {
374
+ if (originalSetter) {
375
+ originalSetter.call(this, value)
376
+ }
377
+ })
378
+ Object.defineProperty(object, 'foo', { set: thirdPartyInstrumentationSpy })
379
+ return thirdPartyInstrumentationSpy
380
+ }
381
+ })