@mirta/testing 0.4.12 → 0.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.
package/dist/index.d.mts CHANGED
@@ -3,20 +3,15 @@ interface SimulatorInstance {
3
3
  reset(): void;
4
4
  }
5
5
 
6
- interface DefineRuleOptions {
7
- /**
8
- * Отключает защиту от расхождения в поведении с настоящей функцией `defineRule`.
9
- *
10
- * Контроллер не отправляет одно и то же значение дважды при использовании `whenChanged`.
11
- */
12
- allowSameValue?: boolean;
6
+ interface RunOptions {
7
+ force?: boolean;
13
8
  }
14
9
  interface DefineRuleSimulator extends SimulatorInstance {
15
10
  /** Отправляет одно или несколько сообщений. */
16
- run(payload: WbRules.MqttMessage | WbRules.MqttMessage[]): void;
11
+ run(payload: WbRules.MqttMessage | WbRules.MqttMessage[], options?: RunOptions): void;
17
12
  }
18
13
  /** Имитатор конструкции defineRule. */
19
- declare function useDefineRule(options?: DefineRuleOptions): DefineRuleSimulator;
14
+ declare function useDefineRule(): DefineRuleSimulator;
20
15
 
21
16
  interface WithDevice {
22
17
  publish(controlId: string, value: WbRules.MqttValue): WithDevice;
@@ -67,12 +62,8 @@ interface CoreSimulator extends SimulatorInstance {
67
62
  get trackMqtt(): TrackMqttSimulator;
68
63
  defineZigbeeDevice(deviceId: string): ZigbeeDevice;
69
64
  }
70
- interface CoreSimulatorOptions {
71
- /** Параметры имитатора `defineRule`. */
72
- defineRule?: DefineRuleOptions;
73
- }
74
65
  /** Единая точка входа для настройки симуляции. */
75
- declare function useSimulator(options?: CoreSimulatorOptions): CoreSimulator;
66
+ declare function useSimulator(): CoreSimulator;
76
67
 
77
68
  export { defineZigbeeDevice, useDefineRule, useGetControl, useGetDevice, useSimulator, useTrackMqtt };
78
- export type { ControlValue, DefineRuleOptions, DefineRuleSimulator, GetControlSimulator, GetDeviceSimulator, TrackMqttSimulator, ZigbeeDevice };
69
+ export type { ControlValue, DefineRuleSimulator, GetControlSimulator, GetDeviceSimulator, TrackMqttSimulator, ZigbeeDevice };
package/dist/index.mjs CHANGED
@@ -1,57 +1,106 @@
1
- import { useEvent } from 'mirta';
1
+ import { useEvent, isObject } from 'mirta';
2
2
  import { mock } from 'vitest-mock-extended';
3
3
 
4
- class SameTopicValueError extends Error {
5
- constructor(message) {
6
- const text = `[Behavior] Value of the topic "${message.topic}"`
7
- + ` must be different from a previous one,`
8
- + ` got "${message.value.toString()}".`
9
- + `\nYou can suppress this error by passing 'allowSameValue: true' option to 'useDefineRule' call.`;
10
- super(text);
4
+ class DevSetValueError extends Error {
5
+ constructor(property) {
6
+ super(`[dev] Missing control name in "${property}"`);
7
+ Error.captureStackTrace(this, DevSetValueError);
11
8
  }
12
9
  }
13
- function createInstance$2(options) {
10
+ // Используем простой объект в качестве базового хранилища.
11
+ let state;
12
+ function setValueSilent(controlPath, value) {
13
+ state[controlPath] = value;
14
+ }
15
+ /**
16
+ * Создаёт прокси для объекта `dev`, который автоматически реагирует
17
+ * на изменения значений, запуская соответствующие симуляторы.
18
+ *
19
+ * @param options Объект с зависимостями симуляторов.
20
+ */
21
+ function createDev({ trackMqtt, defineRule }) {
22
+ const handler = {
23
+ get(_target, prop) {
24
+ // Прозрачное чтение значения.
25
+ return state[prop];
26
+ },
27
+ set(_target, prop, value) {
28
+ const changed = state[prop] !== value;
29
+ // Устанавливаем новое значение в хранилище.
30
+ if (changed)
31
+ state[prop] = value;
32
+ const [deviceName, controlName] = prop.split('/');
33
+ if (!controlName)
34
+ throw new DevSetValueError(prop);
35
+ // Оповещаем о любом обновлении.
36
+ trackMqtt.publish({
37
+ topic: `/devices/${deviceName}/controls/${controlName}`,
38
+ value,
39
+ });
40
+ // Отправляем только изменённое значение.
41
+ if (changed)
42
+ defineRule.run({
43
+ topic: prop,
44
+ value,
45
+ }, {
46
+ force: true,
47
+ });
48
+ return true;
49
+ },
50
+ };
51
+ function reset() {
52
+ state = {};
53
+ global.dev = new Proxy(state, handler);
54
+ }
55
+ reset();
56
+ // Возвращаем прокси, который будет использоваться как `global.dev`.
57
+ return {
58
+ reset,
59
+ };
60
+ }
61
+ /**
62
+ * Хранилище для экземпляра прокси, чтобы он был доступен при сбросе.
63
+ */
64
+ let instance$4;
65
+ /**
66
+ * Возвращает инициализированный прокси для `dev`. Создает его, если он еще не существует.
67
+ * @param options Конфигурация с зависимостями.
68
+ */
69
+ function useDev(options) {
70
+ return instance$4 ??= createDev(options);
71
+ }
72
+
73
+ function createInstance$2() {
14
74
  let mqttEvent;
15
- let values = {};
16
75
  function reset() {
17
76
  mqttEvent = useEvent();
18
- values = {};
19
77
  global.defineRule = (variantA, variantB) => {
20
78
  const rule = typeof variantA !== 'string'
21
79
  ? variantA
22
80
  : variantB;
23
81
  if (!rule)
24
- return;
82
+ return 0;
25
83
  mqttEvent.on(({ topic, value }) => {
26
84
  if (rule.whenChanged === topic)
27
85
  rule.then(value);
28
86
  });
87
+ return 0;
29
88
  };
30
89
  }
31
- // Поведение движка wb-rules, отправляет только изменившиеся значения.
32
- function isValueChanged(message) {
33
- if (options.allowSameValue)
34
- return true;
35
- if (values[message.topic] === message.value) {
36
- const error = new SameTopicValueError(message);
37
- Error.captureStackTrace(error, isValueChanged);
38
- throw error;
39
- }
40
- values[message.topic] = message.value;
41
- return true;
42
- }
43
90
  /** Отправляет одно или несколько сообщений */
44
- function run(payload) {
45
- if (!Array.isArray(payload)) {
46
- if (isValueChanged(payload))
47
- mqttEvent.raise(payload);
48
- }
49
- else {
50
- payload.forEach((item) => {
51
- if (isValueChanged(item))
52
- mqttEvent.raise(item);
53
- });
54
- }
91
+ function run(payload, options = {}) {
92
+ const { force = false } = options;
93
+ payload = Array.isArray(payload) ? payload : [payload];
94
+ payload.forEach((item) => {
95
+ if (dev[item.topic] === item.value && !force) {
96
+ // Значение не изменилось, симуляторы не запускаем.
97
+ return;
98
+ }
99
+ // Устанавливаем значение во внутреннее состояние.
100
+ setValueSilent(item.topic, item.value);
101
+ // Инициируем событие. Ивент не проверяет изменения, он просто вызывает коллбек.
102
+ mqttEvent.raise(item);
103
+ });
55
104
  }
56
105
  reset();
57
106
  return {
@@ -61,10 +110,21 @@ function createInstance$2(options) {
61
110
  }
62
111
  // let instance: DefineRuleSimulator | undefined
63
112
  /** Имитатор конструкции defineRule. */
64
- function useDefineRule(options = {}) {
65
- return /* instance ??= */ createInstance$2(options);
113
+ function useDefineRule() {
114
+ return /* instance ??= */ createInstance$2();
66
115
  }
67
116
 
117
+ function parseDeviceTopic(topic) {
118
+ if (!topic.startsWith('/devices/'))
119
+ return;
120
+ const match = /^\/devices\/([^/]+)\/controls\/([^/]+)$/.exec(topic);
121
+ if (!match)
122
+ return;
123
+ return {
124
+ deviceName: match[1],
125
+ controlName: match[2],
126
+ };
127
+ }
68
128
  function createInstance$1() {
69
129
  let mqttEvent;
70
130
  function reset() {
@@ -77,17 +137,22 @@ function createInstance$1() {
77
137
  };
78
138
  }
79
139
  function publish(payload) {
80
- if (!Array.isArray(payload))
81
- mqttEvent.raise(payload);
82
- else
83
- payload.forEach((item) => {
84
- mqttEvent.raise(item);
85
- });
140
+ payload = Array.isArray(payload) ? payload : [payload];
141
+ payload.forEach((item) => {
142
+ const parts = parseDeviceTopic(item.topic);
143
+ if (parts) {
144
+ // Устанавливаем значение во внутреннее состояние.
145
+ setValueSilent(`${parts.deviceName}/${parts.controlName}`, item.value);
146
+ }
147
+ // Инициируем событие. Ивент не проверяет изменения, он просто вызывает коллбек.
148
+ mqttEvent.raise(item);
149
+ });
86
150
  }
87
151
  function withDevice(deviceId) {
88
152
  return {
89
153
  publish(controlId, value) {
90
- mqttEvent.raise({
154
+ // Устанавливаем значение через publish, чтобы не дублировать логику.
155
+ publish({
91
156
  topic: `/devices/${deviceId}/controls/${controlId}`,
92
157
  value,
93
158
  });
@@ -128,21 +193,22 @@ function useGetDevice() {
128
193
  }
129
194
 
130
195
  function createInstance() {
131
- let values = {};
132
196
  function reset() {
133
- values = {};
134
- global.getControl = (devicePath) => {
197
+ global.getControl = (controlPath) => {
135
198
  return mock({
136
- getValue: () => values[devicePath],
199
+ getValue: () => dev[controlPath],
200
+ setValue: rawValue => dev[controlPath] = isObject(rawValue)
201
+ ? rawValue.value
202
+ : rawValue,
137
203
  });
138
204
  };
139
205
  }
140
206
  function defineValue(deviceId, controlId, value) {
141
- values[`${deviceId}/${controlId}`] = value;
207
+ dev[`${deviceId}/${controlId}`] = value;
142
208
  }
143
209
  function defineValues(presets) {
144
210
  presets.forEach((preset) => {
145
- values[`${preset.deviceId}/${preset.controlId}`] = preset.value;
211
+ dev[`${preset.deviceId}/${preset.controlId}`] = preset.value;
146
212
  });
147
213
  }
148
214
  reset();
@@ -196,7 +262,7 @@ function defineZigbeeDevice(deviceId) {
196
262
  };
197
263
  }
198
264
 
199
- function createSimulator(options) {
265
+ function createSimulator() {
200
266
  const simulators = {};
201
267
  function reset() {
202
268
  for (const key in simulators)
@@ -204,8 +270,11 @@ function createSimulator(options) {
204
270
  }
205
271
  const getDevice = (simulators.getDevice ??= useGetDevice());
206
272
  const getControl = (simulators.getControl ??= useGetControl());
207
- const defineRule = (simulators.defineRule ??= useDefineRule(options.defineRule));
273
+ const defineRule = (simulators.defineRule ??= useDefineRule());
208
274
  const trackMqtt = (simulators.trackMqtt ??= useTrackMqtt());
275
+ // Создаем и устанавливаем прокси на dev, передавая ему зависимости.
276
+ // Это должно быть сделано ПОСЛЕ инициализации defineRule и trackMqtt.
277
+ simulators.dev ??= useDev({ defineRule, trackMqtt });
209
278
  return {
210
279
  reset,
211
280
  getDevice,
@@ -217,8 +286,8 @@ function createSimulator(options) {
217
286
  }
218
287
  let instance;
219
288
  /** Единая точка входа для настройки симуляции. */
220
- function useSimulator(options = {}) {
221
- return instance ??= createSimulator(options);
289
+ function useSimulator() {
290
+ return instance ??= createSimulator();
222
291
  }
223
292
 
224
293
  export { defineZigbeeDevice, useDefineRule, useGetControl, useGetDevice, useSimulator, useTrackMqtt };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mirta/testing",
3
3
  "description": "A unit-test runner equipped with built-in Wiren Board runtime emulation",
4
- "version": "0.4.12",
4
+ "version": "0.5.0",
5
5
  "license": "Unlicense",
6
6
  "keywords": [
7
7
  "mirta",
@@ -14,6 +14,9 @@
14
14
  "README.md"
15
15
  ],
16
16
  "types": "./dist/index.d.mts",
17
+ "imports": {
18
+ "#*": "./src/*.js"
19
+ },
17
20
  "exports": {
18
21
  ".": {
19
22
  "import": {
@@ -39,12 +42,12 @@
39
42
  },
40
43
  "dependencies": {
41
44
  "vitest-mock-extended": "^3.1.0",
42
- "@mirta/env-loader": "0.4.12",
43
- "@mirta/workspace": "0.4.12",
44
- "mirta": "0.4.12"
45
+ "@mirta/env-loader": "0.5.0",
46
+ "@mirta/workspace": "0.5.0",
47
+ "mirta": "0.5.0"
45
48
  },
46
49
  "devDependencies": {
47
- "@mirta/rollup": "0.4.12"
50
+ "@mirta/rollup": "0.5.0"
48
51
  },
49
52
  "scripts": {
50
53
  "build:mono": "rollup -c"