@cuppet/core 1.1.5 → 1.2.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.
@@ -1,6 +1,7 @@
1
1
  const { BeforeAll, AfterAll, Before, After, AfterStep, Status } = require('@cucumber/cucumber');
2
2
  const BrowserManager = require('./browserManager');
3
3
  const AppiumManager = require('./appiumManager');
4
+ const MqttManager = require('./mqttManager');
4
5
  const fs = require('fs');
5
6
  const config = require('config');
6
7
  const dataStore = require('../../src/dataStorage');
@@ -62,10 +63,21 @@ Before(async function (testCase) {
62
63
  // Get Appium capabilities from config
63
64
  const appiumCapabilities = config.get('appiumCapabilities');
64
65
 
65
- // Check if the test is tagged with @appium to either use Appium or Chromium
66
+ // Check if the test is tagged with @appium, @mqtt or neither
66
67
  const arrayTags = testCase.pickle.tags;
67
- const found = arrayTags.find((item) => item.name === '@appium');
68
- if (!found) {
68
+ const appiumTag = arrayTags.find((item) => item.name === '@appium');
69
+ const mqttTag = arrayTags.find((item) => item.name === '@mqtt');
70
+ const apiTag = arrayTags.find((item) => item.name === '@api');
71
+
72
+ // Initialize MQTT Manager if @mqtt tag is present
73
+ if (mqttTag) {
74
+ const mqttManager = new MqttManager();
75
+ await mqttManager.initialize();
76
+ this.mqttManager = mqttManager;
77
+ }
78
+
79
+ // Initialize browser unless it's API-only or MQTT-only test
80
+ if (!appiumTag && !apiTag && !mqttTag) {
69
81
  const browserManager = new BrowserManager(browserViewport, browserArgs, credentials);
70
82
  await browserManager.initialize();
71
83
 
@@ -74,7 +86,7 @@ Before(async function (testCase) {
74
86
  this.browser = browserManager.browser;
75
87
  this.page = browserManager.page;
76
88
  this.scenarioName = testCase.pickle.name;
77
- } else {
89
+ } else if (appiumTag) {
78
90
  const appiumManager = new AppiumManager(appiumCapabilities);
79
91
  await appiumManager.initialize();
80
92
  this.appiumManager = appiumManager;
@@ -87,6 +99,14 @@ After(async function (testCase) {
87
99
  if (testCase.result.status === Status.FAILED) {
88
100
  console.log(`Scenario: '${testCase.pickle.name}' - has failed...\r\n`);
89
101
  }
102
+
103
+ // Cleanup MQTT connection if present
104
+ if (this.mqttManager) {
105
+ await this.mqttManager.stop();
106
+ this.mqttManager = null;
107
+ }
108
+
109
+ // Cleanup browser/appium connections
90
110
  if (this.browser) {
91
111
  await this.browserManager.stop();
92
112
  } else if (this.appiumDriver) {
@@ -0,0 +1,291 @@
1
+ const mqtt = require('mqtt');
2
+ const config = require('config');
3
+
4
+ /**
5
+ * MqttManager class for managing MQTT client lifecycle
6
+ * Follows the same pattern as BrowserManager and AppiumManager
7
+ */
8
+ class MqttManager {
9
+ constructor(brokerUrl = null, options = {}) {
10
+ this.client = null;
11
+ this.brokerUrl =
12
+ brokerUrl || (config.has('mqtt.brokerUrl') ? config.get('mqtt.brokerUrl') : 'mqtt://localhost:1883');
13
+ this.options = this.prepareOptions(options);
14
+ this.messageBuffer = new Map(); // Store messages by topic
15
+ this.subscriptions = new Set(); // Track active subscriptions
16
+ }
17
+
18
+ /**
19
+ * Prepare MQTT connection options from config or provided options
20
+ * @param {Object} customOptions - Custom options to override config
21
+ * @returns {Object} - MQTT connection options
22
+ */
23
+ prepareOptions(customOptions) {
24
+ const defaultOptions = {
25
+ clientId: config.has('mqtt.clientId')
26
+ ? config.get('mqtt.clientId')
27
+ : `cuppet-test-${Math.random().toString(16).slice(2, 8)}`,
28
+ clean: config.has('mqtt.cleanSession') ? config.get('mqtt.cleanSession') : true,
29
+ connectTimeout: config.has('mqtt.connectTimeout') ? config.get('mqtt.connectTimeout') : 5000,
30
+ keepalive: config.has('mqtt.keepalive') ? config.get('mqtt.keepalive') : 60,
31
+ };
32
+
33
+ // Add username and password if provided in config
34
+ if (config.has('mqtt.username')) {
35
+ defaultOptions.username = config.get('mqtt.username');
36
+ }
37
+ if (config.has('mqtt.password')) {
38
+ defaultOptions.password = config.get('mqtt.password');
39
+ }
40
+
41
+ // Merge with custom options
42
+ return { ...defaultOptions, ...customOptions };
43
+ }
44
+
45
+ /**
46
+ * Initialize MQTT client and connect to broker
47
+ * @returns {Promise<void>}
48
+ */
49
+ async initialize() {
50
+ return new Promise((resolve, reject) => {
51
+ try {
52
+ console.log(`Connecting to MQTT broker: ${this.brokerUrl}`);
53
+ this.client = mqtt.connect(this.brokerUrl, this.options);
54
+
55
+ let settled = false; // Guard against multiple resolve/reject calls
56
+
57
+ // Centralized cleanup function
58
+ const cleanup = () => {
59
+ clearTimeout(timeoutId);
60
+ this.client.removeListener('connect', onConnect);
61
+ this.client.removeListener('error', onError);
62
+ };
63
+
64
+ // Declare timeout first
65
+ const timeoutId = setTimeout(() => {
66
+ if (!settled) {
67
+ settled = true;
68
+ cleanup();
69
+ this.client.end(true);
70
+ reject(new Error(`MQTT connection timeout after ${this.options.connectTimeout}ms`));
71
+ }
72
+ }, this.options.connectTimeout);
73
+
74
+ const onConnect = () => {
75
+ if (!settled) {
76
+ settled = true;
77
+ cleanup();
78
+ console.log(`Successfully connected to MQTT broker: ${this.brokerUrl}`);
79
+ this.setupMessageHandler();
80
+ resolve();
81
+ }
82
+ };
83
+
84
+ const onError = (error) => {
85
+ if (!settled) {
86
+ settled = true;
87
+ cleanup();
88
+ console.error(`MQTT connection error: ${error.message}`);
89
+ this.client.end(true);
90
+ reject(error);
91
+ }
92
+ };
93
+
94
+ this.client.once('connect', onConnect);
95
+ this.client.once('error', onError);
96
+ } catch (error) {
97
+ if (this.client) {
98
+ this.client.end(true);
99
+ }
100
+ reject(error);
101
+ }
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Setup message handler to buffer incoming messages
107
+ * @private
108
+ */
109
+ setupMessageHandler() {
110
+ this.client.on('message', (topic, message) => {
111
+ console.log(`Received message on topic: ${topic}`);
112
+
113
+ // Store message in buffer for the specific topic
114
+ if (!this.messageBuffer.has(topic)) {
115
+ this.messageBuffer.set(topic, []);
116
+ }
117
+
118
+ const messageData = {
119
+ topic: topic,
120
+ message: message.toString(),
121
+ timestamp: Date.now(),
122
+ raw: message,
123
+ };
124
+
125
+ this.messageBuffer.get(topic).push(messageData);
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Subscribe to a topic or topics
131
+ * @param {string|string[]} topic - Topic or array of topics to subscribe to
132
+ * @param {Object} options - Subscription options (qos, etc.)
133
+ * @returns {Promise<void>}
134
+ */
135
+ async subscribe(topic, options = { qos: 0 }) {
136
+ return new Promise((resolve, reject) => {
137
+ this.client.subscribe(topic, options, (error, granted) => {
138
+ if (error) {
139
+ console.error(`Failed to subscribe to ${topic}: ${error.message}`);
140
+ reject(error);
141
+ } else {
142
+ const topics = Array.isArray(topic) ? topic : [topic];
143
+ topics.forEach((t) => this.subscriptions.add(t));
144
+ console.log(`Successfully subscribed to: ${JSON.stringify(granted)}`);
145
+ resolve(granted);
146
+ }
147
+ });
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Unsubscribe from a topic or topics
153
+ * @param {string|string[]} topic - Topic or array of topics to unsubscribe from
154
+ * @returns {Promise<void>}
155
+ */
156
+ async unsubscribe(topic) {
157
+ return new Promise((resolve, reject) => {
158
+ this.client.unsubscribe(topic, (error) => {
159
+ if (error) {
160
+ console.error(`Failed to unsubscribe from ${topic}: ${error.message}`);
161
+ reject(error);
162
+ } else {
163
+ const topics = Array.isArray(topic) ? topic : [topic];
164
+ topics.forEach((t) => this.subscriptions.delete(t));
165
+ console.log(`Successfully unsubscribed from: ${topic}`);
166
+ resolve();
167
+ }
168
+ });
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Publish a message to a topic
174
+ * @param {string} topic - Topic to publish to
175
+ * @param {string|Buffer} message - Message to publish
176
+ * @param {Object} options - Publish options (qos, retain, etc.)
177
+ * @returns {Promise<void>}
178
+ */
179
+ async publish(topic, message, options = { qos: 0, retain: false }) {
180
+ return new Promise((resolve, reject) => {
181
+ this.client.publish(topic, message, options, (error) => {
182
+ if (error) {
183
+ console.error(`Failed to publish to ${topic}: ${error.message}`);
184
+ reject(error);
185
+ } else {
186
+ console.log(`Successfully published to: ${topic}`);
187
+ resolve();
188
+ }
189
+ });
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Get messages from buffer for a specific topic
195
+ * @param {string} topic - Topic to get messages for
196
+ * @returns {Array} - Array of messages
197
+ */
198
+ getMessages(topic) {
199
+ return this.messageBuffer.get(topic) || [];
200
+ }
201
+
202
+ /**
203
+ * Get the latest message from buffer for a specific topic
204
+ * @param {string} topic - Topic to get latest message for
205
+ * @returns {Object|null} - Latest message or null
206
+ */
207
+ getLatestMessage(topic) {
208
+ const messages = this.getMessages(topic);
209
+ return messages.length > 0 ? messages[messages.length - 1] : null;
210
+ }
211
+
212
+ /**
213
+ * Clear message buffer for a specific topic or all topics
214
+ * @param {string|null} topic - Topic to clear or null to clear all
215
+ * @returns {<void>}
216
+ */
217
+ clearMessageBuffer(topic = null) {
218
+ if (topic) {
219
+ this.messageBuffer.delete(topic);
220
+ console.log(`Cleared message buffer for topic: ${topic}`);
221
+ } else {
222
+ this.messageBuffer.clear();
223
+ console.log('Cleared all message buffers');
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Wait for a message on a specific topic with timeout
229
+ * @param {string} topic - Topic to wait for message on
230
+ * @param {number} timeoutSeconds - Timeout in seconds
231
+ * @returns {Promise<Object>} - Resolves with message when received
232
+ */
233
+ async waitForMessage(topic, timeoutSeconds = 10) {
234
+ const timeoutMs = timeoutSeconds * 1000;
235
+ const startTime = Date.now();
236
+
237
+ return new Promise((resolve, reject) => {
238
+ const checkInterval = setInterval(() => {
239
+ const latestMessage = this.getLatestMessage(topic);
240
+
241
+ if (latestMessage && latestMessage.timestamp >= startTime) {
242
+ clearInterval(checkInterval);
243
+ resolve(latestMessage);
244
+ } else if (Date.now() - startTime > timeoutMs) {
245
+ clearInterval(checkInterval);
246
+ reject(new Error(`Timeout waiting for message on topic: ${topic} after ${timeoutSeconds} seconds`));
247
+ }
248
+ }, 100); // Check every 100ms
249
+ });
250
+ }
251
+
252
+ /**
253
+ * Check if client is connected
254
+ * @returns {boolean}
255
+ */
256
+ isConnected() {
257
+ return this.client && this.client.connected;
258
+ }
259
+
260
+ /**
261
+ * Stop the MQTT client and cleanup
262
+ * @returns {Promise<void>}
263
+ */
264
+ async stop() {
265
+ return new Promise((resolve) => {
266
+ if (this.client && this.client.connected) {
267
+ console.log('Disconnecting from MQTT broker...');
268
+
269
+ // Unsubscribe from all topics
270
+ if (this.subscriptions.size > 0) {
271
+ const topicsArray = Array.from(this.subscriptions);
272
+ this.client.unsubscribe(topicsArray);
273
+ }
274
+
275
+ // Clear buffers
276
+ this.messageBuffer.clear();
277
+ this.subscriptions.clear();
278
+
279
+ // End connection
280
+ this.client.end(false, () => {
281
+ console.log('MQTT client disconnected');
282
+ resolve();
283
+ });
284
+ } else {
285
+ resolve();
286
+ }
287
+ });
288
+ }
289
+ }
290
+
291
+ module.exports = MqttManager;
@@ -0,0 +1,168 @@
1
+ const { Given, When, Then } = require('@cucumber/cucumber');
2
+ const mqttFunctions = require('../../../src/mqttFunctions');
3
+
4
+ /**
5
+ * MQTT Step Definitions
6
+ * These steps integrate with the hook-managed MQTT connection
7
+ */
8
+
9
+ /**
10
+ * Subscribe to an MQTT topic
11
+ * @example When I subscribe to MQTT topic "devices/sensor1/telemetry"
12
+ * @example When I subscribe to MQTT topic "devices/+/data" with QoS 1
13
+ */
14
+ When('I subscribe to MQTT topic {string}', async function (topic) {
15
+ await mqttFunctions.subscribeToTopic(this.mqttManager, topic, 0);
16
+ });
17
+
18
+ When('I subscribe to MQTT topic {string} with QoS {int}', async function (topic, qos) {
19
+ await mqttFunctions.subscribeToTopic(this.mqttManager, topic, qos);
20
+ });
21
+
22
+ /**
23
+ * Unsubscribe from an MQTT topic
24
+ * @example When I unsubscribe from MQTT topic "devices/sensor1/telemetry"
25
+ */
26
+ When('I unsubscribe from MQTT topic {string}', async function (topic) {
27
+ await mqttFunctions.unsubscribeFromTopic(this.mqttManager, topic);
28
+ });
29
+
30
+ /**
31
+ * Publish a message to an MQTT topic
32
+ * @example When I publish "Hello World" to MQTT topic "test/message"
33
+ * @example When I publish "%savedVariable%" to MQTT topic "devices/sensor1/command"
34
+ */
35
+ When('I publish {string} to MQTT topic {string}', async function (message, topic) {
36
+ await mqttFunctions.publishMessage(this.mqttManager, message, topic, 0, false);
37
+ });
38
+
39
+ /**
40
+ * Publish a message with QoS and retain options
41
+ * @example When I publish "Alert" to MQTT topic "alerts/critical" with QoS 1 and retain true
42
+ */
43
+ When(
44
+ 'I publish {string} to MQTT topic {string} with QoS {int} and retain {word}',
45
+ async function (message, topic, qos, retain) {
46
+ const retainFlag = retain === 'true';
47
+ await mqttFunctions.publishMessage(this.mqttManager, message, topic, qos, retainFlag);
48
+ }
49
+ );
50
+
51
+ /**
52
+ * Publish a JSON message to an MQTT topic
53
+ * @example When I publish JSON '{"temperature": 25, "humidity": 60}' to MQTT topic "sensors/room1"
54
+ */
55
+ When('I publish JSON {string} to MQTT topic {string}', async function (jsonMessage, topic) {
56
+ await mqttFunctions.publishJsonMessage(this.mqttManager, jsonMessage, topic, 0, false);
57
+ });
58
+
59
+ /**
60
+ * Wait for and validate message reception on a topic
61
+ * @example Then I should receive a message on MQTT topic "devices/sensor1/response" within 5 seconds
62
+ * @example Then I should receive a message on MQTT topic "alerts/+/critical" within 10 seconds
63
+ */
64
+ Then('I should receive a message on MQTT topic {string} within {int} seconds', async function (topic, timeout) {
65
+ await mqttFunctions.validateMessageReceived(this.mqttManager, topic, timeout);
66
+ });
67
+
68
+ Then('I should receive a message on MQTT topic {string}', async function (topic) {
69
+ await mqttFunctions.validateMessageReceived(this.mqttManager, topic, 10);
70
+ });
71
+
72
+ /**
73
+ * Validate the content of the latest message on a topic
74
+ * @example Then the MQTT message on topic "test/echo" should equal "Hello World"
75
+ */
76
+ Then('the MQTT message on topic {string} should equal {string}', async function (topic, expectedContent) {
77
+ await mqttFunctions.validateMessageContent(this.mqttManager, topic, expectedContent);
78
+ });
79
+
80
+ /**
81
+ * Validate that the message contains a substring
82
+ * @example Then the MQTT message on topic "logs/app" should contain "ERROR"
83
+ */
84
+ Then('the MQTT message on topic {string} should contain {string}', async function (topic, substring) {
85
+ await mqttFunctions.validateMessageContains(this.mqttManager, topic, substring);
86
+ });
87
+
88
+ /**
89
+ * Validate JSON message property value
90
+ * @example Then the MQTT message on topic "sensors/temp" should have property "temperature" with value "25"
91
+ * @example Then the MQTT message on topic "data/user" should have property "user.name" with value "John"
92
+ */
93
+ Then(
94
+ 'the MQTT message on topic {string} should have property {string} with value {string}',
95
+ async function (topic, property, expectedValue) {
96
+ await mqttFunctions.validateJsonProperty(this.mqttManager, topic, property, expectedValue);
97
+ }
98
+ );
99
+
100
+ /**
101
+ * Validate JSON message property type
102
+ * @example Then the MQTT message on topic "sensors/data" property "temperature" should be a "number"
103
+ */
104
+ Then(
105
+ 'the MQTT message on topic {string} property {string} should be a {string}',
106
+ async function (topic, property, type) {
107
+ await mqttFunctions.validateJsonPropertyType(this.mqttManager, topic, property, type);
108
+ }
109
+ );
110
+
111
+ /**
112
+ * Remember a JSON property value from MQTT message
113
+ * @example Then I remember the MQTT message property "id" from topic "devices/sensor1/response" as "deviceId"
114
+ * @example Then I remember the MQTT message property "data.temperature" from topic "sensors/temp" as "currentTemp"
115
+ */
116
+ Then(
117
+ 'I remember the MQTT message property {string} from topic {string} as {string}',
118
+ async function (property, topic, variableName) {
119
+ await mqttFunctions.rememberJsonProperty(this.mqttManager, topic, property, variableName);
120
+ }
121
+ );
122
+
123
+ /**
124
+ * Remember the entire message content
125
+ * @example Then I remember the MQTT message from topic "test/data" as "lastMessage"
126
+ */
127
+ Then('I remember the MQTT message from topic {string} as {string}', async function (topic, variableName) {
128
+ await mqttFunctions.rememberMessage(this.mqttManager, topic, variableName);
129
+ });
130
+
131
+ /**
132
+ * Clear message buffer for a topic
133
+ * @example When I clear the MQTT message buffer for topic "test/messages"
134
+ * @example When I clear all MQTT message buffers
135
+ */
136
+ When('I clear the MQTT message buffer for topic {string}', async function (topic) {
137
+ await mqttFunctions.clearMessageBuffer(this.mqttManager, topic);
138
+ });
139
+
140
+ When('I clear all MQTT message buffers', async function () {
141
+ await mqttFunctions.clearMessageBuffer(this.mqttManager, null);
142
+ });
143
+
144
+ /**
145
+ * Validate message count on a topic
146
+ * @example Then I should have received 3 messages on MQTT topic "test/counter"
147
+ */
148
+ Then('I should have received {int} messages on MQTT topic {string}', async function (expectedCount, topic) {
149
+ await mqttFunctions.validateMessageCount(this.mqttManager, topic, expectedCount);
150
+ });
151
+
152
+ /**
153
+ * Optional: Explicit connect/disconnect steps for advanced scenarios
154
+ * These are not needed if using the @mqtt tag (connection is automatic)
155
+ */
156
+ Given('I connect to MQTT broker {string}', async function (brokerUrl) {
157
+ const MqttManager = require('../mqttManager');
158
+ const mqttManager = new MqttManager(brokerUrl);
159
+ await mqttManager.initialize();
160
+ this.mqttManager = mqttManager;
161
+ });
162
+
163
+ Given('I disconnect from MQTT broker', async function () {
164
+ if (this.mqttManager) {
165
+ await this.mqttManager.stop();
166
+ this.mqttManager = null;
167
+ }
168
+ });
@@ -0,0 +1,7 @@
1
+ Feature: Open DEMO QA (as base site) and run performance tests
2
+
3
+ Scenario: Light house performance test
4
+ Given I go to "/"
5
+ Then I save the path of the current page
6
+ And I generate lighthouse report for the saved page
7
+
@@ -0,0 +1,9 @@
1
+ @appium
2
+ Feature: Click elements on mobile device
3
+ Scenario: Open settings page and click on a specific setting
4
+ Given I go to "com.google.android.youtube" app package and ".HomeActivity" activity
5
+ And I click on the element "id=com.android.permissioncontroller:id/permission_deny_button" on mobile
6
+ And I wait for "2" seconds
7
+ Then I scroll to the element 'text("Reject all")' on mobile
8
+ Then I click on the element '.text("Reject all")' on mobile
9
+ Then I click on the element '.description("Search")' on mobile
@@ -0,0 +1,6 @@
1
+ Feature: Open DEMO QA (as base site) run accessibility tests
2
+
3
+ Scenario: Pa11y accessibility test
4
+ Given I go to "/"
5
+ Then I save the path of the current page
6
+ And I validate the saved page accessibility
@@ -0,0 +1,5 @@
1
+ Feature: Open DEMO QA and run visual tests
2
+
3
+ Scenario: Backstop visual test
4
+ And I generate reference screenshot for "https://demoqa.com/elements"
5
+ Then I compare "https://demoqa.com/elements" to reference screenshot
@@ -0,0 +1,48 @@
1
+ @mqtt
2
+ Feature: MQTT Testing Examples
3
+ Test MQTT messaging functionality using Cucumber and mqtt.js
4
+
5
+ Background:
6
+ Given I subscribe to MQTT topic "test/echo"
7
+ And I subscribe to MQTT topic "sensors/temperature"
8
+
9
+ Scenario: Simple message publish and receive
10
+ When I publish "Hello MQTT World" to MQTT topic "test/echo"
11
+ Then I should receive a message on MQTT topic "test/echo" within 5 seconds
12
+ And the MQTT message on topic "test/echo" should equal "Hello MQTT World"
13
+
14
+ Scenario: JSON message with property validation
15
+ When I publish JSON '{"temperature": 25.5, "humidity": 60, "unit": "celsius"}' to MQTT topic "sensors/temperature"
16
+ Then I should receive a message on MQTT topic "sensors/temperature" within 5 seconds
17
+ And the MQTT message on topic "sensors/temperature" should have property "temperature" with value "25.5"
18
+ And the MQTT message on topic "sensors/temperature" should have property "unit" with value "celsius"
19
+ And the MQTT message on topic "sensors/temperature" property "temperature" should be a "number"
20
+ And the MQTT message on topic "sensors/temperature" property "humidity" should be a "number"
21
+
22
+ Scenario: Store and reuse MQTT message data
23
+ Given I subscribe to MQTT topic "devices/sensor-001/status"
24
+ When I publish JSON '{"deviceId": "sensor-001", "status": "active"}' to MQTT topic "devices/sensor-001/status"
25
+ Then I should receive a message on MQTT topic "devices/sensor-001/status" within 5 seconds
26
+ And I remember the MQTT message property "deviceId" from topic "devices/sensor-001/status" as "savedDeviceId"
27
+ And I publish "Command for %savedDeviceId%" to MQTT topic "devices/commands"
28
+
29
+ Scenario: Test message with QoS and retain
30
+ When I publish "Critical Alert" to MQTT topic "alerts/critical" with QoS 1 and retain true
31
+ And I subscribe to MQTT topic "alerts/critical" with QoS 1
32
+ Then I should receive a message on MQTT topic "alerts/critical" within 5 seconds
33
+ And the MQTT message on topic "alerts/critical" should contain "Critical"
34
+
35
+ Scenario: Validate message count
36
+ When I clear the MQTT message buffer for topic "test/counter"
37
+ And I subscribe to MQTT topic "test/counter"
38
+ And I publish "Message 1" to MQTT topic "test/counter"
39
+ And I publish "Message 2" to MQTT topic "test/counter"
40
+ And I publish "Message 3" to MQTT topic "test/counter"
41
+ And I wait for "1" seconds
42
+ Then I should have received 3 messages on MQTT topic "test/counter"
43
+
44
+ Scenario: Wildcard topic subscription
45
+ When I subscribe to MQTT topic "devices/+/telemetry"
46
+ And I publish JSON '{"device": "sensor1", "value": 100}' to MQTT topic "devices/sensor1/telemetry"
47
+ Then I should receive a message on MQTT topic "devices/sensor1/telemetry" within 5 seconds
48
+ And the MQTT message on topic "devices/sensor1/telemetry" should have property "device" with value "sensor1"
package/index.js CHANGED
@@ -7,6 +7,7 @@ const dataStorage = require('./src/dataStorage');
7
7
  const mainFunctions = require('./src/mainFunctions');
8
8
  const helperFunctions = require('./src/helperFunctions');
9
9
  const apiFunctions = require('./src/apiFunctions');
10
+ const mqttFunctions = require('./src/mqttFunctions');
10
11
  const appiumTesting = require('./src/appiumTesting');
11
12
  const accessibilityTesting = require('./src/accessibilityTesting');
12
13
  const lighthouse = require('./src/lighthouse');
@@ -15,6 +16,7 @@ const visualRegression = require('./src/visualRegression');
15
16
  // Export managers
16
17
  const BrowserManager = require('./features/app/browserManager');
17
18
  const AppiumManager = require('./features/app/appiumManager');
19
+ const MqttManager = require('./features/app/mqttManager');
18
20
 
19
21
  // Export step definitions
20
22
  const stepDefinitions = require('./stepDefinitions');
@@ -26,6 +28,7 @@ module.exports = {
26
28
  mainFunctions,
27
29
  helperFunctions,
28
30
  apiFunctions,
31
+ mqttFunctions,
29
32
  appiumTesting,
30
33
  accessibilityTesting,
31
34
  lighthouse,
@@ -34,6 +37,7 @@ module.exports = {
34
37
  // Managers
35
38
  BrowserManager,
36
39
  AppiumManager,
40
+ MqttManager,
37
41
 
38
42
  // Step definitions
39
43
  stepDefinitions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cuppet/core",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "Core testing framework components for Cuppet - BDD framework based on Cucumber and Puppeteer",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -17,6 +17,8 @@
17
17
  "cucumber",
18
18
  "puppeteer",
19
19
  "appium",
20
+ "mqtt",
21
+ "iot",
20
22
  "automation",
21
23
  "e2e"
22
24
  ],
@@ -32,12 +34,13 @@
32
34
  "appium": "3.0.1",
33
35
  "appium-uiautomator2-driver": "5.0",
34
36
  "axios": "^1.11.0",
35
- "backstopjs": "^6.3.23",
37
+ "backstopjs": "^6.3.25",
36
38
  "chai": "6.0.1",
37
39
  "lighthouse": "^12.8.0",
38
40
  "mime": "^3.0.0",
39
41
  "mime-types": "^3.0.1",
40
42
  "moment": "^2.30.1",
43
+ "mqtt": "^5.11.3",
41
44
  "pa11y": "^9.0.0",
42
45
  "pa11y-reporter-html": "^2.0.0",
43
46
  "puppeteer": "^24.0.1",
@@ -60,7 +63,14 @@
60
63
  "semantic-release": "^24.2.5"
61
64
  },
62
65
  "scripts": {
63
- "test": "cucumber-js features/tests",
66
+ "test": "cucumber-js features/tests/example.feature",
67
+ "run:visual": "cucumber-js features/tests/example-visual-test.feature",
68
+ "run:pa11y": "cucumber-js features/tests/example-pa11y.feature",
69
+ "run:lighthouse": "cucumber-js features/tests/example-lighthouse.feature",
70
+ "run:mqtt": "cucumber-js features/tests/mqttExample.feature",
71
+ "run:mobile": "cucumber-js features/tests/example-mobile.feature",
72
+ "run:accessibility": "cucumber-js features/tests/example-accessibility.feature",
73
+ "run:performance": "cucumber-js features/tests/example-performance.feature",
64
74
  "postinstall": "node postinstall.js",
65
75
  "lint:check": "eslint .",
66
76
  "format:check": "prettier --check .",
@@ -1,6 +1,7 @@
1
1
  const axios = require('axios');
2
2
  const config = require('config');
3
3
  const storage = require('./dataStorage');
4
+ const helper = require('./helperFunctions');
4
5
  const xml2js = require('xml2js');
5
6
  const assert = require('chai').assert;
6
7
  const fs = require('fs');
@@ -201,7 +202,7 @@ module.exports = {
201
202
  * @returns {Promise<void>}
202
203
  */
203
204
  propertyHasValue: async function (property, expectedValue) {
204
- const actualValue = await this.getPropertyValue(property);
205
+ const actualValue = await helper.getPropertyValue(this.response.data, property);
205
206
  assert.strictEqual(actualValue, expectedValue, `Property "${property}" does not have the expected value`);
206
207
  },
207
208
 
@@ -214,28 +215,10 @@ module.exports = {
214
215
  * @returns {Promise<void>}
215
216
  */
216
217
  iRememberVariable: async function (property, variable) {
217
- const propValue = await this.getPropertyValue(property);
218
+ const propValue = await helper.getPropertyValue(this.response.data, property);
218
219
  await storage.iStoreVariableWithValueToTheJsonFile(propValue, variable);
219
220
  },
220
221
 
221
- /**
222
- * Go through the response object and return the value of specific property
223
- * @param property - name of the property. For nested structure use -> parent.child1.child2 etc.
224
- * @returns {Promise<*>}
225
- */
226
- getPropertyValue: async function (property) {
227
- const response = this.response.data;
228
- const keys = property.split('.');
229
- let value = response;
230
- for (let key of keys) {
231
- value = value[key];
232
- }
233
- if (!value) {
234
- throw new Error(`Value with property: ${property} is not found!`);
235
- }
236
- return value;
237
- },
238
-
239
222
  /**
240
223
  * Load custom json file and make a request body from it
241
224
  * @param path
@@ -32,6 +32,24 @@ module.exports = {
32
32
  .substring(2, length + 2);
33
33
  },
34
34
 
35
+ /**
36
+ * Iterate through json object and return the value of specific property
37
+ * @param object - the json object to iterate through
38
+ * @param property - name of the property. For nested structure use -> parent.child1.child2 etc.
39
+ * @returns {*}
40
+ */
41
+ getPropertyValue: function (object, property) {
42
+ const keys = property.split('.');
43
+ let value = object;
44
+ for (let key of keys) {
45
+ if (value === undefined || value === null) {
46
+ throw new Error(`Property path "${property}" not found in object`);
47
+ }
48
+ value = value[key];
49
+ }
50
+ return value;
51
+ },
52
+
35
53
  /**
36
54
  * Wait until AJAX request is completed
37
55
  * @param {Page} page
@@ -162,4 +180,28 @@ module.exports = {
162
180
  let pathName = newUrl.pathname;
163
181
  return pathName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
164
182
  },
183
+
184
+ /**
185
+ * Convert string representations of primitive types to their actual types
186
+ * Handles: "null" -> null, "true" -> true, "false" -> false, "undefined" -> undefined, numeric strings -> numbers
187
+ * @param {*} value - The value to convert
188
+ * @returns {*} - The converted value or original value if no conversion needed
189
+ */
190
+ castPrimitiveType: function (value) {
191
+ if (typeof value === 'string') {
192
+ if (value === 'null') {
193
+ return null;
194
+ } else if (value === 'true') {
195
+ return true;
196
+ } else if (value === 'false') {
197
+ return false;
198
+ } else if (value === 'undefined') {
199
+ return undefined;
200
+ } else if (!isNaN(value) && value.trim() !== '') {
201
+ // Convert numeric strings to numbers
202
+ return Number(value);
203
+ }
204
+ }
205
+ return value;
206
+ },
165
207
  };
@@ -0,0 +1,285 @@
1
+ const assert = require('chai').assert;
2
+ const storage = require('./dataStorage');
3
+ const helper = require('./helperFunctions');
4
+
5
+ /**
6
+ * MQTT Functions Module
7
+ * Provides core MQTT testing operations following the same pattern as Puppeteer and Appium functions
8
+ */
9
+ module.exports = {
10
+ /**
11
+ * Prepare topic by replacing variables
12
+ * @param {string} topic - Topic with potential variables
13
+ * @returns {Promise<string>} - Resolved topic
14
+ */
15
+ prepareTopic: async function (topic) {
16
+ return await storage.checkForMultipleVariables(topic);
17
+ },
18
+
19
+ /**
20
+ * Prepare message by replacing variables
21
+ * @param {string} message - Message with potential variables
22
+ * @returns {Promise<string>} - Resolved message
23
+ */
24
+ prepareMessage: async function (message) {
25
+ return await storage.checkForMultipleVariables(message);
26
+ },
27
+
28
+ /**
29
+ * Subscribe to a topic
30
+ * @param {object} mqttManager - MQTT manager instance
31
+ * @param {string} topic - Topic to subscribe to
32
+ * @param {number} qos - Quality of Service level (0, 1, or 2)
33
+ * @returns {Promise<void>}
34
+ */
35
+ subscribeToTopic: async function (mqttManager, topic, qos = 0) {
36
+ const resolvedTopic = await this.prepareTopic(topic);
37
+ await mqttManager.subscribe(resolvedTopic, { qos });
38
+ },
39
+
40
+ /**
41
+ * Unsubscribe from a topic
42
+ * @param {object} mqttManager - MQTT manager instance
43
+ * @param {string} topic - Topic to unsubscribe from
44
+ * @returns {Promise<void>}
45
+ */
46
+ unsubscribeFromTopic: async function (mqttManager, topic) {
47
+ const resolvedTopic = await this.prepareTopic(topic);
48
+ await mqttManager.unsubscribe(resolvedTopic);
49
+ },
50
+
51
+ /**
52
+ * Publish a message to a topic
53
+ * @param {object} mqttManager - MQTT manager instance
54
+ * @param {string} message - Message to publish
55
+ * @param {string} topic - Topic to publish to
56
+ * @param {number} qos - Quality of Service level (0, 1, or 2)
57
+ * @param {boolean} retain - Whether to retain the message
58
+ * @returns {Promise<void>}
59
+ */
60
+ publishMessage: async function (mqttManager, message, topic, qos = 0, retain = false) {
61
+ const resolvedTopic = await this.prepareTopic(topic);
62
+ const resolvedMessage = await this.prepareMessage(message);
63
+ await mqttManager.publish(resolvedTopic, resolvedMessage, { qos, retain });
64
+ },
65
+
66
+ /**
67
+ * Publish a JSON message to a topic
68
+ * @param {object} mqttManager - MQTT manager instance
69
+ * @param {string} jsonString - JSON string to publish
70
+ * @param {string} topic - Topic to publish to
71
+ * @param {number} qos - Quality of Service level
72
+ * @param {boolean} retain - Whether to retain the message
73
+ * @returns {Promise<void>}
74
+ */
75
+ publishJsonMessage: async function (mqttManager, jsonString, topic, qos = 0, retain = false) {
76
+ const resolvedTopic = await this.prepareTopic(topic);
77
+ const resolvedJson = await this.prepareMessage(jsonString);
78
+
79
+ // Validate JSON
80
+ try {
81
+ JSON.parse(resolvedJson);
82
+ } catch (error) {
83
+ throw new Error(`Invalid JSON message: ${error.message}`);
84
+ }
85
+
86
+ await mqttManager.publish(resolvedTopic, resolvedJson, { qos, retain });
87
+ },
88
+
89
+ /**
90
+ * Wait for a message on a specific topic
91
+ * @param {object} mqttManager - MQTT manager instance
92
+ * @param {string} topic - Topic to wait for message on
93
+ * @param {number} timeoutSeconds - Timeout in seconds
94
+ * @returns {Promise<Object>} - Message object
95
+ */
96
+ waitForMessage: async function (mqttManager, topic, timeoutSeconds = 10) {
97
+ const resolvedTopic = await this.prepareTopic(topic);
98
+ return await mqttManager.waitForMessage(resolvedTopic, timeoutSeconds);
99
+ },
100
+
101
+ /**
102
+ * Get the latest message from a topic
103
+ * @param {object} mqttManager - MQTT manager instance
104
+ * @param {string} topic - Topic to get message from
105
+ * @returns {Promise<Object>} - Latest message object
106
+ */
107
+ getLatestMessage: async function (mqttManager, topic) {
108
+ const resolvedTopic = await this.prepareTopic(topic);
109
+ const message = mqttManager.getLatestMessage(resolvedTopic);
110
+
111
+ if (!message) {
112
+ throw new Error(`No messages received on topic: ${resolvedTopic}`);
113
+ }
114
+
115
+ return message;
116
+ },
117
+
118
+ /**
119
+ * Parse message as JSON
120
+ * @param {Object} messageObj - Message object from buffer
121
+ * @returns {Object} - Parsed JSON object
122
+ */
123
+ parseMessageAsJson: function (messageObj) {
124
+ try {
125
+ return JSON.parse(messageObj.message);
126
+ } catch (error) {
127
+ throw new Error(`Failed to parse message as JSON: ${error.message}`);
128
+ }
129
+ },
130
+
131
+ /**
132
+ * Validate that a message was received on a topic
133
+ * @param {object} mqttManager - MQTT manager instance
134
+ * @param {string} topic - Topic to check
135
+ * @param {number} timeoutSeconds - Timeout in seconds
136
+ * @returns {Promise<Object>} - Message object
137
+ */
138
+ validateMessageReceived: async function (mqttManager, topic, timeoutSeconds = 10) {
139
+ const message = await this.waitForMessage(mqttManager, topic, timeoutSeconds);
140
+ assert.isDefined(message, `No message received on topic: ${topic}`);
141
+ return message;
142
+ },
143
+
144
+ /**
145
+ * Validate message content equals expected value
146
+ * @param {object} mqttManager - MQTT manager instance
147
+ * @param {string} topic - Topic to check message on
148
+ * @param {string} expectedContent - Expected message content
149
+ * @returns {Promise<void>}
150
+ */
151
+ validateMessageContent: async function (mqttManager, topic, expectedContent) {
152
+ const resolvedExpected = await storage.checkForSavedVariable(expectedContent);
153
+ const message = await this.getLatestMessage(mqttManager, topic);
154
+ assert.strictEqual(
155
+ message.message,
156
+ resolvedExpected,
157
+ `Message content does not match. Expected: ${resolvedExpected}, Actual: ${message.message}`
158
+ );
159
+ },
160
+
161
+ /**
162
+ * Validate message contains a substring
163
+ * @param {object} mqttManager - MQTT manager instance
164
+ * @param {string} topic - Topic to check message on
165
+ * @param {string} substring - Substring to search for
166
+ * @returns {Promise<void>}
167
+ */
168
+ validateMessageContains: async function (mqttManager, topic, substring) {
169
+ const resolvedSubstring = await storage.checkForSavedVariable(substring);
170
+ const message = await this.getLatestMessage(mqttManager, topic);
171
+ assert.include(
172
+ message.message,
173
+ resolvedSubstring,
174
+ `Message does not contain expected substring: ${resolvedSubstring}`
175
+ );
176
+ },
177
+
178
+ /**
179
+ * Validate JSON message property has expected value
180
+ * @param {object} mqttManager - MQTT manager instance
181
+ * @param {string} topic - Topic to get message from
182
+ * @param {string} property - Property path (e.g., "data.temperature")
183
+ * @param {string} expectedValue - Expected value
184
+ * @returns {Promise<void>}
185
+ */
186
+ validateJsonProperty: async function (mqttManager, topic, property, expectedValue) {
187
+ let resolvedExpected = await storage.checkForSavedVariable(expectedValue);
188
+ resolvedExpected = helper.castPrimitiveType(resolvedExpected);
189
+
190
+ const message = await this.getLatestMessage(mqttManager, topic);
191
+ const jsonData = this.parseMessageAsJson(message);
192
+ const value = helper.getPropertyValue(jsonData, property);
193
+
194
+ assert.strictEqual(
195
+ value,
196
+ resolvedExpected,
197
+ `The value of the property "${property}" does not match: Expected: ${resolvedExpected}, Actual: ${value}`
198
+ );
199
+ },
200
+
201
+ /**
202
+ * Validate JSON message property type
203
+ * @param {object} mqttManager - MQTT manager instance
204
+ * @param {string} topic - Topic to get message from
205
+ * @param {string} property - Property path
206
+ * @param {string} type - Expected type (string, number, boolean, object, array)
207
+ * @returns {Promise<void>}
208
+ */
209
+ validateJsonPropertyType: async function (mqttManager, topic, property, type) {
210
+ const message = await this.getLatestMessage(mqttManager, topic);
211
+ const jsonData = this.parseMessageAsJson(message);
212
+ const value = helper.getPropertyValue(jsonData, property);
213
+ assert.typeOf(value, type, `Property "${property}" is not of type ${type}`);
214
+ },
215
+
216
+ /**
217
+ * Remember a property value from JSON message
218
+ * @param {object} mqttManager - MQTT manager instance
219
+ * @param {string} topic - Topic to get message from
220
+ * @param {string} property - Property path
221
+ * @param {string} variableName - Variable name to store value
222
+ * @returns {Promise<void>}
223
+ */
224
+ rememberJsonProperty: async function (mqttManager, topic, property, variableName) {
225
+ const message = await this.getLatestMessage(mqttManager, topic);
226
+ const jsonData = this.parseMessageAsJson(message);
227
+ const value = helper.getPropertyValue(jsonData, property);
228
+ await storage.iStoreVariableWithValueToTheJsonFile(value, variableName);
229
+ },
230
+
231
+ /**
232
+ * Remember the entire message content
233
+ * @param {object} mqttManager - MQTT manager instance
234
+ * @param {string} topic - Topic to get message from
235
+ * @param {string} variableName - Variable name to store message
236
+ * @returns {Promise<void>}
237
+ */
238
+ rememberMessage: async function (mqttManager, topic, variableName) {
239
+ const message = await this.getLatestMessage(mqttManager, topic);
240
+ await storage.iStoreVariableWithValueToTheJsonFile(message.message, variableName);
241
+ },
242
+
243
+ /**
244
+ * Clear message buffer for a topic
245
+ * @param {object} mqttManager - MQTT manager instance
246
+ * @param {string} topic - Topic to clear buffer for (or null for all)
247
+ * @returns {Promise<void>}
248
+ */
249
+ clearMessageBuffer: async function (mqttManager, topic = null) {
250
+ if (topic) {
251
+ const resolvedTopic = await this.prepareTopic(topic);
252
+ mqttManager.clearMessageBuffer(resolvedTopic);
253
+ } else {
254
+ mqttManager.clearMessageBuffer();
255
+ }
256
+ },
257
+
258
+ /**
259
+ * Get message count for a topic
260
+ * @param {object} mqttManager - MQTT manager instance
261
+ * @param {string} topic - Topic to get message count for
262
+ * @returns {Promise<number>} - Number of messages
263
+ */
264
+ getMessageCount: async function (mqttManager, topic) {
265
+ const resolvedTopic = await this.prepareTopic(topic);
266
+ return (await mqttManager.getMessages(resolvedTopic)).length;
267
+ },
268
+
269
+ /**
270
+ * Validate message count on a topic
271
+ * @param {object} mqttManager - MQTT manager instance
272
+ * @param {string} topic - Topic to check
273
+ * @param {number} expectedCount - Expected number of messages
274
+ * @returns {Promise<void>}
275
+ */
276
+ validateMessageCount: async function (mqttManager, topic, expectedCount) {
277
+ const resolvedTopic = await this.prepareTopic(topic);
278
+ const actualCount = await this.getMessageCount(mqttManager, resolvedTopic);
279
+ assert.strictEqual(
280
+ actualCount,
281
+ expectedCount,
282
+ `Expected ${expectedCount} messages on topic "${topic}", but found ${actualCount}`
283
+ );
284
+ },
285
+ };
@@ -10,6 +10,7 @@ module.exports = {
10
10
  iframeSteps: require('./features/app/stepDefinitions/iframeSteps'),
11
11
  ifVisibleSteps: require('./features/app/stepDefinitions/ifVisibleSteps'),
12
12
  lighthouseSteps: require('./features/app/stepDefinitions/lighthouseSteps'),
13
+ mqttSteps: require('./features/app/stepDefinitions/mqttSteps'),
13
14
  pageElements: require('./features/app/stepDefinitions/pageElements'),
14
15
  pageElementsConfig: require('./features/app/stepDefinitions/pageElementsConfig'),
15
16
  pageElementsJson: require('./features/app/stepDefinitions/pageElementsJson'),