@cratis/arc 18.4.3 → 18.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.
Files changed (94) hide show
  1. package/commands/for_Command/given/a_command.ts +4 -5
  2. package/commands/for_Command/when_executing/with_custom_http_headers.ts +4 -1
  3. package/commands/for_Command/when_executing/with_origin_and_api_base_path.ts +5 -2
  4. package/commands/for_Command/when_executing/with_route_parameters.ts +4 -2
  5. package/dist/cjs/helpers/fetchHelper.d.ts +7 -0
  6. package/dist/cjs/helpers/fetchHelper.d.ts.map +1 -0
  7. package/dist/cjs/identity/IIdentityProvider.d.ts +2 -1
  8. package/dist/cjs/identity/IIdentityProvider.d.ts.map +1 -1
  9. package/dist/cjs/identity/IIdentityProvider.js.map +1 -1
  10. package/dist/cjs/identity/IdentityProvider.d.ts +4 -3
  11. package/dist/cjs/identity/IdentityProvider.d.ts.map +1 -1
  12. package/dist/cjs/identity/IdentityProvider.js +12 -9
  13. package/dist/cjs/identity/IdentityProvider.js.map +1 -1
  14. package/dist/cjs/queries/IObservableQueryConnection.d.ts +2 -0
  15. package/dist/cjs/queries/IObservableQueryConnection.d.ts.map +1 -1
  16. package/dist/cjs/queries/NullObservableQueryConnection.d.ts +2 -0
  17. package/dist/cjs/queries/NullObservableQueryConnection.d.ts.map +1 -1
  18. package/dist/cjs/queries/NullObservableQueryConnection.js +6 -0
  19. package/dist/cjs/queries/NullObservableQueryConnection.js.map +1 -1
  20. package/dist/cjs/queries/ObservableQueryConnection.d.ts +14 -1
  21. package/dist/cjs/queries/ObservableQueryConnection.d.ts.map +1 -1
  22. package/dist/cjs/queries/ObservableQueryConnection.js +73 -2
  23. package/dist/cjs/queries/ObservableQueryConnection.js.map +1 -1
  24. package/dist/cjs/queries/WebSocketMessage.d.ts +11 -0
  25. package/dist/cjs/queries/WebSocketMessage.d.ts.map +1 -0
  26. package/dist/cjs/queries/WebSocketMessage.js +9 -0
  27. package/dist/cjs/queries/WebSocketMessage.js.map +1 -0
  28. package/dist/cjs/queries/index.d.ts +1 -0
  29. package/dist/cjs/queries/index.d.ts.map +1 -1
  30. package/dist/cjs/queries/index.js +5 -0
  31. package/dist/cjs/queries/index.js.map +1 -1
  32. package/dist/esm/helpers/fetchHelper.d.ts +7 -0
  33. package/dist/esm/helpers/fetchHelper.d.ts.map +1 -0
  34. package/dist/esm/helpers/fetchHelper.js +22 -0
  35. package/dist/esm/helpers/fetchHelper.js.map +1 -0
  36. package/dist/esm/identity/IIdentityProvider.d.ts +2 -1
  37. package/dist/esm/identity/IIdentityProvider.d.ts.map +1 -1
  38. package/dist/esm/identity/IIdentityProvider.js.map +1 -1
  39. package/dist/esm/identity/IdentityProvider.d.ts +4 -3
  40. package/dist/esm/identity/IdentityProvider.d.ts.map +1 -1
  41. package/dist/esm/identity/IdentityProvider.js +12 -9
  42. package/dist/esm/identity/IdentityProvider.js.map +1 -1
  43. package/dist/esm/queries/IObservableQueryConnection.d.ts +2 -0
  44. package/dist/esm/queries/IObservableQueryConnection.d.ts.map +1 -1
  45. package/dist/esm/queries/NullObservableQueryConnection.d.ts +2 -0
  46. package/dist/esm/queries/NullObservableQueryConnection.d.ts.map +1 -1
  47. package/dist/esm/queries/NullObservableQueryConnection.js +6 -0
  48. package/dist/esm/queries/NullObservableQueryConnection.js.map +1 -1
  49. package/dist/esm/queries/ObservableQueryConnection.d.ts +14 -1
  50. package/dist/esm/queries/ObservableQueryConnection.d.ts.map +1 -1
  51. package/dist/esm/queries/ObservableQueryConnection.js +73 -2
  52. package/dist/esm/queries/ObservableQueryConnection.js.map +1 -1
  53. package/dist/esm/queries/WebSocketMessage.d.ts +11 -0
  54. package/dist/esm/queries/WebSocketMessage.d.ts.map +1 -0
  55. package/dist/esm/queries/WebSocketMessage.js +9 -0
  56. package/dist/esm/queries/WebSocketMessage.js.map +1 -0
  57. package/dist/esm/queries/index.d.ts +1 -0
  58. package/dist/esm/queries/index.d.ts.map +1 -1
  59. package/dist/esm/queries/index.js +1 -0
  60. package/dist/esm/queries/index.js.map +1 -1
  61. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  62. package/helpers/fetchHelper.ts +30 -0
  63. package/identity/IIdentityProvider.ts +4 -1
  64. package/identity/IdentityProvider.ts +14 -9
  65. package/identity/for_IdentityProvider/given/an_identity_provider.ts +4 -5
  66. package/identity/for_IdentityProvider/when_getting_current/with_type_safe_details.ts +43 -0
  67. package/identity/for_IdentityProvider/when_refreshing/with_type_safe_details.ts +42 -0
  68. package/package.json +1 -1
  69. package/queries/IObservableQueryConnection.ts +10 -0
  70. package/queries/NullObservableQueryConnection.ts +10 -0
  71. package/queries/ObservableQueryConnection.ts +93 -2
  72. package/queries/WebSocketMessage.ts +44 -0
  73. package/queries/for_ObservableQueryConnection/given/an_observable_query_connection.ts +27 -0
  74. package/queries/for_ObservableQueryConnection/when_constructing.ts +15 -0
  75. package/queries/for_ObservableQueryFor/when_performing/with_enumerable_query.ts +5 -2
  76. package/queries/for_ObservableQueryFor/when_performing/with_paging.ts +5 -2
  77. package/queries/for_ObservableQueryFor/when_performing/with_parameter_descriptor_values.ts +5 -2
  78. package/queries/for_ObservableQueryFor/when_performing/with_partial_parameter_descriptor_values.ts +5 -2
  79. package/queries/for_ObservableQueryFor/when_performing/with_route_parameters_and_unused_parameters.ts +5 -2
  80. package/queries/for_ObservableQueryFor/when_performing/with_sorting.ts +5 -2
  81. package/queries/for_ObservableQueryFor/when_performing/with_valid_arguments.ts +7 -2
  82. package/queries/for_QueryFor/when_performing/with_abort_controller.ts +5 -2
  83. package/queries/for_QueryFor/when_performing/with_enumerable_query.ts +5 -2
  84. package/queries/for_QueryFor/when_performing/with_fetch_error.ts +5 -2
  85. package/queries/for_QueryFor/when_performing/with_json_parse_error.ts +6 -3
  86. package/queries/for_QueryFor/when_performing/with_paging.ts +5 -2
  87. package/queries/for_QueryFor/when_performing/with_parameter_descriptor_values.ts +5 -2
  88. package/queries/for_QueryFor/when_performing/with_partial_parameter_descriptor_values.ts +5 -2
  89. package/queries/for_QueryFor/when_performing/with_query_without_required_parameters.ts +5 -2
  90. package/queries/for_QueryFor/when_performing/with_route_parameters_and_unused_parameters.ts +5 -2
  91. package/queries/for_QueryFor/when_performing/with_sorting.ts +7 -3
  92. package/queries/for_QueryFor/when_performing/with_valid_arguments.ts +6 -3
  93. package/queries/for_WebSocketMessage/when_creating_messages.ts +67 -0
  94. package/queries/index.ts +2 -1
