@matterbridge/test-utils 3.10.7-dev-20260823-f911eb8

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 (57) hide show
  1. package/dist/buntest/export.d.ts +25 -0
  2. package/dist/buntest/export.d.ts.map +1 -0
  3. package/dist/buntest/export.js +26 -0
  4. package/dist/buntest/export.js.map +1 -0
  5. package/dist/buntest/matterTest.d.ts +351 -0
  6. package/dist/buntest/matterTest.d.ts.map +1 -0
  7. package/dist/buntest/matterTest.js +722 -0
  8. package/dist/buntest/matterTest.js.map +1 -0
  9. package/dist/buntest/setupTest.d.ts +84 -0
  10. package/dist/buntest/setupTest.d.ts.map +1 -0
  11. package/dist/buntest/setupTest.js +153 -0
  12. package/dist/buntest/setupTest.js.map +1 -0
  13. package/dist/export.d.ts +25 -0
  14. package/dist/export.d.ts.map +1 -0
  15. package/dist/export.js +25 -0
  16. package/dist/export.js.map +1 -0
  17. package/dist/flushAsync.d.ts +36 -0
  18. package/dist/flushAsync.d.ts.map +1 -0
  19. package/dist/flushAsync.js +43 -0
  20. package/dist/flushAsync.js.map +1 -0
  21. package/dist/jest/export.d.ts +25 -0
  22. package/dist/jest/export.d.ts.map +1 -0
  23. package/dist/jest/export.js +26 -0
  24. package/dist/jest/export.js.map +1 -0
  25. package/dist/jest/matterTest.d.ts +351 -0
  26. package/dist/jest/matterTest.d.ts.map +1 -0
  27. package/dist/jest/matterTest.js +722 -0
  28. package/dist/jest/matterTest.js.map +1 -0
  29. package/dist/jest/setupTest.d.ts +83 -0
  30. package/dist/jest/setupTest.d.ts.map +1 -0
  31. package/dist/jest/setupTest.js +154 -0
  32. package/dist/jest/setupTest.js.map +1 -0
  33. package/dist/logKeepAlives.d.ts +32 -0
  34. package/dist/logKeepAlives.d.ts.map +1 -0
  35. package/dist/logKeepAlives.js +69 -0
  36. package/dist/logKeepAlives.js.map +1 -0
  37. package/dist/matter/export.d.ts +24 -0
  38. package/dist/matter/export.d.ts.map +1 -0
  39. package/dist/matter/export.js +25 -0
  40. package/dist/matter/export.js.map +1 -0
  41. package/dist/matter/matterRequest.d.ts +257 -0
  42. package/dist/matter/matterRequest.d.ts.map +1 -0
  43. package/dist/matter/matterRequest.js +455 -0
  44. package/dist/matter/matterRequest.js.map +1 -0
  45. package/dist/vitest/export.d.ts +25 -0
  46. package/dist/vitest/export.d.ts.map +1 -0
  47. package/dist/vitest/export.js +26 -0
  48. package/dist/vitest/export.js.map +1 -0
  49. package/dist/vitest/matterTest.d.ts +351 -0
  50. package/dist/vitest/matterTest.d.ts.map +1 -0
  51. package/dist/vitest/matterTest.js +723 -0
  52. package/dist/vitest/matterTest.js.map +1 -0
  53. package/dist/vitest/setupTest.d.ts +62 -0
  54. package/dist/vitest/setupTest.d.ts.map +1 -0
  55. package/dist/vitest/setupTest.js +129 -0
  56. package/dist/vitest/setupTest.js.map +1 -0
  57. package/package.json +97 -0
