Compare commits

..
Author SHA1 Message Date
Thomas Nordquist f6856d66cc chore: update eclipse server url 2024-05-24 22:27:24 +02:00
Thomas Nordquist ae0ce79e26 Merge pull request #794 from thomasnordquist/feat/use-tahu-for-sparkplug-decoding
Feat/use tahu for sparkplug decoding
2024-05-24 10:12:21 +02:00
Björn Dalfors c452b9f417 add sparkplug messages to demovideo 2024-05-20 16:42:54 +02:00
Björn Dalfors b04f5dee16 feat: use tahu for sparkplug decoding 2024-05-18 21:48:14 +02:00
Björn Dalfors 7617430a3f fix regex 2024-05-18 21:42:25 +02:00
Björn Dalfors a346c48d3e refine sparkplug detection 2024-05-17 09:08:34 +02:00
Björn Dalfors 8a2c39ba8e fix: use sparkplugb decoder only for spBv1.0 topic 2024-05-15 15:24:30 +02:00
Thomas Nordquist 65b86ac5f6 chore: remove precondition 2024-05-11 21:55:11 +02:00
Thomas Nordquist 4ead740982 chore: allow manual update of website 2024-05-11 21:54:20 +02:00
Thomas Nordquist ddaf06b682 chore: remove precondition 2024-05-11 21:51:38 +02:00
Thomas Nordquist 9e18c4db4e chore: prevent new builds being uploaded without a release 2024-05-11 21:49:27 +02:00
Thomas Nordquist ee783f15c0 chore: move website update to separate release action 2024-05-11 21:44:28 +02:00
Thomas Nordquist 0c31843ed5 chore: reduce builds 2024-05-11 21:29:53 +02:00
Thomas Nordquist ffc93d20d8 chore: retry release beta 5 2024-05-11 21:17:47 +02:00
Thomas Nordquist 3c62da7e19 chore: only build macos dmg 2024-05-11 21:12:27 +02:00
Thomas Nordquist 6846dbbb28 chore: update website with new version 2024-05-11 21:10:57 +02:00
semantic-release-bot 270e3e3ead chore(release): 0.4.0-beta.5 [skip ci] 2024-05-11 15:47:26 +00:00
Thomas Nordquist 0d73b0f519 feat: test to trigger beta release 5 2024-05-11 17:46:23 +02:00
semantic-release-bot ee83022d19 chore(release): 0.4.0-beta.4 [skip ci] 2024-05-11 15:35:58 +00:00
Thomas Nordquist f27467ed97 fix: add macos 2024-05-11 17:34:48 +02:00
semantic-release-bot ddbf3d9f1f chore(release): 0.4.0-beta.3 [skip ci] 2024-05-11 14:19:25 +00:00
Thomas Nordquist 60bbbc54d7 chore: use GithubApp to bypass branch protection 2024-05-11 16:07:16 +02:00
Thomas Nordquist 538aa7dc29 chore: use custom token fo releases 2024-05-11 15:31:08 +02:00
Thomas Nordquist b37af291cc feat: skip package release until github and semantic-release are in sync 2024-05-11 14:42:17 +02:00
Thomas Nordquist 333a1bcabe fix: fix repository 2024-05-11 14:39:34 +02:00
Thomas Nordquist c2404a7eb6 Merge pull request #779 from thomasnordquist/dependabot/npm_and_yarn/ejs-3.1.10
Bump ejs from 3.1.9 to 3.1.10
2024-05-11 12:18:00 +02:00
dependabot[bot] bb602cd28e Bump ejs from 3.1.9 to 3.1.10
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-11 10:11:20 +00:00
Thomas Nordquist 898fd3896c Merge pull request #786 from thomasnordquist/tnordquist/test-ci
chore: add ci / cd piplelines as github actions
2024-05-11 12:10:36 +02:00
Thomas Nordquist 7fb0483889 chore: document release 2024-05-11 12:03:01 +02:00
Thomas Nordquist 7d77110c1f chore: upload test video as test artifact 2024-05-11 11:00:31 +02:00
Thomas Nordquist a7ce6b4419 chore: upload test video as test artifact 2024-05-10 22:30:02 +02:00
Thomas Nordquist 204f6dbcde chore: fix docker context 2024-05-10 22:17:39 +02:00
Thomas Nordquist 7fa997087f chore: build docker test image 2024-05-10 21:15:04 +02:00
semantic-release-bot a96b08eaaa chore(release): 0.4.0-beta.2 [skip ci] 2024-05-10 18:12:27 +00:00
Thomas Nordquist c9c997d13a fix: trigger build 2024-05-10 20:04:31 +02:00
semantic-release-bot 6f3e0f62e1 chore(release): 0.4.0-beta.1 [skip ci] 2024-05-10 18:03:44 +00:00
Thomas Nordquist 824b39637c chore: use newer node version 2024-05-10 19:56:01 +02:00
Thomas Nordquist 0100b2988a chore: dry-run 2024-05-10 18:52:25 +02:00
Thomas Nordquist 0e72329c77 fix: update semantic-release 2024-05-10 18:48:38 +02:00
Thomas Nordquist 9b7c3b8e9c feat: use semantic-release 2024-05-10 18:46:06 +02:00
Thomas Nordquist b9a5a5f1d8 add semantic release 2024-05-10 18:05:41 +02:00
23 changed files with 3158 additions and 577 deletions
+29
View File
@@ -0,0 +1,29 @@
on:
push:
branches:
- master
- release
- beta
paths:
- Dockerfile
- .github
jobs:
create-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
@@ -3,14 +3,15 @@ name: Build
on:
push:
branches:
- master
- release
- beta
concurrency:
group: ${{ github.ref }}
cancel-in-progress: false
jobs:
platform_matrix:
build:
strategy:
matrix:
build:
@@ -18,8 +19,8 @@ jobs:
task: linux
- os: windows-latest
task: win
# - os: macos-latest
# task: mac
- os: macos-latest
task: mac
runs-on: ${{ matrix.build.os }}
steps:
- if: matrix.build.os == 'ubuntu-latest'
@@ -27,12 +28,26 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- run: npm install -g yarn
- run: yarn
- id: create_token # get ReleaseBot access token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.RELEASE_BOT_APP_ID }}
private_key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v4
id: semantic # Need an `id` for output variables
env:
GITHUB_TOKEN: ${{ steps.create_token.outputs.token }}
- run: yarn build
if: steps.semantic.outputs.new_release_published == 'true'
- run: yarn prepare-release
if: steps.semantic.outputs.new_release_published == 'true'
- run: yarn package ${{ matrix.build.task }}
if: steps.semantic.outputs.new_release_published == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
+38
View File
@@ -0,0 +1,38 @@
on:
pull_request_target: # Use pull_request_target
branches: [master, beta, release]
jobs:
test:
runs-on: ubuntu-latest
container:
image: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
volumes:
- ./:/app
options: --user root
steps:
- uses: actions/checkout@v4
- name: Install Packages
run: yarn install --frozen-lockfile
- name: Build
run: yarn build
- name: Test
run: yarn test
- name: UI-Test
run: yarn ui-test
- name: Post-processing
run: ./scripts/prepareVideo.sh
- uses: hkusu/s3-upload-action@v2
id: upload # specify some ID for use in subsequent steps
with:
aws-access-key-id: ${{ vars.AWS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: 'eu-central-1'
aws-bucket: ${{ vars.AWS_BUCKET }}
file-path: './ui-test.gif'
content-type: image/gif
output-file-url: 'true'
- name: Show URL
run: echo '${{ steps.upload.outputs.file-url }}'
id: artifact-upload-step
- run: echo '![test](${{ steps.upload.outputs.file-url }})' >> $GITHUB_STEP_SUMMARY
+17
View File
@@ -0,0 +1,17 @@
name: Update Website
on: [release, workflow_dispatch]
jobs:
update-website:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: gh-pages
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npm run readme
- uses: stefanzweifel/git-auto-commit-action@v5
-1
View File
@@ -1 +0,0 @@
engine-strict=true
+30
View File
@@ -0,0 +1,30 @@
{
"branches": [
"release",
{
"name": "beta",
"prerelease": true
}
],
repositoryUrl: "git@github.com:thomasnordquist/MQTT-Explorer.git",
"plugins": [
"@semantic-release/commit-analyzer",
"semantic-release-export-data",
"@semantic-release/changelog",
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": [
"package.json",
"yarn.lock"
]
}
]
]
}
-46
View File
@@ -1,46 +0,0 @@
language: node_js
services:
- xvfb
cache:
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
- $HOME/.npm/_prebuilds
node_js:
- "10"
os:
- linux
- osx
osx_image: xcode10.2
dist: bionic
services:
- docker
install:
- yarn install --frozen-lockfile
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update && sudo apt-get -y install snap squashfs-tools && sudo snap install snapcraft --classic; fi;
script:
- yarn run build
- yarn lint
- yarn test
- export TRAVIS_BUILD_NUMBER="" # Override travis build number since it is uses for tagging the binary version https://github.com/electron-userland/electron-builder/issues/3730
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker run -e GH_TOKEN=$GH_TOKEN -e GIT_TAG=$TRAVIS_TAG --rm -v `pwd`:/app thomasnordquist/ui-test-recording-env sh -c "cd app && docker/testMounted.sh"; fi
- if [[ "$TRAVIS_TAG" != "" ]]; then yarn run prepare-release; fi
- |
if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then
openssl aes-256-cbc -d -in res/snapstore-credentials.enc -out credentials -k $SNAPSTORE_CREDENTIALS_DECRYPTION_KEY;
snapcraft login --with credentials;
rm credentials;
yarn run package linux;
fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then unset CSC_LINK; yarn run package -- win; fi
+2 -3
View File
@@ -1,7 +1,7 @@
FROM node:19.8.1-bullseye-slim
FROM node:20
RUN DEBIAN_FRONTEND="noninteractive" apt-get update \
&& apt-get install -y --no-install-recommends nano ffmpeg xvfb git-core tmux locales mosquitto x11vnc
&& apt-get install -y --no-install-recommends ca-certificates nano ffmpeg xvfb git-core tmux locales mosquitto x11vnc
RUN apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3
# Generate locales for TMUX
@@ -12,6 +12,5 @@ ENV LC_ALL en_US.UTF-8
CMD /bin/bash
COPY cloneBuildAndTest.sh ./
VOLUME /app
EXPOSE 5900
+14 -3
View File
@@ -6,11 +6,12 @@
[![Build status](https://ci.appveyor.com/api/projects/status/c35tkm29rm4m5364/branch/master?svg=true)](https://ci.appveyor.com/project/thomasnordquist/mqtt-explorer/branch/master)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/47b26e03fce543ceac7914214482334a)](https://app.codacy.com/app/thomasnordquist/MQTT-Explorer?utm_source=github.com&utm_medium=referral&utm_content=thomasnordquist/MQTT-Explorer&utm_campaign=Badge_Grade_Dashboard)
| | | |
|:---:|:---:|:---:|
|[![screen_composite](https://mqtt-explorer.com/img/screen-composite_small.png)](https://mqtt-explorer.com/img/screen-composite.png)|[![screen2_small](https://mqtt-explorer.com/img/screen2_small.png)](https://mqtt-explorer.com/img/screen2.png)|[![screen3_small](https://mqtt-explorer.com/img/screen3_small.png)](https://mqtt-explorer.com/img/screen3.png)|
| | | |
| :---------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------: |
| [![screen_composite](https://mqtt-explorer.com/img/screen-composite_small.png)](https://mqtt-explorer.com/img/screen-composite.png) | [![screen2_small](https://mqtt-explorer.com/img/screen2_small.png)](https://mqtt-explorer.com/img/screen2.png) | [![screen3_small](https://mqtt-explorer.com/img/screen3_small.png)](https://mqtt-explorer.com/img/screen3.png) |
# The App has moved to [mqtt-explorer.com](https://mqtt-explorer.com)
MQTT Explorer is a comprehensive and easy-to-use MQTT Client.
Downloads can be found at the link above.
@@ -29,6 +30,7 @@ yarn start
## Develop
Launch Application
```bash
npm install -g yarn
yarn
@@ -63,6 +65,15 @@ npm run build
node dist/src/spec/webdriverio.js
```
## Create a release
Create a PR to `release` branch.
There needs to be a "feat: some new feature" or "fix: some bugfix" commit for a new release to be created
## Create a beta release
Create a PR to `beta` branch. A "feat" or "fix" commit is necessary to create a new version.
## Write docs
```
+4 -4
View File
@@ -77,11 +77,11 @@ export function createEmptyConnection(): ConnectionOptions {
export function makeDefaultConnections() {
return {
// remember: there was also iot.eclipse.org once
'mqtt.eclipse.org': {
'mqtt.eclipseprojects.io': {
...createEmptyConnection(),
id: 'mqtt.eclipse.org',
name: 'mqtt.eclipse.org',
host: 'mqtt.eclipse.org',
id: 'mqtt.eclipseprojects.io',
name: 'mqtt.eclipseprojects.io',
host: 'mqtt.eclipseprojects.io',
},
'test.mosquitto.org': {
...createEmptyConnection(),
-204
View File
@@ -1,204 +0,0 @@
/* spell-checker: disable */
const protocol = `
syntax = "proto2";
//
// To compile:
// cd client_libraries/java
// protoc --proto_path=../../ --java_out=src/main/java ../../sparkplug_b.proto
//
package com.cirruslink.sparkplug.protobuf;
option java_package = "com.cirruslink.sparkplug.protobuf";
option java_outer_classname = "SparkplugBProto";
message Payload {
/*
// Indexes of Data Types
// Unknown placeholder for future expansion.
Unknown = 0;
// Basic Types
Int8 = 1;
Int16 = 2;
Int32 = 3;
Int64 = 4;
UInt8 = 5;
UInt16 = 6;
UInt32 = 7;
UInt64 = 8;
Float = 9;
Double = 10;
Boolean = 11;
String = 12;
DateTime = 13;
Text = 14;
// Additional Metric Types
UUID = 15;
DataSet = 16;
Bytes = 17;
File = 18;
Template = 19;
// Additional PropertyValue Types
PropertySet = 20;
PropertySetList = 21;
*/
message Template {
message Parameter {
optional string name = 1;
optional uint32 type = 2;
oneof value {
uint32 int_value = 3;
uint64 long_value = 4;
float float_value = 5;
double double_value = 6;
bool boolean_value = 7;
string string_value = 8;
ParameterValueExtension extension_value = 9;
}
message ParameterValueExtension {
extensions 1 to max;
}
}
optional string version = 1; // The version of the Template to prevent mismatches
repeated Metric metrics = 2; // Each metric is the name of the metric and the datatype of the member but does not contain a value
repeated Parameter parameters = 3;
optional string template_ref = 4; // Reference to a template if this is extending a Template or an instance - must exist if an instance
optional bool is_definition = 5;
extensions 6 to max;
}
message DataSet {
message DataSetValue {
oneof value {
uint32 int_value = 1;
uint64 long_value = 2;
float float_value = 3;
double double_value = 4;
bool boolean_value = 5;
string string_value = 6;
DataSetValueExtension extension_value = 7;
}
message DataSetValueExtension {
extensions 1 to max;
}
}
message Row {
repeated DataSetValue elements = 1;
extensions 2 to max; // For third party extensions
}
optional uint64 num_of_columns = 1;
repeated string columns = 2;
repeated uint32 types = 3;
repeated Row rows = 4;
extensions 5 to max; // For third party extensions
}
message PropertyValue {
optional uint32 type = 1;
optional bool is_null = 2;
oneof value {
uint32 int_value = 3;
uint64 long_value = 4;
float float_value = 5;
double double_value = 6;
bool boolean_value = 7;
string string_value = 8;
PropertySet propertyset_value = 9;
PropertySetList propertysets_value = 10; // List of Property Values
PropertyValueExtension extension_value = 11;
}
message PropertyValueExtension {
extensions 1 to max;
}
}
message PropertySet {
repeated string keys = 1; // Names of the properties
repeated PropertyValue values = 2;
extensions 3 to max;
}
message PropertySetList {
repeated PropertySet propertyset = 1;
extensions 2 to max;
}
message MetaData {
// Bytes specific metadata
optional bool is_multi_part = 1;
// General metadata
optional string content_type = 2; // Content/Media type
optional uint64 size = 3; // File size, String size, Multi-part size, etc
optional uint64 seq = 4; // Sequence number for multi-part messages
// File metadata
optional string file_name = 5; // File name
optional string file_type = 6; // File type (i.e. xml, json, txt, cpp, etc)
optional string md5 = 7; // md5 of data
// Catchalls and future expansion
optional string description = 8; // Could be anything such as json or xml of custom properties
extensions 9 to max;
}
message Metric {
optional string name = 1; // Metric name - should only be included on birth
optional uint64 alias = 2; // Metric alias - tied to name on birth and included in all later DATA messages
optional uint64 timestamp = 3; // Timestamp associated with data acquisition time
optional uint32 datatype = 4; // DataType of the metric/tag value
optional bool is_historical = 5; // If this is historical data and should not update real time tag
optional bool is_transient = 6; // Tells consuming clients such as MQTT Engine to not store this as a tag
optional bool is_null = 7; // If this is null - explicitly say so rather than using -1, false, etc for some datatypes.
optional MetaData metadata = 8; // Metadata for the payload
optional PropertySet properties = 9;
oneof value {
uint32 int_value = 10;
uint64 long_value = 11;
float float_value = 12;
double double_value = 13;
bool boolean_value = 14;
string string_value = 15;
bytes bytes_value = 16; // Bytes, File
DataSet dataset_value = 17;
Template template_value = 18;
MetricValueExtension extension_value = 19;
}
message MetricValueExtension {
extensions 1 to max;
}
}
optional uint64 timestamp = 1; // Timestamp at message sending time
repeated Metric metrics = 2; // Repeated forever - no limit in Google Protobufs
optional uint64 seq = 3; // Sequence number
optional string uuid = 4; // UUID to track message type in terms of schema definitions
optional bytes body = 5; // To optionally bypass the whole definition above
extensions 6 to max; // For third party extensions
}
`
/* spell-checker: enable */
export default protocol
+9 -12
View File
@@ -1,24 +1,21 @@
// cSpell:words protobuf
import * as protobuf from 'protobufjs'
import protocol from './sparkplugb.proto'
import { Base64Message } from './Base64Message'
import { Decoder } from './Decoder'
const root = protobuf.parse(protocol).root
/* cspell:disable-next-line */
export let SparkplugPayload = root.lookupType('com.cirruslink.sparkplug.protobuf.Payload')
import { get } from 'sparkplug-payload'
var sparkplug = get("spBv1.0")
export const SparkplugDecoder = {
decode(input: Buffer): Base64Message | undefined {
decode(input: Buffer): Base64Message {
try {
const message = Base64Message.fromString(
JSON.stringify(SparkplugPayload.toObject(SparkplugPayload.decode(new Uint8Array(input))))
const message = Base64Message.fromString(JSON.stringify(
// @ts-ignore
sparkplug.decodePayload(new Uint8Array(input)))
)
message.decoder = Decoder.SPARKPLUG
return message
} catch {
// ignore
const message = Base64Message.fromString("Failed to decode sparkplugb payload")
message.decoder = Decoder.NONE
return message
}
return undefined
},
}
+9 -1
View File
@@ -47,9 +47,17 @@ export class ConnectionManager {
buffer = buffer.slice(0, 20000)
}
let decoded_payload = null
// spell-checker: disable-next-line
if (topic.match(/^spBv1\.0\/[^/]+\/[ND](DATA|CMD|DEATH|BIRTH)\/[^/]+(\/[^/]+)?$/u)) {
decoded_payload = SparkplugDecoder.decode(buffer)
} else {
decoded_payload = Base64Message.fromBuffer(buffer)
}
backendEvents.emit(messageEvent, {
topic,
payload: SparkplugDecoder.decode(buffer) ?? Base64Message.fromBuffer(buffer),
payload: decoded_payload,
qos: packet.qos,
retain: packet.retain,
messageId: packet.messageId,
-4
View File
@@ -1,10 +1,6 @@
#!/bin/bash
set -e
git clone https://github.com/thomasnordquist/MQTT-Explorer.git /app
cd /app
git checkout travis-ui-tests
yarn install --frozen-lockfile
yarn build
+10 -3
View File
@@ -1,11 +1,12 @@
{
"name": "MQTT-Explorer",
"version": "0.4.0-beta4",
"version": "0.4.0-beta.5",
"description": "Explore your message queues",
"main": "dist/src/electron.js",
"engines": {
"node": ">=18"
},
"private": "true",
"scripts": {
"start": "electron .",
"test": "yarn test:app && yarn test:backend",
@@ -78,6 +79,9 @@
"@babel/runtime": "^7.17.2",
"@cspell/dict-typescript": "^3.1.2",
"@electron/notarize": "^2.3.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^12.0.0",
"@semantic-release/git": "^10.0.1",
"@types/chai": "^4.1.7",
"@types/fs-extra": "8",
"@types/lowdb": "^1.0.6",
@@ -100,7 +104,10 @@
"playwright": "^1.43.0",
"prettier": "^3.2.5",
"redux-thunk": "^2.3.0",
"semantic-release": "^23.0.8",
"semantic-release-export-data": "^1.0.1",
"source-map-support": "^0.5.9",
"sparkplug-client": "^3.2.4",
"ts-node": "^10.9.2",
"tslint": "^6.1.3",
"tslint-config-airbnb": "^5.11.2",
@@ -120,9 +127,9 @@
"lowdb": "^1.0.0",
"mime": "^2.4.4",
"mqtt": "^4.3.6",
"protobufjs": "^6.11.4",
"sha1": "^1.1.1",
"sparkplug-payload": "^1.0.3",
"uuid": "^8.3.2",
"yarn-run-all": "^3.1.1"
}
}
}
+24 -14
View File
@@ -7,12 +7,30 @@ const linuxAppImage: builder.CliOptions = {
x64: true,
ia32: false,
armv7l: true,
arm64: false,
arm64: true,
projectDir: './build/clean',
publish: 'always',
}
const linuxSnap: builder.CliOptions = {
x64: true,
ia32: false,
armv7l: false, // not supported to build on x64
arm64: false, // not supported to build on x64
projectDir: './build/clean',
publish: 'always',
}
const linuxDeb: builder.CliOptions = {
x64: true,
ia32: false,
armv7l: true,
arm64: true,
projectDir: './build/clean',
publish: 'always',
}
const winPortable: builder.CliOptions = {
x64: true,
ia32: false,
armv7l: false,
@@ -21,18 +39,9 @@ const linuxSnap: builder.CliOptions = {
publish: 'always',
}
const winPortable: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
projectDir: './build/clean',
publish: 'always',
}
const winNsis: builder.CliOptions = {
x64: true,
ia32: true,
ia32: false,
armv7l: false,
arm64: false,
projectDir: './build/clean',
@@ -41,7 +50,7 @@ const winNsis: builder.CliOptions = {
const winAppx: builder.CliOptions = {
x64: true,
ia32: true,
ia32: false,
armv7l: false,
arm64: false,
projectDir: './build/clean',
@@ -69,11 +78,12 @@ async function executeBuild() {
case 'linux':
await buildWithOptions(linuxAppImage, { platform: 'linux', package: 'AppImage' })
await buildWithOptions(linuxSnap, { platform: 'linux', package: 'snap' })
await buildWithOptions(linuxDeb, { platform: 'linux', package: 'deb' })
break
case 'mac':
await buildWithOptions(mac, { platform: 'mac', package: 'dmg' })
await buildWithOptions(mac, { platform: 'mac', package: 'mas' })
await buildWithOptions(mac, { platform: 'mac', package: 'zip' })
// await buildWithOptions(mac, { platform: 'mac', package: 'mas' })
// await buildWithOptions(mac, { platform: 'mac', package: 'zip' })
break
default:
await buildWithOptions({ ...mac, projectDir: '' }, { platform: 'mac', package: 'mas-dev' })
-197
View File
@@ -1,197 +0,0 @@
syntax = "proto2";
//
// To compile:
// cd client_libraries/java
// protoc --proto_path=../../ --java_out=src/main/java ../../sparkplug_b.proto
//
package com.cirruslink.sparkplug.protobuf;
option java_package = "com.cirruslink.sparkplug.protobuf";
option java_outer_classname = "SparkplugBProto";
message Payload {
/*
// Indexes of Data Types
// Unknown placeholder for future expansion.
Unknown = 0;
// Basic Types
Int8 = 1;
Int16 = 2;
Int32 = 3;
Int64 = 4;
UInt8 = 5;
UInt16 = 6;
UInt32 = 7;
UInt64 = 8;
Float = 9;
Double = 10;
Boolean = 11;
String = 12;
DateTime = 13;
Text = 14;
// Additional Metric Types
UUID = 15;
DataSet = 16;
Bytes = 17;
File = 18;
Template = 19;
// Additional PropertyValue Types
PropertySet = 20;
PropertySetList = 21;
*/
message Template {
message Parameter {
optional string name = 1;
optional uint32 type = 2;
oneof value {
uint32 int_value = 3;
uint64 long_value = 4;
float float_value = 5;
double double_value = 6;
bool boolean_value = 7;
string string_value = 8;
ParameterValueExtension extension_value = 9;
}
message ParameterValueExtension {
extensions 1 to max;
}
}
optional string version = 1; // The version of the Template to prevent mismatches
repeated Metric metrics = 2; // Each metric is the name of the metric and the datatype of the member but does not contain a value
repeated Parameter parameters = 3;
optional string template_ref = 4; // Reference to a template if this is extending a Template or an instance - must exist if an instance
optional bool is_definition = 5;
extensions 6 to max;
}
message DataSet {
message DataSetValue {
oneof value {
uint32 int_value = 1;
uint64 long_value = 2;
float float_value = 3;
double double_value = 4;
bool boolean_value = 5;
string string_value = 6;
DataSetValueExtension extension_value = 7;
}
message DataSetValueExtension {
extensions 1 to max;
}
}
message Row {
repeated DataSetValue elements = 1;
extensions 2 to max; // For third party extensions
}
optional uint64 num_of_columns = 1;
repeated string columns = 2;
repeated uint32 types = 3;
repeated Row rows = 4;
extensions 5 to max; // For third party extensions
}
message PropertyValue {
optional uint32 type = 1;
optional bool is_null = 2;
oneof value {
uint32 int_value = 3;
uint64 long_value = 4;
float float_value = 5;
double double_value = 6;
bool boolean_value = 7;
string string_value = 8;
PropertySet propertyset_value = 9;
PropertySetList propertysets_value = 10; // List of Property Values
PropertyValueExtension extension_value = 11;
}
message PropertyValueExtension {
extensions 1 to max;
}
}
message PropertySet {
repeated string keys = 1; // Names of the properties
repeated PropertyValue values = 2;
extensions 3 to max;
}
message PropertySetList {
repeated PropertySet propertyset = 1;
extensions 2 to max;
}
message MetaData {
// Bytes specific metadata
optional bool is_multi_part = 1;
// General metadata
optional string content_type = 2; // Content/Media type
optional uint64 size = 3; // File size, String size, Multi-part size, etc
optional uint64 seq = 4; // Sequence number for multi-part messages
// File metadata
optional string file_name = 5; // File name
optional string file_type = 6; // File type (i.e. xml, json, txt, cpp, etc)
optional string md5 = 7; // md5 of data
// Catchalls and future expansion
optional string description = 8; // Could be anything such as json or xml of custom properties
extensions 9 to max;
}
message Metric {
optional string name = 1; // Metric name - should only be included on birth
optional uint64 alias = 2; // Metric alias - tied to name on birth and included in all later DATA messages
optional uint64 timestamp = 3; // Timestamp associated with data acquisition time
optional uint32 datatype = 4; // DataType of the metric/tag value
optional bool is_historical = 5; // If this is historical data and should not update real time tag
optional bool is_transient = 6; // Tells consuming clients such as MQTT Engine to not store this as a tag
optional bool is_null = 7; // If this is null - explicitly say so rather than using -1, false, etc for some datatypes.
optional MetaData metadata = 8; // Metadata for the payload
optional PropertySet properties = 9;
oneof value {
uint32 int_value = 10;
uint64 long_value = 11;
float float_value = 12;
double double_value = 13;
bool boolean_value = 14;
string string_value = 15;
bytes bytes_value = 16; // Bytes, File
DataSet dataset_value = 17;
Template template_value = 18;
MetricValueExtension extension_value = 19;
}
message MetricValueExtension {
extensions 1 to max;
}
}
optional uint64 timestamp = 1; // Timestamp at message sending time
repeated Metric metrics = 2; // Repeated forever - no limit in Google Protobufs
optional uint64 seq = 3; // Sequence number
optional string uuid = 4; // UUID to track message type in terms of schema definitions
optional bytes body = 5; // To optionally bypass the whole definition above
extensions 6 to max; // For third party extensions
}
+232
View File
@@ -0,0 +1,232 @@
/********************************************************************************
* Copyright (c) 2016-2018 Cirrus Link Solutions and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Cirrus Link Solutions - initial implementation
********************************************************************************/
var SparkplugClient = require('sparkplug-client')
/*
* Main sample function which includes the run() function for running the sample
*/
var sample = (function () {
var config = {
serverUrl: 'tcp://127.0.0.1:1883',
username: '',
password: '',
groupId: 'Sparkplug Devices',
edgeNode: 'JavaScript Edge Node',
clientId: 'JavaScriptSimpleEdgeNode',
version: 'spBv1.0',
},
hwVersion = 'Emulated Hardware',
swVersion = 'v1.0.0',
deviceId = 'Emulated Device',
sparkPlugClient,
publishPeriod = 5000,
// Generates a random integer
randomInt = function () {
return 1 + Math.floor(Math.random() * 10)
},
// Get BIRTH payload for the edge node
getNodeBirthPayload = function () {
return {
timestamp: new Date().getTime(),
metrics: [
{
name: 'Node Control/Rebirth',
type: 'boolean',
value: false,
},
{
name: 'Template1',
type: 'template',
value: {
isDefinition: true,
metrics: [
{ name: 'myBool', value: false, type: 'boolean' },
{ name: 'myInt', value: 0, type: 'int' },
],
parameters: [
{
name: 'param1',
type: 'string',
value: 'value1',
},
],
},
},
],
}
},
// Get BIRTH payload for the device
getDeviceBirthPayload = function () {
return {
timestamp: new Date().getTime(),
metrics: [
{ name: 'my_boolean', value: Math.random() > 0.5, type: 'boolean' },
{ name: 'my_double', value: Math.random() * 0.123456789, type: 'double' },
{ name: 'my_float', value: Math.random() * 0.123, type: 'float' },
{ name: 'my_int', value: randomInt(), type: 'int' },
{ name: 'my_long', value: randomInt() * 214748364700, type: 'long' },
{ name: 'Inputs/0', value: true, type: 'boolean' },
{ name: 'Inputs/1', value: 0, type: 'int' },
{ name: 'Inputs/2', value: 1.23, type: 'float' },
{ name: 'Outputs/0', value: true, type: 'boolean' },
{ name: 'Outputs/1', value: 0, type: 'int' },
{ name: 'Outputs/2', value: 1.23, type: 'float' },
{ name: 'Properties/hw_version', value: hwVersion, type: 'string' },
{ name: 'Properties/sw_version', value: swVersion, type: 'string' },
{
name: 'my_dataset',
type: 'dataset',
value: {
numOfColumns: 2,
types: ['string', 'string'],
columns: ['str1', 'str2'],
rows: [
['x', 'a'],
['y', 'b'],
],
},
},
{
name: 'TemplateInstance1',
type: 'template',
value: {
templateRef: 'Template1',
isDefinition: false,
metrics: [
{ name: 'myBool', value: true, type: 'boolean' },
{ name: 'myInt', value: 100, type: 'int' },
],
parameters: [
{
name: 'param1',
type: 'string',
value: 'value2',
},
],
},
},
],
}
},
// Get data payload for the device
getDataPayload = function () {
return {
timestamp: new Date().getTime(),
metrics: [
{ name: 'my_boolean', value: Math.random() > 0.5, type: 'boolean' },
{ name: 'my_double', value: Math.random() * 0.123456789, type: 'double' },
{ name: 'my_float', value: Math.random() * 0.123, type: 'float' },
{ name: 'my_int', value: randomInt(), type: 'int' },
{ name: 'my_long', value: randomInt() * 214748364700, type: 'long' },
],
}
},
// Runs the sample
run = function () {
// Create the SparkplugClient
sparkplugClient = SparkplugClient.newClient(config)
// Create Incoming Message Handler
sparkplugClient.on('message', function (topic, payload) {
console.log(topic, payload)
})
// Create 'birth' handler
sparkplugClient.on('birth', function () {
// Publish Node BIRTH certificate
sparkplugClient.publishNodeBirth(getNodeBirthPayload())
// Publish Device BIRTH certificate
sparkplugClient.publishDeviceBirth(deviceId, getDeviceBirthPayload())
})
// Create node command handler
sparkplugClient.on('ncmd', function (payload) {
var timestamp = payload.timestamp,
metrics = payload.metrics
if (metrics !== undefined && metrics !== null) {
for (var i = 0; i < metrics.length; i++) {
var metric = metrics[i]
if (metric.name == 'Node Control/Rebirth' && metric.value) {
console.log("Received 'Rebirth' command")
// Publish Node BIRTH certificate
sparkplugClient.publishNodeBirth(getNodeBirthPayload())
// Publish Device BIRTH certificate
sparkplugClient.publishDeviceBirth(deviceId, getDeviceBirthPayload())
}
}
}
})
// Create device command handler
sparkplugClient.on('dcmd', function (deviceId, payload) {
var timestamp = payload.timestamp,
metrics = payload.metrics,
inboundMetricMap = {},
outboundMetric = [],
outboundPayload
console.log('Command recevied for device ' + deviceId)
// Loop over the metrics and store them in a map
if (metrics !== undefined && metrics !== null) {
for (var i = 0; i < metrics.length; i++) {
var metric = metrics[i]
inboundMetricMap[metric.name] = metric.value
}
}
if (inboundMetricMap['Outputs/0'] !== undefined && inboundMetricMap['Outputs/0'] !== null) {
console.log('Outputs/0: ' + inboundMetricMap['Outputs/0'])
outboundMetric.push({ name: 'Inputs/0', value: inboundMetricMap['Outputs/0'], type: 'boolean' })
outboundMetric.push({ name: 'Outputs/0', value: inboundMetricMap['Outputs/0'], type: 'boolean' })
console.log('Updated value for Inputs/0 ' + inboundMetricMap['Outputs/0'])
} else if (inboundMetricMap['Outputs/1'] !== undefined && inboundMetricMap['Outputs/1'] !== null) {
console.log('Outputs/1: ' + inboundMetricMap['Outputs/1'])
outboundMetric.push({ name: 'Inputs/1', value: inboundMetricMap['Outputs/1'], type: 'int' })
outboundMetric.push({ name: 'Outputs/1', value: inboundMetricMap['Outputs/1'], type: 'int' })
console.log('Updated value for Inputs/1 ' + inboundMetricMap['Outputs/1'])
} else if (inboundMetricMap['Outputs/2'] !== undefined && inboundMetricMap['Outputs/2'] !== null) {
console.log('Outputs/2: ' + inboundMetricMap['Outputs/2'])
outboundMetric.push({ name: 'Inputs/2', value: inboundMetricMap['Outputs/2'], type: 'float' })
outboundMetric.push({ name: 'Outputs/2', value: inboundMetricMap['Outputs/2'], type: 'float' })
console.log('Updated value for Inputs/2 ' + inboundMetricMap['Outputs/2'])
}
outboundPayload = {
timestamp: new Date().getTime(),
metrics: outboundMetric,
}
// Publish device data
sparkplugClient.publishDeviceData(deviceId, outboundPayload)
})
for (var i = 1; i < 101; i++) {
// Set up a device data publish for i*publishPeriod milliseconds from now
setTimeout(function () {
// Publish device data
sparkplugClient.publishDeviceData(deviceId, getDataPayload())
// End the client connection after the last publish
if (i === 100) {
sparkplugClient.stop()
}
}, i * publishPeriod)
}
}
return { run: run }
})()
// Run the sample
sample.run()
-3
View File
@@ -59,7 +59,4 @@ tmux send-keys -t record q
# Ensure video is written
sleep 5
# Process the video
./scripts/prepareVideo.sh
exit $TEST_EXIT_CODE
+3 -1
View File
@@ -5,6 +5,7 @@ import * as path from 'path'
import { ElectronApplication, _electron as electron } from 'playwright'
import mockMqtt, { stop as stopMqtt } from './mock-mqtt'
import { default as MockSparkplug } from './mock-sparkplugb'
import { clearOldTopics } from './scenarios/clearOldTopics'
import { clearSearch, searchTree } from './scenarios/searchTree'
import { clickOnHistory, createFakeMousePointer, hideText, showText, sleep } from './util'
@@ -64,6 +65,7 @@ async function doStuff() {
const scenes = new SceneBuilder()
await scenes.record('connect', async () => {
await connectTo('127.0.0.1', page)
await MockSparkplug.run() // Start sparkplug client after connect or birth topics will be missed
await sleep(1000)
})
@@ -142,7 +144,7 @@ async function doStuff() {
})
stopMqtt()
console.log('Stopped mqtt')
console.log('Stopped mqtt client')
cleanUp(scenes, electronApp)
}
+249
View File
@@ -0,0 +1,249 @@
/********************************************************************************
* Copyright (c) 2016-2018 Cirrus Link Solutions and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Cirrus Link Solutions - initial implementation
********************************************************************************/
import * as SparkplugClient from 'sparkplug-client'
import type { UPayload } from 'sparkplug-client'
import type { UMetric } from 'sparkplug-payload/lib/sparkplugbpayload'
/*
* Main sample function which includes the run() function for running the sample
*/
export interface MockSparkplugClient {
stop: () => void
}
var sample = (function () {
var config = {
serverUrl: 'tcp://127.0.0.1:1883',
username: '',
password: '',
groupId: 'Sparkplug Devices',
edgeNode: 'JavaScript Edge Node',
clientId: 'JavaScriptSimpleEdgeNode',
version: 'spBv1.0',
},
hwVersion = 'Emulated Hardware',
swVersion = 'v1.0.0',
deviceId = 'Emulated Device',
sparkPlugClient,
publishPeriod = 5000,
// Generates a random integer
randomInt = function () {
return 1 + Math.floor(Math.random() * 10)
},
// Get BIRTH payload for the edge node
getNodeBirthPayload = function (): UPayload {
return {
timestamp: new Date().getTime(),
metrics: [
{
name: 'Node Control/Rebirth',
type: 'Boolean',
value: false,
},
{
name: 'Template1',
type: 'Template',
value: {
isDefinition: true,
metrics: [
{ name: 'myBool', value: false, type: 'Boolean' },
{ name: 'myInt', value: 0, type: 'UInt32' },
],
parameters: [
{
name: 'param1',
type: 'String',
value: 'value1',
},
],
},
},
],
}
},
// Get BIRTH payload for the device
getDeviceBirthPayload = function (): UPayload {
return {
timestamp: new Date().getTime(),
metrics: [
{ name: 'my_boolean', value: Math.random() > 0.5, type: 'Boolean' },
{ name: 'my_double', value: Math.random() * 0.123456789, type: 'Double' },
{ name: 'my_float', value: Math.random() * 0.123, type: 'Float' },
{ name: 'my_int', value: randomInt(), type: 'Int8' },
{ name: 'my_long', value: randomInt() * 214748364700, type: 'Int64' },
{ name: 'Inputs/0', value: true, type: 'Boolean' },
{ name: 'Inputs/1', value: 0, type: 'Int8' },
{ name: 'Inputs/2', value: 1.23, type: 'UInt64' },
{ name: 'Outputs/0', value: true, type: 'Boolean' },
{ name: 'Outputs/1', value: 0, type: 'Int16' },
{ name: 'Outputs/2', value: 1.23, type: 'UInt64' },
{ name: 'Properties/hw_version', value: hwVersion, type: 'String' },
{ name: 'Properties/sw_version', value: swVersion, type: 'String' },
{
name: 'my_dataset',
type: 'DataSet',
value: {
numOfColumns: 2,
types: ['String', 'String'],
columns: ['str1', 'str2'],
rows: [
['x', 'a'],
['y', 'b'],
],
},
},
{
name: 'TemplateInstance1',
type: 'Template',
value: {
templateRef: 'Template1',
isDefinition: false,
metrics: [
{ name: 'myBool', value: true, type: 'Boolean' },
{ name: 'myInt', value: 100, type: 'Int8' },
],
parameters: [
{
name: 'param1',
type: 'String',
value: 'value2',
},
],
},
},
],
}
},
// Get data payload for the device
getDataPayload = function (): UPayload {
return {
timestamp: new Date().getTime(),
metrics: [
{ name: 'my_boolean', value: Math.random() > 0.5, type: 'Boolean' },
{ name: 'my_double', value: Math.random() * 0.123456789, type: 'Double' },
{ name: 'my_float', value: Math.random() * 0.123, type: 'UInt64' },
{ name: 'my_int', value: randomInt(), type: 'Int16' },
{ name: 'my_long', value: randomInt() * 214748364700, type: 'UInt64' },
],
}
},
// Runs the sample
run = async function (): Promise<MockSparkplugClient> {
// Create the SparkplugClient
const sparkplugClient = SparkplugClient.newClient(config)
let updateInterval: NodeJS.Timeout | null = null
const connected = new Promise<MockSparkplugClient>((resolve) => {
// Create 'birth' handler
sparkplugClient.on('birth', () => {
// Publish Node BIRTH certificate
sparkplugClient.publishNodeBirth(getNodeBirthPayload())
// Publish Device BIRTH certificate
sparkplugClient.publishDeviceBirth(deviceId, getDeviceBirthPayload())
resolve({
stop: () => {
if (updateInterval) {
clearInterval(updateInterval)
}
sparkplugClient.stop()
}
})
})
})
// Create Incoming Message Handler
sparkplugClient.on('message', function (topic: string, payload: UPayload) {
console.log(topic, payload)
})
// Create node command handler
// spell-checker: disable-next-line
sparkplugClient.on('ncmd', function (payload: UPayload) {
var timestamp = payload.timestamp,
metrics = payload.metrics
if (metrics !== undefined && metrics !== null) {
for (var i = 0; i < metrics.length; i++) {
var metric = metrics[i]
if (metric.name == 'Node Control/Rebirth' && metric.value) {
console.log("Received 'Rebirth' command")
// Publish Node BIRTH certificate
sparkplugClient.publishNodeBirth(getNodeBirthPayload())
// Publish Device BIRTH certificate
sparkplugClient.publishDeviceBirth(deviceId, getDeviceBirthPayload())
}
}
}
})
// Create device command handler
// spell-checker: disable-next-line
sparkplugClient.on('dcmd', function (deviceId: string, payload: UPayload) {
var timestamp = payload.timestamp,
metrics = payload.metrics,
inboundMetricMap: { [name: string]: any } = {},
outboundMetric: Array<UMetric> = [],
outboundPayload: UPayload
console.log('Command received for device ' + deviceId)
// Loop over the metrics and store them in a map
if (metrics !== undefined && metrics !== null) {
for (var i = 0; i < metrics.length; i++) {
var metric = metrics[i]
if (metric.name !== undefined && metric.name !== null) {
inboundMetricMap[metric.name] = metric.value
}
}
}
if (inboundMetricMap['Outputs/0'] !== undefined && inboundMetricMap['Outputs/0'] !== null) {
console.log('Outputs/0: ' + inboundMetricMap['Outputs/0'])
outboundMetric.push({ name: 'Inputs/0', value: inboundMetricMap['Outputs/0'], type: 'Boolean' })
outboundMetric.push({ name: 'Outputs/0', value: inboundMetricMap['Outputs/0'], type: 'Boolean' })
console.log('Updated value for Inputs/0 ' + inboundMetricMap['Outputs/0'])
} else if (inboundMetricMap['Outputs/1'] !== undefined && inboundMetricMap['Outputs/1'] !== null) {
console.log('Outputs/1: ' + inboundMetricMap['Outputs/1'])
outboundMetric.push({ name: 'Inputs/1', value: inboundMetricMap['Outputs/1'], type: 'Int32' })
outboundMetric.push({ name: 'Outputs/1', value: inboundMetricMap['Outputs/1'], type: 'Int32' })
console.log('Updated value for Inputs/1 ' + inboundMetricMap['Outputs/1'])
} else if (inboundMetricMap['Outputs/2'] !== undefined && inboundMetricMap['Outputs/2'] !== null) {
console.log('Outputs/2: ' + inboundMetricMap['Outputs/2'])
outboundMetric.push({ name: 'Inputs/2', value: inboundMetricMap['Outputs/2'], type: 'UInt64' })
outboundMetric.push({ name: 'Outputs/2', value: inboundMetricMap['Outputs/2'], type: 'UInt64' })
console.log('Updated value for Inputs/2 ' + inboundMetricMap['Outputs/2'])
}
outboundPayload = {
timestamp: new Date().getTime(),
metrics: outboundMetric,
}
// Publish device data
sparkplugClient.publishDeviceData(deviceId, outboundPayload)
})
updateInterval = setInterval(function () {
// Publish device data
sparkplugClient.publishDeviceData(deviceId, getDataPayload())
}, 2000)
return connected
}
return { run: run }
})()
export default sample
-8
View File
@@ -21,14 +21,6 @@ export function sleep(ms: number, required = false) {
export async function writeText(text: string, element: Locator, delay = 0) {
return element.fill(text)
if (fast) {
return element.fill(text)
}
for (const c of text.split('')) {
await element.press(c)
await sleep(delay)
}
}
export async function deleteTextWithBackspaces(element: Locator, delay = 0, count = 0) {
+2468 -68
View File
File diff suppressed because it is too large Load Diff