@@ -0,0 +1,43 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Guid } from '@cratis/fundamentals';
5
+ import { IdentityProvider } from '../../IdentityProvider';
6
+ import { an_identity_provider } from '../given/an_identity_provider';
7
+ import { given } from '../../../given';
8
+
9
+ class TestDetails {
10
+ userId: Guid = Guid.empty;
11
+ role: string = '';
12
+ }
13
+
14
+ describe('when getting current with type safe details', given(an_identity_provider, () => {
15
+ let result: { id: string; name: string; details: TestDetails };
16
+
17
+ beforeEach(async () => {
18
+ const testGuid = Guid.create();
19
+
20
+ // Mock document.cookie with base64-encoded identity data
21
+ const identityData = {
22
+ id: 'test-user-id',
23
+ name: 'Test User',
24
+ details: {
25
+ userId: testGuid.toString(),
26
+ role: 'admin'
27
+ }
28
+ };
29
+ const encodedData = btoa(JSON.stringify(identityData));
30
+ (global as { document?: { cookie: string } }).document!.cookie = `.cratis-identity=${encodedData}`;
31
+
32
+ const identity = await IdentityProvider.getCurrent(TestDetails);
33
+ result = {
34
+ id: identity.id,
35
+ name: identity.name,
36
+ details: identity.details
37
+ };
38
+ });
39
+
40
+ it('should deserialize details with proper types', () => {
41
+ result.details.userId.should.be.instanceOf(Guid);
42
+ });
43
+ }));
@@ -0,0 +1,42 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Guid } from '@cratis/fundamentals';
5
+ import { IdentityProvider } from '../../IdentityProvider';
6
+ import { an_identity_provider } from '../given/an_identity_provider';
7
+ import { given } from '../../../given';
8
+
9
+ class TestDetails {
10
+ userId: Guid = Guid.empty;
11
+ role: string = '';
12
+ }
13
+
14
+ describe('when refreshing with type safe details', given(an_identity_provider, context => {
15
+ let result: { id: string; name: string; details: TestDetails };
16
+
17
+ beforeEach(async () => {
18
+ const testGuid = Guid.create();
19
+ context.fetchStub.resolves({
20
+ ok: true,
21
+ json: async () => ({
22
+ id: 'test-user-id',
23
+ name: 'Test User',
24
+ details: {
25
+ userId: testGuid.toString(),
26
+ role: 'admin'
27
+ }
28
+ })
29
+ } as Response);
30
+
31
+ const identity = await IdentityProvider.refresh(TestDetails);
32
+ result = {
33
+ id: identity.id,
34
+ name: identity.name,
35
+ details: identity.details
36
+ };
37
+ });
38
+
39
+ it('should deserialize details with proper types', () => {
40
+ result.details.userId.should.be.instanceOf(Guid);
41
+ });
42
+ }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/arc",
3
- "version": "18.4.3",
3
+ "version": "18.5.0",
4
4
  "description": "",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
@@ -7,6 +7,16 @@ import { DataReceived } from './ObservableQueryConnection';
7
7
  * Defines a connection for observable queries.
8
8
  */
9
9
  export interface IObservableQueryConnection<TDataType> {
10
+ /**
11
+ * Gets the latency of the last ping/pong sequence in milliseconds.
12
+ */
13
+ readonly lastPingLatency: number;
14
+
15
+ /**
16
+ * Gets the average latency since the connection started in milliseconds.
17
+ */
18
+ readonly averageLatency: number;
19
+
10
20
  /**
11
21
  * Connect to a specific route.
12
22
  * @param {DataReceived<TDataType> dataReceived Callback that will receive the data.
@@ -17,6 +17,16 @@ export class NullObservableQueryConnection<TDataType> implements IObservableQuer
17
17
  constructor(readonly defaultValue: TDataType) {
18
18
  }
19
19
 
20
+ /** @inheritdoc */
21
+ get lastPingLatency(): number {
22
+ return 0;
23
+ }
24
+
25
+ /** @inheritdoc */
26
+ get averageLatency(): number {
27
+ return 0;
28
+ }
29
+
20
30
  /** @inheritdoc */
21
31
  connect(dataReceived: DataReceived<TDataType>) {
22
32
  dataReceived(QueryResult.empty(this.defaultValue));
@@ -4,6 +4,7 @@
4
4
  import { Globals } from '../Globals';
5
5
  import { IObservableQueryConnection } from './IObservableQueryConnection';
6
6
  import { QueryResult } from './QueryResult';
7
+ import { WebSocketMessage, WebSocketMessageType } from './WebSocketMessage';
7
8
 
8
9
  export type DataReceived<TDataType> = (data: QueryResult<TDataType>) => void;
9
10
 
@@ -15,12 +16,21 @@ export class ObservableQueryConnection<TDataType> implements IObservableQueryCon
15
16
  private _socket!: WebSocket;
16
17
  private _disconnected = false;
17
18
  private _url: string;
19
+ private _pingInterval?: ReturnType<typeof setInterval>;
20
+ private _pingIntervalMs: number = 10000;
21
+ private _lastPingSentTime?: number;
22
+ private _lastPongLatency: number = 0;
23
+ private _latencySamples: number[] = [];
24
+ private _connectionStartTime?: number;
18
25
 
19
26
  /**
20
27
  * Initializes a new instance of the {@link ObservableQueryConnection<TDataType>} class.
21
28
  * @param {Url} url The fully qualified Url.
29
+ * @param {string} _microservice The microservice name.
30
+ * @param {number} pingIntervalMs The ping interval in milliseconds (default: 10000).
22
31
  */
23
- constructor(url: URL, private readonly _microservice: string) {
32
+ constructor(url: URL, private readonly _microservice: string, pingIntervalMs: number = 10000) {
33
+ this._pingIntervalMs = pingIntervalMs;
24
34
  const secure = url.protocol?.indexOf('https') === 0 || false;
25
35
 
26
36
  this._url = `${secure ? 'wss' : 'ws'}://${url.host}${url.pathname}${url.search}`;
@@ -41,6 +51,24 @@ export class ObservableQueryConnection<TDataType> implements IObservableQueryCon
41
51
  this.disconnect();
42
52
  }
43
53
 
54
+ /**
55
+ * Gets the latency of the last ping/pong sequence in milliseconds.
56
+ */
57
+ get lastPingLatency(): number {
58
+ return this._lastPongLatency;
59
+ }
60
+
61
+ /**
62
+ * Gets the average latency since the connection started in milliseconds.
63
+ */
64
+ get averageLatency(): number {
65
+ if (this._latencySamples.length === 0) {
66
+ return 0;
67
+ }
68
+ const sum = this._latencySamples.reduce((acc, val) => acc + val, 0);
69
+ return sum / this._latencySamples.length;
70
+ }
71
+
44
72
  /** @inheritdoc */
45
73
  connect(dataReceived: DataReceived<TDataType>, queryArguments?: object) {
46
74
  let url = this._url;
@@ -80,15 +108,19 @@ export class ObservableQueryConnection<TDataType> implements IObservableQueryCon
80
108
  console.log(`Connection for '${url}' established`);
81
109
  timeToWait = 500;
82
110
  currentAttempt = 0;
111
+ this._connectionStartTime = Date.now();
112
+ this.startPinging();
83
113
  };
84
114
  this._socket.onclose = () => {
85
115
  if (this._disconnected) return;
86
116
  console.log(`Unexpected connection closed for route '${url}'`);
117
+ this.stopPinging();
87
118
  retry();
88
119
  };
89
120
  this._socket.onerror = (error) => {
90
121
  if (this._disconnected) return;
91
122
  console.log(`Error with connection for '${url}' - ${error}`);
123
+ this.stopPinging();
92
124
  retry();
93
125
  };
94
126
  this._socket.onmessage = (ev) => {
@@ -96,7 +128,7 @@ export class ObservableQueryConnection<TDataType> implements IObservableQueryCon
96
128
  console.log('Received message after closing connection');
97
129
  return;
98
130
  }
99
- dataReceived(JSON.parse(ev.data));
131
+ this.handleMessage(ev.data, dataReceived);
100
132
  };
101
133
  };
102
134
 
@@ -111,8 +143,67 @@ export class ObservableQueryConnection<TDataType> implements IObservableQueryCon
111
143
  }
112
144
  console.log(`Disconnecting '${this._url}'`);
113
145
  this._disconnected = true;
146
+ this.stopPinging();
114
147
  this._socket?.close();
115
148
  console.log(`Connection for '${this._url}' closed`);
116
149
  this._socket = undefined!;
117
150
  }
151
+
152
+ private startPinging() {
153
+ this.stopPinging();
154
+ this._pingInterval = setInterval(() => {
155
+ this.sendPing();
156
+ }, this._pingIntervalMs);
157
+ }
158
+
159
+ private stopPinging() {
160
+ if (this._pingInterval) {
161
+ clearInterval(this._pingInterval);
162
+ this._pingInterval = undefined;
163
+ }
164
+ }
165
+
166
+ private sendPing() {
167
+ if (this._disconnected || !this._socket || this._socket.readyState !== WebSocket.OPEN) {
168
+ return;
169
+ }
170
+
171
+ this._lastPingSentTime = Date.now();
172
+ const pingMessage: WebSocketMessage = {
173
+ type: WebSocketMessageType.Ping,
174
+ timestamp: this._lastPingSentTime
175
+ };
176
+
177
+ this._socket.send(JSON.stringify(pingMessage));
178
+ }
179
+
180
+ private handleMessage(rawData: string, dataReceived: DataReceived<TDataType>) {
181
+ try {
182
+ const message = JSON.parse(rawData) as WebSocketMessage;
183
+
184
+ // Handle messages based on type
185
+ if (message.type === WebSocketMessageType.Pong) {
186
+ this.handlePong(message);
187
+ } else if (message.type === WebSocketMessageType.Data || !message.type) {
188
+ // For backward compatibility, treat messages without a type as data messages
189
+ const data = message.data ?? message;
190
+ dataReceived(data as QueryResult<TDataType>);
191
+ }
192
+ } catch (error) {
193
+ console.error('Error parsing WebSocket message:', error);
194
+ }
195
+ }
196
+
197
+ private handlePong(message: WebSocketMessage) {
198
+ if (message.timestamp && this._lastPingSentTime) {
199
+ const latency = Date.now() - message.timestamp;
200
+ this._lastPongLatency = latency;
201
+ this._latencySamples.push(latency);
202
+
203
+ // Keep only the last 100 samples for average calculation
204
+ if (this._latencySamples.length > 100) {
205
+ this._latencySamples.shift();
206
+ }
207
+ }
208
+ }
118
209
  }