@@ -0,0 +1,722 @@
1
+ /**
2
+ * @file packages/jest-utils/src/jestMatterTest.ts
3
+ * @description This file contains the Jest Matter Test Environment.
4
+ * @author Luca Liguori
5
+ * @created 2026-04-19
6
+ * @version 1.0.0
7
+ * @license Apache-2.0
8
+ *
9
+ * Copyright 2025, 2026, 2027 Luca Liguori.
10
+ *
11
+ * Licensed under the Apache License, Version 2.0 (the "License");
12
+ * you may not use this file except in compliance with the License.
13
+ * You may obtain a copy of the License at
14
+ *
15
+ * http://www.apache.org/licenses/LICENSE-2.0
16
+ *
17
+ * Unless required by applicable law or agreed to in writing, software
18
+ * distributed under the License is distributed on an "AS IS" BASIS,
19
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ * See the License for the specific language governing permissions and
21
+ * limitations under the License.
22
+ */
23
+ // oxlint-disable-next-line import/no-unassigned-import
24
+ import '@matter/nodejs'; // Set up Node.js environment for matter.js
25
+ import path from 'node:path';
26
+ // @matter
27
+ import { Environment, Lifecycle, LogFormat as MatterLogFormat, LogLevel as MatterLogLevel } from '@matter/general';
28
+ import { Endpoint, ServerNode, ServerNodeStore } from '@matter/node';
29
+ import { BridgedDeviceBasicInformationServer } from '@matter/node/behaviors/bridged-device-basic-information';
30
+ import { DescriptorServer } from '@matter/node/behaviors/descriptor';
31
+ import { OnOffServer } from '@matter/node/behaviors/on-off';
32
+ import { MountedOnOffControlDevice } from '@matter/node/devices/mounted-on-off-control';
33
+ import { AggregatorEndpoint } from '@matter/node/endpoints';
34
+ import { DeviceTypeId, VendorId } from '@matter/types/datatype';
35
+ // @matterbridge
36
+ import { MATTER_STORAGE_DIR } from '@matterbridge/types';
37
+ import { inspectError } from '@matterbridge/utils/error';
38
+ // node-ansi-logger module
39
+ import { er, rs } from 'node-ansi-logger';
40
+ // local modules
41
+ import { flushAsync } from '../flushAsync.js';
42
+ import { HOMEDIR, log, NAME } from './setupTest.js';
43
+ export let environment;
44
+ export let server;
45
+ export let aggregator;
46
+ const noop = () => undefined;
47
+ /**
48
+ * Create a matter test environment for testing:
49
+ * - it will remove any existing home directory
50
+ * - setup the matter environment with name, debug logging and ANSI format
51
+ *
52
+ * @returns {Environment} - The default matter environment.
53
+ * @example
54
+ * ```typescript
55
+ * const NAME = 'BaseTest';
56
+ * const MATTER_PORT = 8000;
57
+ * const MATTER_CREATE_ONLY = true;
58
+ *
59
+ * // Setup the test environment
60
+ * await setupTest(NAME, false);
61
+ *
62
+ * // Setup the Matter test environment
63
+ * await createTestEnvironment();
64
+ *
65
+ * // Create the server node and aggregator
66
+ * await createServerNode(MATTER_PORT);
67
+ *
68
+ * // Start the server node if not in create-only mode
69
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
70
+ *
71
+ * // Stop or flush the server node depending on the create-only mode
72
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
73
+ * else await stopServerNode();
74
+ *
75
+ * // Destroy the Matter test environment
76
+ * await destroyTestEnvironment();
77
+ * ```
78
+ */
79
+ // oxlint-disable-next-line typescript/require-await
80
+ export async function createTestEnvironment() {
81
+ expect(NAME).toBeDefined();
82
+ expect(typeof NAME).toBe('string');
83
+ expect(NAME.length).toBeGreaterThanOrEqual(4); // avoid accidental deletion of short paths like "/" or "C:\"
84
+ // Setup the matter environment
85
+ environment = Environment.default;
86
+ environment.vars.set('log.level', MatterLogLevel.DEBUG);
87
+ environment.vars.set('log.format', MatterLogFormat.ANSI);
88
+ environment.vars.set('path.root', path.join(HOMEDIR, '.matterbridge', MATTER_STORAGE_DIR));
89
+ environment.vars.set('runtime.signals', false);
90
+ environment.vars.set('runtime.exitcode', false);
91
+ return environment;
92
+ }
93
+ /**
94
+ * Destroy the matter test environment
95
+ *
96
+ * @returns {Promise<void>} A promise that resolves when the test environment is destroyed.
97
+ * @example
98
+ * ```typescript
99
+ * const NAME = 'BaseTest';
100
+ * const MATTER_PORT = 8000;
101
+ * const MATTER_CREATE_ONLY = true;
102
+ *
103
+ * // Setup the test environment
104
+ * await setupTest(NAME, false);
105
+ *
106
+ * // Setup the Matter test environment
107
+ * await createTestEnvironment();
108
+ *
109
+ * // Create the server node and aggregator
110
+ * await createServerNode(MATTER_PORT);
111
+ *
112
+ * // Start the server node if not in create-only mode
113
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
114
+ *
115
+ * // Stop or flush the server node depending on the create-only mode
116
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
117
+ * else await stopServerNode();
118
+ *
119
+ * // Destroy the Matter test environment
120
+ * await destroyTestEnvironment();
121
+ * ```
122
+ */
123
+ export async function destroyTestEnvironment() {
124
+ // Nothing to clean up right now
125
+ }
126
+ /**
127
+ * Get a mocked PlatformMatterbridge.
128
+ *
129
+ * @returns {PlatformMatterbridge} An object representing the mocked PlatformMatterbridge.
130
+ */
131
+ export function getMatterbridge() {
132
+ const matterbridge = {
133
+ systemInformation: {
134
+ interfaceName: 'eth0',
135
+ macAddress: 'aa:bb:cc:dd:ee:ff',
136
+ ipv4Address: '192.168.68.100',
137
+ ipv6Address: 'fd78:cbf8:4939:46e2:51b2:2163:7f88:c33d',
138
+ nodeVersion: '24.16.0',
139
+ hostname: 'matterbridge',
140
+ user: 'jest',
141
+ osType: 'Linux',
142
+ osRelease: '6.8.0-xxxx',
143
+ osPlatform: 'linux',
144
+ osArch: 'arm64',
145
+ totalMemory: '0 B',
146
+ freeMemory: '0 B',
147
+ systemUptime: '0s',
148
+ processUptime: '0s',
149
+ cpuUsage: '0%',
150
+ processCpuUsage: '0%',
151
+ rss: '0 B',
152
+ heapTotal: '0 B',
153
+ heapUsed: '0 B',
154
+ },
155
+ uuid: '00000000-0000-0000-0000-000000000000',
156
+ rootDirectory: HOMEDIR,
157
+ homeDirectory: HOMEDIR,
158
+ matterbridgeDirectory: path.join(HOMEDIR, '.matterbridge'),
159
+ matterbridgePluginDirectory: path.join(HOMEDIR, 'Matterbridge'),
160
+ matterbridgeCertDirectory: path.join(HOMEDIR, '.mattercert'),
161
+ globalModulesDirectory: path.join(HOMEDIR, 'node_modules'),
162
+ matterbridgeVersion: '3.10.7',
163
+ matterbridgeLatestVersion: '3.10.7',
164
+ matterbridgeDevVersion: '3.10.7',
165
+ frontendVersion: '3.5.9',
166
+ bridgeMode: 'none',
167
+ restartMode: 'none',
168
+ virtualMode: 'mounted_switch',
169
+ aggregatorVendorId: VendorId(0xfff1),
170
+ aggregatorVendorName: 'Matterbridge',
171
+ aggregatorProductId: 0x8000,
172
+ aggregatorProductName: 'Matterbridge Jest',
173
+ };
174
+ return matterbridge;
175
+ }
176
+ /**
177
+ * Add a bridged endpoint
178
+ *
179
+ * @param {string} pluginName The name of the plugin.
180
+ * @param {Endpoint} device The device to add.
181
+ * @returns {Promise<boolean>} A promise that resolves to true if the endpoint was added successfully, or rejects with an error if the operation failed.
182
+ */
183
+ export const addBridgedEndpoint = async (pluginName, device) => {
184
+ try {
185
+ await aggregator.add(device);
186
+ return Promise.resolve(true);
187
+ }
188
+ catch (error) {
189
+ return Promise.reject(error);
190
+ }
191
+ };
192
+ /**
193
+ * Remove a bridged endpoint
194
+ *
195
+ * @param {string} pluginName The name of the plugin.
196
+ * @param {Endpoint} device The device to remove.
197
+ * @returns {Promise<boolean>} A promise that resolves to true if the endpoint was removed successfully, or rejects with an error if the operation failed.
198
+ */
199
+ export const removeBridgedEndpoint = async (pluginName, device) => {
200
+ try {
201
+ await device.delete();
202
+ return Promise.resolve(true);
203
+ }
204
+ catch (error) {
205
+ return Promise.reject(error);
206
+ }
207
+ };
208
+ /**
209
+ * Remove all bridged endpoints
210
+ *
211
+ * @param {string} _pluginName The name of the plugin.
212
+ * @param {number} _delay The delay before removing all endpoints.
213
+ * @returns {Promise<boolean>} A promise that resolves to true if all endpoints were removed successfully, or rejects with an error if the operation failed.
214
+ */
215
+ export const removeAllBridgedEndpoints = async (_pluginName, _delay = 0) => {
216
+ try {
217
+ for (const device of aggregator.parts) {
218
+ await device.delete();
219
+ }
220
+ return Promise.resolve(true);
221
+ }
222
+ catch (error) {
223
+ return Promise.reject(error);
224
+ }
225
+ };
226
+ /**
227
+ * Add a virtual endpoint
228
+ *
229
+ * @param {string} pluginName The name of the plugin.
230
+ * @param {string} name The name of the virtual endpoint.
231
+ * @param {'light' | 'outlet' | 'switch' | 'mounted_switch'} type The type of the virtual endpoint.
232
+ * @param {() => Promise<void>} callback The callback to execute when the virtual endpoint is triggered.
233
+ * @returns {Promise<boolean>} A promise that resolves to true if the virtual endpoint was added successfully, or rejects with an error if the operation failed.
234
+ */
235
+ export const addVirtualEndpoint = async (pluginName, name, type, callback) => {
236
+ try {
237
+ const device = new Endpoint(MountedOnOffControlDevice.with(BridgedDeviceBasicInformationServer), {
238
+ id: name.replaceAll(' ', '') + ':' + type,
239
+ bridgedDeviceBasicInformation: {
240
+ vendorId: VendorId(0xfff1),
241
+ vendorName: 'Matterbridge',
242
+ productName: 'Matterbridge Virtual Device',
243
+ nodeLabel: name.slice(0, 32),
244
+ softwareVersion: 20000,
245
+ softwareVersionString: '2.0.0',
246
+ },
247
+ onOff: { onOff: false },
248
+ });
249
+ // Set up an event listener for when the `onOff` state changes.
250
+ device.events.onOff.onOff$Changed.on((value) => {
251
+ // If the `onOff` state becomes true, turn off the virtual device and execute the callback.
252
+ if (value) {
253
+ void callback().catch(/* v8 ignore next */ noop);
254
+ void device.setStateOf(OnOffServer, { onOff: false }).catch(/* v8 ignore next */ noop);
255
+ }
256
+ });
257
+ // Add the created device to the aggregator.
258
+ await aggregator.add(device);
259
+ // Add the OnOffPlugInUnit to MountedOnOffControlDevice (Matter 1.4.2 specs added this (new case of superset) for legacy controllers to recognize the mounted switch).
260
+ await device.act(async (agent) => {
261
+ const descriptor = await agent.load(DescriptorServer);
262
+ descriptor.addDeviceTypes('OnOffPlugInUnit');
263
+ });
264
+ // Initially set the state of the virtual device's `OnOffServer` to false (off).
265
+ await device.setStateOf(OnOffServer, { onOff: false });
266
+ return Promise.resolve(true);
267
+ }
268
+ catch (error) {
269
+ return Promise.reject(error);
270
+ }
271
+ };
272
+ /**
273
+ * Inject matterbridge platform for testing.
274
+ *
275
+ * @param {object} platform The platform to inject.
276
+ *
277
+ * @example
278
+ * ```typescript
279
+ * platform = new MatterbridgePlatform(matterbridge, log, config);
280
+ * // Inject in the platform the Matterbridge environment
281
+ * addMatterbridge(platform);
282
+ * ```
283
+ */
284
+ export function addMatterbridge(platform) {
285
+ expect(platform).toBeDefined();
286
+ // Setup the platform MatterNode helpers
287
+ // @ts-expect-error - setMatterNode is intentionally private
288
+ platform.setMatterNode(addBridgedEndpoint, removeBridgedEndpoint, removeAllBridgedEndpoints, addVirtualEndpoint);
289
+ }
290
+ /**
291
+ * Flush (await) the lazy endpoint number persistence mechanism used by matter.js.
292
+ *
293
+ * Background:
294
+ * assignNumber() batches persistence (store.saveNumber + updating __nextNumber__) via an internal promise (#numbersPersisted).
295
+ * Calling endpointStores.close() waits for the current batch only. If new endpoints were added in the same macrotask
296
+ * cycle additional micro/macro turns might be needed to ensure the batch started. We defensively yield macrotasks
297
+ * (setImmediate) and then await close() multiple rounds.
298
+ *
299
+ * @param {ServerNode} targetServer The server whose endpoint numbering persistence should be flushed.
300
+ * @param {number} rounds Number of macrotask + close cycles to run (3 is usually sufficient).
301
+ * @param {number} ticks Number of macrotask (setImmediate) turns to yield after stopping the server to allow asynchronous work to complete before returning (default 1).
302
+ * @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
303
+ * @param {number} pause Duration in ms to wait between cycles (default 10ms) to allow any follow-up work scheduled by close() to run.
304
+ * @returns {Promise<void>} Resolves when pending number persistence batches have completed.
305
+ */
306
+ export async function flushAllEndpointNumberPersistence(targetServer, rounds = 3, ticks = 1, microTurns = 1, pause = 10) {
307
+ const nodeStore = targetServer.env.get(ServerNodeStore);
308
+ for (let i = 0; i < rounds; i++) {
309
+ await flushAsync(ticks, microTurns, pause);
310
+ await nodeStore.endpointStores.close();
311
+ }
312
+ }
313
+ /**
314
+ * Get the root ServerNode for a given Endpoint by traversing up the owner chain until the root endpoint is reached.
315
+ *
316
+ * @param {Endpoint} endpoint The endpoint to find the root server for.
317
+ * @returns {ServerNode<ServerNode.RootEndpoint>} The root ServerNode of the given endpoint.
318
+ */
319
+ function getRootServerNode(endpoint) {
320
+ let current = endpoint;
321
+ while (current.owner) {
322
+ current = current.owner;
323
+ }
324
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
325
+ return current;
326
+ }
327
+ /**
328
+ * Collect all endpoints in the server endpoint tree (root -> descendants).
329
+ *
330
+ * @param {Endpoint} root Root endpoint (typically the ServerNode root endpoint cast as Endpoint).
331
+ * @returns {Endpoint[]} Flat array including the root and every descendant once.
332
+ */
333
+ function collectAllEndpoints(root) {
334
+ const list = [];
335
+ const walk = (ep) => {
336
+ list.push(ep);
337
+ for (const child of ep.parts) {
338
+ walk(child);
339
+ }
340
+ };
341
+ walk(root);
342
+ return list;
343
+ }
344
+ /**
345
+ * Assert that every endpoint attached to the server has an assigned and (batch-)persisted endpoint number.
346
+ *
347
+ * This waits for any outstanding number persistence batch (endpointStores.close()), then traverses the endpoint
348
+ * graph and asserts:
349
+ * - Root endpoint: number is 0 (allowing undefined to coerce to 0 via nullish coalescing check).
350
+ * - All other endpoints: number > 0.
351
+ *
352
+ * @param {ServerNode} targetServer The server whose endpoint numbers are verified.
353
+ * @returns {Promise<void>} Resolves when assertions complete.
354
+ */
355
+ export async function assertAllEndpointNumbersPersisted(targetServer) {
356
+ const nodeStore = targetServer.env.get(ServerNodeStore);
357
+ // Ensure any pending persistence finished (flush any in-flight batch promise)
358
+ await nodeStore.endpointStores.close();
359
+ const all = collectAllEndpoints(targetServer);
360
+ for (const ep of all) {
361
+ const store = nodeStore.storeForEndpoint(ep);
362
+ if (ep.maybeNumber === 0) {
363
+ expect(store.number ?? 0).toBe(0); // root
364
+ }
365
+ else {
366
+ expect(store.number).toBeGreaterThan(0);
367
+ }
368
+ }
369
+ return all.length;
370
+ }
371
+ /**
372
+ * Close the server node stores to flush any pending endpoint number persistence.
373
+ *
374
+ * @param {ServerNode} targetServer The server whose endpoint stores should be closed.
375
+ * @returns {Promise<void>} Resolves when the stores have been closed.
376
+ */
377
+ export async function closeServerNodeStores(targetServer) {
378
+ const resolvedTargetServer = targetServer ?? server;
379
+ await resolvedTargetServer?.env.get(ServerNodeStore)?.endpointStores.close();
380
+ }
381
+ /**
382
+ * Create a matter server node for testing.
383
+ *
384
+ * @param {number} port TCP port to listen on.
385
+ * @param {DeviceTypeId} deviceType Device type identifier for the server node. Defaults to the bridge device type.
386
+ * @param {number} ticks Number of macrotask (setImmediate) turns to yield after starting the server to allow asynchronous work to complete before returning (default 1).
387
+ * @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
388
+ * @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server start process to run before returning (default 10ms).
389
+ * @returns {Promise<[ServerNode<ServerNode.RootEndpoint>, Endpoint<AggregatorEndpoint>]>} Resolves to an array containing the created ServerNode and its AggregatorNode.
390
+ * @example
391
+ * ```typescript
392
+ * const NAME = 'BaseTest';
393
+ * const MATTER_PORT = 8000;
394
+ * const MATTER_CREATE_ONLY = true;
395
+ *
396
+ * // Setup the test environment
397
+ * await setupTest(NAME, false);
398
+ *
399
+ * // Setup the Matter test environment
400
+ * await createTestEnvironment();
401
+ *
402
+ * // Create the server node and aggregator
403
+ * await createServerNode(MATTER_PORT);
404
+ *
405
+ * // Start the server node if not in create-only mode
406
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
407
+ *
408
+ * // Stop or flush the server node depending on the create-only mode
409
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
410
+ * else await stopServerNode();
411
+ *
412
+ * // Destroy the Matter test environment
413
+ * await destroyTestEnvironment();
414
+ * ```
415
+ */
416
+ export async function createServerNode(port, deviceType = DeviceTypeId(0x000e), ticks = 1, microTurns = 1, pause = 10) {
417
+ const { randomBytes } = await import('node:crypto');
418
+ const random = randomBytes(8).toString('hex');
419
+ // Create the server node
420
+ server = await ServerNode.create({
421
+ id: NAME + 'ServerNode',
422
+ // Provide the environment
423
+ environment,
424
+ // Provide Node announcement settings
425
+ productDescription: {
426
+ name: NAME + 'ServerNode',
427
+ deviceType: DeviceTypeId(deviceType),
428
+ vendorId: VendorId(0xfff1),
429
+ productId: 0x8000,
430
+ },
431
+ // Provide defaults for the BasicInformation cluster on the Root endpoint
432
+ basicInformation: {
433
+ vendorId: VendorId(0xfff1),
434
+ vendorName: 'Matterbridge',
435
+ productId: 0x8000,
436
+ productName: ('Matterbridge ' + NAME).slice(0, 32), // truncate to 32 chars to satisfy BasicInformation cluster constraints
437
+ productLabel: ('Label ' + NAME).slice(0, 64), // truncate to 64 chars to satisfy BasicInformation cluster constraints
438
+ nodeLabel: (NAME + 'ServerNode').slice(0, 32), // truncate to 32 chars to satisfy BasicInformation cluster constraints
439
+ hardwareVersion: 1,
440
+ softwareVersion: 1,
441
+ productUrl: 'https://matterbridge.io',
442
+ configurationVersion: 1,
443
+ reachable: true,
444
+ serialNumber: 'SN' + random,
445
+ uniqueId: 'UI' + random,
446
+ },
447
+ // Provide Network relevant configuration like the port
448
+ network: {
449
+ listeningAddressIpv4: undefined,
450
+ listeningAddressIpv6: undefined,
451
+ port,
452
+ tcp: true,
453
+ transportPreference: 'udp',
454
+ },
455
+ // Provide the certificate for the device
456
+ operationalCredentials: {
457
+ certification: undefined,
458
+ },
459
+ });
460
+ expect(server).toBeDefined();
461
+ expect(server.lifecycle.isReady).toBeTruthy();
462
+ // Create the aggregator node
463
+ aggregator = new Endpoint(AggregatorEndpoint, {
464
+ id: NAME + 'AggregatorNode',
465
+ });
466
+ expect(aggregator).toBeDefined();
467
+ // Add the aggregator to the server
468
+ await server.add(aggregator);
469
+ expect(server.parts.has(aggregator.id)).toBeTruthy();
470
+ expect(server.parts.has(aggregator)).toBeTruthy();
471
+ expect(aggregator.lifecycle.isReady).toBeTruthy();
472
+ expect(server.lifecycle.isOnline).toBeFalsy();
473
+ // Ensure the queue is empty and pause 100ms to allow any pending work to complete before returning the server and aggregator
474
+ await flushAsync(ticks, microTurns, pause);
475
+ return [server, aggregator];
476
+ }
477
+ /**
478
+ * Start a matter server node for testing.
479
+ *
480
+ * @param {number} ticks Number of macrotask (setImmediate) turns to yield after starting the server to allow asynchronous work to complete before returning (default 1).
481
+ * @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
482
+ * @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server start process to run before returning (default 10ms).
483
+ * @returns {Promise<[ServerNode<ServerNode.RootEndpoint>, Endpoint<AggregatorEndpoint>]>} Resolves to an array containing the created ServerNode and its AggregatorNode.
484
+ * @example
485
+ * ```typescript
486
+ * const NAME = 'BaseTest';
487
+ * const MATTER_PORT = 8000;
488
+ * const MATTER_CREATE_ONLY = true;
489
+ *
490
+ * // Setup the test environment
491
+ * await setupTest(NAME, false);
492
+ *
493
+ * // Setup the Matter test environment
494
+ * await createTestEnvironment();
495
+ *
496
+ * // Create the server node and aggregator
497
+ * await createServerNode(MATTER_PORT);
498
+ *
499
+ * // Start the server node if not in create-only mode
500
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
501
+ *
502
+ * // Stop or flush the server node depending on the create-only mode
503
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
504
+ * else await stopServerNode();
505
+ *
506
+ * // Destroy the Matter test environment
507
+ * await destroyTestEnvironment();
508
+ * ```
509
+ */
510
+ export async function startServerNode(ticks = 1, microTurns = 1, pause = 10) {
511
+ // Create the server node
512
+ if (!server || !aggregator) {
513
+ /* v8 ignore next */
514
+ throw new Error('Server node and aggregator must be created before starting the server. Call createServerNode() first.');
515
+ }
516
+ // Wait for the server to be online
517
+ await new Promise((resolve, reject) => {
518
+ server.lifecycle.online.on(() => {
519
+ resolve();
520
+ });
521
+ server.start().catch((err) => reject(err));
522
+ });
523
+ // Check if the server is online
524
+ expect(server.lifecycle.isReady).toBeTruthy();
525
+ expect(server.lifecycle.isOnline).toBeTruthy();
526
+ expect(server.lifecycle.isCommissioned).toBeFalsy();
527
+ expect(server.lifecycle.isPartsReady).toBeTruthy();
528
+ expect(server.lifecycle.hasId).toBeTruthy();
529
+ expect(server.lifecycle.hasNumber).toBeTruthy();
530
+ expect(aggregator.lifecycle.isReady).toBeTruthy();
531
+ expect(aggregator.lifecycle.isInstalled).toBeTruthy();
532
+ expect(aggregator.lifecycle.isPartsReady).toBeTruthy();
533
+ expect(aggregator.lifecycle.hasId).toBeTruthy();
534
+ expect(aggregator.lifecycle.hasNumber).toBeTruthy();
535
+ // Ensure the queue is empty
536
+ await flushAsync(ticks, microTurns, pause);
537
+ return [server, aggregator];
538
+ }
539
+ /**
540
+ * Stop a matter server node.
541
+ *
542
+ * @param {number} ticks Number of macrotask (setImmediate) turns to yield after stopping the server to allow asynchronous work to complete before returning (default 1).
543
+ * @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
544
+ * @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server stop process to run before returning (default 10ms).
545
+ * @returns {Promise<void>} Resolves when the server has stopped.
546
+ * @example
547
+ * ```typescript
548
+ * const NAME = 'BaseTest';
549
+ * const MATTER_PORT = 8000;
550
+ * const MATTER_CREATE_ONLY = true;
551
+ *
552
+ * // Setup the test environment
553
+ * await setupTest(NAME, false);
554
+ *
555
+ * // Setup the Matter test environment
556
+ * await createTestEnvironment();
557
+ *
558
+ * // Create the server node and aggregator
559
+ * await createServerNode(MATTER_PORT);
560
+ *
561
+ * // Start the server node if not in create-only mode
562
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
563
+ *
564
+ * // Stop or flush the server node depending on the create-only mode
565
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
566
+ * else await stopServerNode();
567
+ *
568
+ * // Destroy the Matter test environment
569
+ * await destroyTestEnvironment();
570
+ * ```
571
+ */
572
+ export async function stopServerNode(ticks = 1, microTurns = 1, pause = 10) {
573
+ // Flush any pending endpoint number persistence
574
+ await flushAllEndpointNumberPersistence(server);
575
+ // Ensure all endpoint numbers are persisted
576
+ await assertAllEndpointNumbersPersisted(server);
577
+ // Stop the server
578
+ expect(server).toBeDefined();
579
+ expect(server.lifecycle.isReady).toBeTruthy();
580
+ expect(server.lifecycle.isOnline).toBeTruthy();
581
+ // Close the server to release UDP sockets and other network resources created
582
+ // by ServerNode.create() (mDNS udp4/udp6 sockets), which would otherwise keep
583
+ // the Jest process alive indefinitely after all tests finish.
584
+ await server.close();
585
+ expect(server.lifecycle.isReady).toBeFalsy();
586
+ expect(server.lifecycle.isOnline).toBeFalsy();
587
+ // Ensure the queue is empty
588
+ await flushAsync(ticks, microTurns, pause);
589
+ }
590
+ /**
591
+ * Flush any pending endpoint number persistence.
592
+ *
593
+ * @param {number} ticks Number of macrotask (setImmediate) turns to yield after stopping the server to allow asynchronous work to complete before returning (default 1).
594
+ * @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
595
+ * @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server stop process to run before returning (default 10ms).
596
+ * @returns {Promise<void>} Resolves when the server has stopped.
597
+ * @example
598
+ * ```typescript
599
+ * const NAME = 'BaseTest';
600
+ * const MATTER_PORT = 8000;
601
+ * const MATTER_CREATE_ONLY = true;
602
+ *
603
+ * // Setup the test environment
604
+ * await setupTest(NAME, false);
605
+ *
606
+ * // Setup the Matter test environment
607
+ * await createTestEnvironment();
608
+ *
609
+ * // Create the server node and aggregator
610
+ * await createServerNode(MATTER_PORT);
611
+ *
612
+ * // Start the server node if not in create-only mode
613
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
614
+ *
615
+ * // Stop or flush the server node depending on the create-only mode
616
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
617
+ * else await stopServerNode();
618
+ *
619
+ * // Destroy the Matter test environment
620
+ * await destroyTestEnvironment();
621
+ * ```
622
+ */
623
+ export async function flushServerNode(ticks = 1, microTurns = 1, pause = 10) {
624
+ // Flush any pending endpoint number persistence
625
+ await flushAllEndpointNumberPersistence(server);
626
+ // Ensure all endpoint numbers are persisted
627
+ await assertAllEndpointNumbersPersisted(server);
628
+ // Stop the server
629
+ expect(server).toBeDefined();
630
+ expect(server.lifecycle.isReady).toBeTruthy();
631
+ expect(server.lifecycle.isOnline).toBeFalsy();
632
+ // Close the server to release UDP sockets and other network resources created
633
+ // by ServerNode.create() (mDNS udp4/udp6 sockets), which would otherwise keep
634
+ // the Jest process alive indefinitely after all tests finish.
635
+ await server.close();
636
+ expect(server.lifecycle.isReady).toBeFalsy();
637
+ expect(server.lifecycle.isOnline).toBeFalsy();
638
+ // Ensure the queue is empty
639
+ await flushAsync(ticks, microTurns, pause);
640
+ }
641
+ /**
642
+ * Add a device (endpoint) to a matter server node or an aggregator.
643
+ *
644
+ * @param {ServerNode<ServerNode.RootEndpoint> | Endpoint<AggregatorEndpoint>} owner The server or aggregator to add the device to.
645
+ * @param {Endpoint} device The device to add.
646
+ * @param {number} rounds The number of rounds to perform after addition (default 3).
647
+ * @param {number} pause The pause time in milliseconds after addition (default 10ms).
648
+ * @returns {Promise<void>} Resolves when the device has been added and is ready.
649
+ * @example
650
+ * ```typescript
651
+ * expect(await addDevice(aggregator, device)).toBeTruthy();
652
+ * ```
653
+ */
654
+ export async function addDevice(owner, device, rounds = 3, pause = 10) {
655
+ expect(owner).toBeDefined();
656
+ expect(device).toBeDefined();
657
+ expect(owner.lifecycle.isReady).toBeTruthy();
658
+ expect(owner.construction.status).toBe(Lifecycle.Status.Active);
659
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
660
+ const rootServerNode = getRootServerNode(owner);
661
+ await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
662
+ /* v8 ignore next */
663
+ try {
664
+ await owner.add(device);
665
+ }
666
+ catch (error) {
667
+ inspectError(log, `Error adding device ${device.maybeId}.${device.maybeNumber}`, error);
668
+ process.stderr.write(`${er}Error adding device ${device.maybeId}.${device.maybeNumber}${rs}\n`);
669
+ return false;
670
+ }
671
+ await device.construction.ready;
672
+ expect(owner.parts.has(device)).toBeTruthy();
673
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
674
+ expect(device.lifecycle.isReady).toBeTruthy();
675
+ expect(device.lifecycle.isInstalled).toBeTruthy();
676
+ expect(device.lifecycle.hasId).toBeTruthy();
677
+ expect(device.lifecycle.hasNumber).toBeTruthy();
678
+ expect(device.construction.status).toBe(Lifecycle.Status.Active);
679
+ await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
680
+ return true;
681
+ }
682
+ /**
683
+ * Delete a device (endpoint) from a matter server node or an aggregator.
684
+ *
685
+ * @param {ServerNode<ServerNode.RootEndpoint> | Endpoint<AggregatorEndpoint>} owner The server or aggregator to remove the device from.
686
+ * @param {Endpoint} device The device to remove.
687
+ * @param {number} rounds The number of rounds to perform after deletion (default 3).
688
+ * @param {number} pause The pause time in milliseconds after deletion (default 10ms).
689
+ * @returns {Promise<void>} Resolves when the device has been removed and is no longer ready.
690
+ * @example
691
+ * ```typescript
692
+ * expect(await deleteDevice(aggregator, device)).toBeTruthy();
693
+ * ```
694
+ */
695
+ export async function deleteDevice(owner, device, rounds = 3, pause = 10) {
696
+ expect(owner).toBeDefined();
697
+ expect(device).toBeDefined();
698
+ expect(owner.lifecycle.isReady).toBeTruthy();
699
+ expect(owner.construction.status).toBe(Lifecycle.Status.Active);
700
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
701
+ const rootServerNode = getRootServerNode(owner);
702
+ await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
703
+ /* v8 ignore next */
704
+ try {
705
+ await device.delete();
706
+ }
707
+ catch (error) {
708
+ inspectError(log, `Error deleting device ${device.maybeId}.${device.maybeNumber}`, error);
709
+ process.stderr.write(`${er}Error deleting device ${device.maybeId}.${device.maybeNumber}${rs}\n`);
710
+ return false;
711
+ }
712
+ expect(owner.parts.has(device)).toBeFalsy();
713
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
714
+ expect(device.lifecycle.isReady).toBeFalsy();
715
+ expect(device.lifecycle.isInstalled).toBeFalsy();
716
+ expect(device.lifecycle.hasId).toBeTruthy();
717
+ expect(device.lifecycle.hasNumber).toBeTruthy();
718
+ expect(device.construction.status).toBe(Lifecycle.Status.Destroyed);
719
+ await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
720
+ return true;
721
+ }
722
+ //# sourceMappingURL=matterTest.js.map