@@ -0,0 +1,44 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /**
5
+ * Represents the type of WebSocket message.
6
+ */
7
+ export enum WebSocketMessageType {
8
+ /**
9
+ * A data message containing query results.
10
+ */
11
+ Data = 'Data',
12
+
13
+ /**
14
+ * A ping message sent from client to server.
15
+ */
16
+ Ping = 'Ping',
17
+
18
+ /**
19
+ * A pong message sent from server to client in response to a ping.
20
+ */
21
+ Pong = 'Pong'
22
+ }
23
+
24
+ /**
25
+ * Represents a WebSocket message envelope.
26
+ */
27
+ export type WebSocketMessage = {
28
+ /**
29
+ * The type of message.
30
+ */
31
+ type: WebSocketMessageType;
32
+
33
+ /**
34
+ * The timestamp when the message was sent (for ping/pong latency tracking).
35
+ */
36
+ timestamp?: number;
37
+
38
+ /**
39
+ * The payload data (for data messages).
40
+ */
41
+ /* eslint-disable @typescript-eslint/no-explicit-any */
42
+ data?: any;
43
+ /* eslint-enable @typescript-eslint/no-explicit-any */
44
+ };
@@ -0,0 +1,27 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { ObservableQueryConnection } from '../../ObservableQueryConnection';
5
+ import * as sinon from 'sinon';
6
+
7
+ export class an_observable_query_connection {
8
+ connection: ObservableQueryConnection<string>;
9
+ url: URL;
10
+ microservice: string;
11
+ mockWebSocket: sinon.SinonStubbedInstance<WebSocket>;
12
+
13
+ constructor() {
14
+ this.url = new URL('https://example.com/api/test');
15
+ this.microservice = 'test-microservice';
16
+
17
+ // Stub the WebSocket constructor
18
+ this.mockWebSocket = sinon.createStubInstance(WebSocket);
19
+ Object.defineProperty(this.mockWebSocket, 'readyState', {
20
+ value: WebSocket.OPEN,
21
+ writable: false
22
+ });
23
+
24
+ // Create connection with a short ping interval for testing (100ms)
25
+ this.connection = new ObservableQueryConnection<string>(this.url, this.microservice, 100);
26
+ }
27
+ }
@@ -0,0 +1,15 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { an_observable_query_connection } from './given/an_observable_query_connection';
5
+ import { given } from '../../given';
6
+
7
+ describe('when constructing', given(an_observable_query_connection, context => {
8
+ it('should have zero last ping latency', () => {
9
+ context.connection.lastPingLatency.should.equal(0);
10
+ });
11
+
12
+ it('should have zero average latency', () => {
13
+ context.connection.averageLatency.should.equal(0);
14
+ });
15
+ }));
@@ -4,11 +4,13 @@
4
4
  import { an_observable_query_for } from '../given/an_observable_query_for';
5
5
  import { given } from '../../../given';
6
6
  import * as sinon from 'sinon';
7
+ import { createFetchHelper } from '../../../helpers/fetchHelper';
7
8
  import { QueryResult } from '../../QueryResult';
8
9
 
9
10
  describe('when performing with enumerable query', given(an_observable_query_for, context => {
10
11
  let result: QueryResult<string[]>;
11
12
  let fetchStub: sinon.SinonStub;
13
+ let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
12
14
  const mockResponse = {
13
15
  data: ['item1', 'item2', 'item3'],
14
16
  isSuccess: true,
@@ -27,7 +29,8 @@ describe('when performing with enumerable query', given(an_observable_query_for,
27
29
  };
28
30
 
29
31
  beforeEach(async () => {
30
- fetchStub = sinon.stub(global, 'fetch');
32
+ fetchHelper = createFetchHelper();
33
+ fetchStub = fetchHelper.stubFetch();
31
34
  fetchStub.resolves({
32
35
  json: sinon.stub().resolves(mockResponse),
33
36
  ok: true,
@@ -41,7 +44,7 @@ describe('when performing with enumerable query', given(an_observable_query_for,
41
44
  });
42
45
 
43
46
  afterEach(() => {
44
- fetchStub.restore();
47
+ fetchHelper.restore();
45
48
  });
46
49
 
47
50
  it('should return successful result', () => {
@@ -4,10 +4,12 @@
4
4
  import { an_observable_query_for } from '../given/an_observable_query_for';
5
5
  import { given } from '../../../given';
6
6
  import * as sinon from 'sinon';
7
+ import { createFetchHelper } from '../../../helpers/fetchHelper';
7
8
  import { Paging } from '../../Paging';
8
9
 
9
10
  describe('when performing with paging', given(an_observable_query_for, context => {
10
11
  let fetchStub: sinon.SinonStub;
12
+ let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
11
13
  const mockResponse = {
12
14
  data: 'test-result',
13
15
  isSuccess: true,
@@ -30,7 +32,8 @@ describe('when performing with paging', given(an_observable_query_for, context =
30
32
  if ((globalThis.fetch as sinon.SinonStub)?.restore) {
31
33
  (globalThis.fetch as sinon.SinonStub).restore();
32
34
  }
33
- fetchStub = sinon.stub(global, 'fetch');
35
+ fetchHelper = createFetchHelper();
36
+ fetchStub = fetchHelper.stubFetch();
34
37
  fetchStub.resolves({
35
38
  json: sinon.stub().resolves(mockResponse),
36
39
  ok: true,
@@ -44,7 +47,7 @@ describe('when performing with paging', given(an_observable_query_for, context =
44
47
  });
45
48
 
46
49
  afterEach(() => {
47
- fetchStub.restore();
50
+ fetchHelper.restore();
48
51
  });
49
52
 
50
53
  it('should include page parameter in URL', () => {
@@ -4,11 +4,13 @@
4
4
  import { an_observable_query_for } from '../given/an_observable_query_for';
5
5
  import { given } from '../../../given';
6
6
  import * as sinon from 'sinon';
7
+ import { createFetchHelper } from '../../../helpers/fetchHelper';
7
8
  import { QueryResult } from '../../QueryResult';
8
9
 
9
10
  describe('with parameter descriptor values', given(an_observable_query_for, context => {
10
11
  let result: QueryResult<string>;
11
12
  let fetchStub: sinon.SinonStub;
13
+ let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
12
14
  const mockResponse = {
13
15
  data: 'test-result',
14
16
  isSuccess: true,
@@ -27,7 +29,8 @@ describe('with parameter descriptor values', given(an_observable_query_for, cont
27
29
  };
28
30
 
29
31
  beforeEach(async () => {
30
- fetchStub = sinon.stub(global, 'fetch');
32
+ fetchHelper = createFetchHelper();
33
+ fetchStub = fetchHelper.stubFetch();
31
34
  fetchStub.resolves({
32
35
  json: sinon.stub().resolves(mockResponse),
33
36
  ok: true,
@@ -45,7 +48,7 @@ describe('with parameter descriptor values', given(an_observable_query_for, cont
45
48
  });
46
49
 
47
50
  afterEach(() => {
48
- fetchStub.restore();
51
+ fetchHelper.restore();
49
52
  });
50
53
 
51
54
  it('should return successful result', () => {
@@ -4,11 +4,13 @@
4
4
  import { an_observable_query_for } from '../given/an_observable_query_for';
5
5
  import { given } from '../../../given';
6
6
  import * as sinon from 'sinon';
7
+ import { createFetchHelper } from '../../../helpers/fetchHelper';
7
8
  import { QueryResult } from '../../QueryResult';
8
9
 
9
10
  describe('with partial parameter descriptor values', given(an_observable_query_for, context => {
10
11
  let result: QueryResult<string>;
11
12
  let fetchStub: sinon.SinonStub;
13
+ let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
12
14
  const mockResponse = {
13
15
  data: 'test-result',
14
16
  isSuccess: true,
@@ -27,7 +29,8 @@ describe('with partial parameter descriptor values', given(an_observable_query_f
27
29
  };
28
30
 
29
31
  beforeEach(async () => {
30
- fetchStub = sinon.stub(global, 'fetch');
32
+ fetchHelper = createFetchHelper();
33
+ fetchStub = fetchHelper.stubFetch();
31
34
  fetchStub.resolves({
32
35
  json: sinon.stub().resolves(mockResponse),
33
36
  ok: true,
@@ -45,7 +48,7 @@ describe('with partial parameter descriptor values', given(an_observable_query_f
45
48
  });
46
49
 
47
50
  afterEach(() => {
48
- fetchStub.restore();
51
+ fetchHelper.restore();
49
52
  });
50
53
 
51
54
  it('should return successful result', () => {
@@ -4,12 +4,14 @@
4
4
  import { an_observable_query_for } from '../given/an_observable_query_for';
5
5
  import { given } from '../../../given';
6
6
  import * as sinon from 'sinon';
7
+ import { createFetchHelper } from '../../../helpers/fetchHelper';
7
8
  import { QueryResult } from '../../QueryResult';
8
9
  import { expect } from 'chai';
9
10
 
10
11
  describe('when performing with route parameters and unused parameters', given(an_observable_query_for, context => {
11
12
  let result: QueryResult<string>;
12
13
  let fetchStub: sinon.SinonStub;
14
+ let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
13
15
  const mockResponse = {
14
16
  data: 'test-result',
15
17
  isSuccess: true,
@@ -28,7 +30,8 @@ describe('when performing with route parameters and unused parameters', given(an
28
30
  };
29
31
 
30
32
  beforeEach(async () => {
31
- fetchStub = sinon.stub(global, 'fetch');
33
+ fetchHelper = createFetchHelper();
34
+ fetchStub = fetchHelper.stubFetch();
32
35
  fetchStub.resolves({
33
36
  json: sinon.stub().resolves(mockResponse),
34
37
  ok: true,
@@ -43,7 +46,7 @@ describe('when performing with route parameters and unused parameters', given(an
43
46
  });
44
47
 
45
48
  afterEach(() => {
46
- fetchStub.restore();
49
+ fetchHelper.restore();
47
50
  });
48
51
 
49
52
  it('should return successful result', () => {
@@ -4,11 +4,13 @@
4
4
  import { an_observable_query_for } from '../given/an_observable_query_for';
5
5
  import { given } from '../../../given';
6
6
  import * as sinon from 'sinon';
7
+ import { createFetchHelper } from '../../../helpers/fetchHelper';
7
8
  import { Sorting } from '../../Sorting';
8
9
  import { SortDirection } from '../../SortDirection';
9
10
 
10
11
  describe('when performing with sorting', given(an_observable_query_for, context => {
11
12
  let fetchStub: sinon.SinonStub;
13
+ let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
12
14
  const mockResponse = {
13
15
  data: 'test-result',
14
16
  isSuccess: true,
@@ -27,7 +29,8 @@ describe('when performing with sorting', given(an_observable_query_for, context
27
29
  };
28
30
 
29
31
  beforeEach(async () => {
30
- fetchStub = sinon.stub(global, 'fetch');
32
+ fetchHelper = createFetchHelper();
33
+ fetchStub = fetchHelper.stubFetch();
31
34
  fetchStub.resolves({
32
35
  json: sinon.stub().resolves(mockResponse),
33
36
  ok: true,
@@ -41,7 +44,7 @@ describe('when performing with sorting', given(an_observable_query_for, context
41
44
  });
42
45
 
43
46
  afterEach(() => {
44
- fetchStub.restore();
47
+ fetchHelper.restore();
45
48
  });
46
49
 
47
50
  it('should include sortBy parameter in URL', () => {
@@ -6,9 +6,12 @@ import { given } from '../../../given';
6
6
  import * as sinon from 'sinon';
7
7
  import { QueryResult } from '../../QueryResult';
8
8
 
9
+ import { createFetchHelper } from '../../../helpers/fetchHelper';
10
+
9
11
  describe('when performing with valid arguments', given(an_observable_query_for, context => {
10
12
  let result: QueryResult<string>;
11
13
  let fetchStub: sinon.SinonStub;
14
+ let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
12
15
  const mockResponse = {
13
16
  data: 'test-result',
14
17
  isSuccess: true,
@@ -27,7 +30,9 @@ describe('when performing with valid arguments', given(an_observable_query_for,
27
30
  };
28
31
 
29
32
  beforeEach(async () => {
30
- fetchStub = sinon.stub(global, 'fetch');
33
+ // Setup fetch mock using helper
34
+ fetchHelper = createFetchHelper();
35
+ fetchStub = fetchHelper.stubFetch();
31
36
  fetchStub.resolves({
32
37
  json: sinon.stub().resolves(mockResponse),
33
38
  ok: true,
@@ -42,7 +47,7 @@ describe('when performing with valid arguments', given(an_observable_query_for,
42
47
  });
43
48
 
44
49
  afterEach(() => {
45
- fetchStub.restore();
50
+ fetchHelper.restore();
46
51
  });
47
52
 
48
53
  it('should return successful result', () => {
@@ -5,12 +5,14 @@ import { a_query_for } from '../given/a_query_for';
5
5
  import { given } from '../../../given';
6
6
 
7
7
  import * as sinon from 'sinon';
8
+ import { createFetchHelper } from '../../../helpers/fetchHelper';
8
9
  import { QueryResult } from '../../QueryResult';
9
10
 
10
11
  describe('with abort controller', given(a_query_for, context => {
11
12
  let result1: QueryResult<string>;
12
13
  let result2: QueryResult<string>;
13
14
  let fetchStub: sinon.SinonStub;
15
+ let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
14
16
  let firstAbortController: AbortController;
15
17
  const mockResponse = {
16
18
  data: 'test-result',
@@ -31,7 +33,8 @@ describe('with abort controller', given(a_query_for, context => {
31
33
 
32
34
  beforeEach(async () => {
33
35
  // Setup fetch mock
34
- fetchStub = sinon.stub(global, 'fetch');
36
+ fetchHelper = createFetchHelper();
37
+ fetchStub = fetchHelper.stubFetch();
35
38
  fetchStub.resolves({
36
39
  json: sinon.stub().resolves(mockResponse),
37
40
  ok: true,
@@ -49,7 +52,7 @@ describe('with abort controller', given(a_query_for, context => {
49
52
  });
50
53
 
51
54
  afterEach(() => {
52
- fetchStub.restore();
55
+ fetchHelper.restore();
53
56
  });
54
57
 
55
58
  it('should create new abort controller on second call', () => {
@@ -5,11 +5,13 @@ import { a_query_for } from '../given/a_query_for';
5
5
  import { given } from '../../../given';
6
6
 
7
7
  import * as sinon from 'sinon';
8
+ import { createFetchHelper } from '../../../helpers/fetchHelper';
8
9
  import { QueryResult } from '../../QueryResult';
9
10
 
10
11
  describe('with enumerable query', given(a_query_for, context => {
11
12
  let result: QueryResult<string[]>;
12
13
  let fetchStub: sinon.SinonStub;
14
+ let fetchHelper: { stubFetch: () => sinon.SinonStub; restore: () => void };
13
15
  const mockResponse = {
14
16
  data: ['item1', 'item2'],
15
17
  isSuccess: true,
@@ -29,7 +31,8 @@ describe('with enumerable query', given(a_query_for, context => {
29
31
 
30
32
  beforeEach(async () => {
31
33
  // Setup fetch mock
32
- fetchStub = sinon.stub(global, 'fetch');
34
+ fetchHelper = createFetchHelper();
35
+ fetchStub = fetchHelper.stubFetch();
33
36
  fetchStub.resolves({
34
37
  json: sinon.stub().resolves(mockResponse),
35
38
  ok: true,
@@ -43,7 +46,7 @@ describe('with enumerable query', given(a_query_for, context => {
43
46
  });
44
47
 
45
48
  afterEach(() => {
46
- fetchStub.restore();
49
+ fetchHelper.restore();
47
50
  });
48
51
 
49
52
  it('should return successful result', () => {