diff --git a/.github/workflows/test-typescript-npm.yml b/.github/workflows/test-typescript-npm.yml new file mode 100644 index 0000000..e7eb503 --- /dev/null +++ b/.github/workflows/test-typescript-npm.yml @@ -0,0 +1,73 @@ +name: Test TypeScript + +env: + # See: https://github.com/actions/setup-node/#readme + NODE_VERSION: 10.x + +on: + push: + paths: + - ".github/workflows/test-typescript-npm.ya?ml" + - ".github/.?codecov.ya?ml" + - "dev/.?codecov.ya?ml" + - ".?codecov.ya?ml" + - "jest.config.js" + - "package.json" + - "package-lock.json" + - "tsconfig.json" + - "**.js" + - "**.jsx" + - "**.ts" + - "**.tsx" + pull_request: + paths: + - ".github/workflows/test-typescript-npm.ya?ml" + - "jest.config.js" + - "package.json" + - "package-lock.json" + - "tsconfig.json" + - "**.js" + - "**.jsx" + - "**.ts" + - "**.tsx" + schedule: + # Run periodically to catch breakage caused by external changes. + - cron: "0 13 * * WED" + workflow_dispatch: + repository_dispatch: + +jobs: + test: + runs-on: ${{ matrix.operating-system }} + + strategy: + fail-fast: false + + matrix: + operating-system: + - macos-latest + - ubuntu-latest + # The version of node-gyp used by this project (7.1.2) requires an older version of Visual Studio that is not + # available in the latest Windows GitHub Actions runner. + - windows-2019 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm run-script test + + - name: Send unit test coverage to Codecov + if: runner.os == 'Linux' + uses: codecov/codecov-action@v3 + with: + fail_ci_if_error: ${{ github.repository == 'arduino/arduino-serial-plotter-webapp' }} diff --git a/README.md b/README.md index 14813e0..cb9d602 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Serial Plotter WebApp +[![Test TypeScript status](https://github.com/arduino/arduino-serial-plotter-webapp/actions/workflows/test-typescript-npm.yml/badge.svg)](https://github.com/arduino/arduino-serial-plotter-webapp/actions/workflows/test-typescript-npm.yml) + This is a SPA that receives data points over WebSocket and prints graphs. The purpose is to provide a visual and live representation of data printed to the Serial Port. The application is designed to be as agnostic as possible regarding how and where it runs. For this reason, it accepts different settings when it's launched in order to configure the look&feel and the connection parameters. @@ -162,6 +164,7 @@ These are sent to the middleware to be stored and propagated to other clients. ## Development - `npm i` to install dependencies +- `npm test` to run automated tests - `npm start` to run the application in development mode @ [http://localhost:3000](http://localhost:3000) ## Deployment diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..d0d02df --- /dev/null +++ b/jest.config.js @@ -0,0 +1,12 @@ +/* + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/en/configuration.html + */ + +module.exports = { + collectCoverage: true, + coverageDirectory: "coverage", + coverageReporters: ["lcov"], + preset: "ts-jest", + testEnvironment: "node", +}; diff --git a/package-lock.json b/package-lock.json index 6ea40da..28c6c1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5818,6 +5818,15 @@ "picocolors": "^0.2.1" } }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, "bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -9612,7 +9621,7 @@ "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", "dev": true, "optional": true }, @@ -10305,9 +10314,9 @@ } }, "import-local": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", - "integrity": "sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "dev": true, "requires": { "pkg-dir": "^4.2.0", @@ -10897,9 +10906,9 @@ } }, "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "requires": { "debug": "^4.1.1", @@ -10916,9 +10925,9 @@ } }, "istanbul-reports": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.3.tgz", - "integrity": "sha512-0i77ZFLsb9U3DHi22WzmIngVzfoyxxbQcZRqlF3KoKmCJGq9nhFHoGi8FqBztN2rE8w6hURnZghetn0xpkVb6A==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -13015,6 +13024,12 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "makeerror": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", @@ -18615,9 +18630,9 @@ } }, "supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", "dev": true, "requires": { "has-flag": "^4.0.0", @@ -19105,6 +19120,38 @@ "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", "dev": true }, + "ts-jest": { + "version": "26.5.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.6.tgz", + "integrity": "sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^26.1.0", + "json5": "2.x", + "lodash": "4.x", + "make-error": "1.x", + "mkdirp": "1.x", + "semver": "7.x", + "yargs-parser": "20.x" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } + } + }, "ts-pnp": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", @@ -19539,9 +19586,9 @@ }, "dependencies": { "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true } } diff --git a/package.json b/package.json index e00f1ce..5466acc 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "test": "react-scripts test", + "test": "jest", "eject": "react-scripts eject" }, "files": [ @@ -49,6 +49,7 @@ "chartjs-plugin-streaming": "^2.0.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", + "jest": "^26.6.0", "luxon": "^2.1.0", "node-sass": "^6.0.1", "prettier": "^2.4.1", @@ -60,6 +61,7 @@ "react-scripts": "4.0.3", "react-select": "^5.1.0", "react-switch": "^6.0.0", + "ts-jest": "^26.5.6", "typescript": "^4.4.3", "web-vitals": "^1.1.2", "worker-loader": "^3.0.8" diff --git a/src/msgAggregatorWorker.test.ts b/src/msgAggregatorWorker.test.ts new file mode 100644 index 0000000..baa460a --- /dev/null +++ b/src/msgAggregatorWorker.test.ts @@ -0,0 +1,121 @@ +class WorkerStub { + listener = (_: { data: { command: string } }) => {}; + addEventListener(_: string, listenerCallback: (event: object) => void) { + this.listener = listenerCallback; + } +} +let worker = new WorkerStub(); +(global as any).self = worker; + +const messageAggregator = require("./msgAggregatorWorker"); + +beforeEach(() => { + worker.listener({ data: { command: "cleanup" } }); +}); + +describe("Parsing data", () => { + describe.each([ + ["space", " "], + ["tab", "\t"], + ["comma", ","], + ])("%s field delimiter", (_, fieldDelimiter) => { + describe.each([ + ["trailing", fieldDelimiter], + ["no trailing", ""], + ])("%s", (_, trailingFieldDelimiter) => { + describe.each([ + ["LF", "\n"], + ["CRLF", "\r\n"], + ])("%s record delimiter", (_, recordDelimiter) => { + test("single field", () => { + const messages = [ + `0${trailingFieldDelimiter}${recordDelimiter}`, + `1${trailingFieldDelimiter}${recordDelimiter}`, + `2${trailingFieldDelimiter}${recordDelimiter}`, + ]; + + const assertion = { + datasetNames: ["value 1"], + parsedLines: [{ "value 1": 1 }, { "value 1": 2 }], + }; + + expect(messageAggregator.parseSerialMessages(messages)).toEqual( + assertion + ); + }); + + test("multi-field", () => { + const messages = [ + `0${trailingFieldDelimiter}${recordDelimiter}`, + `1${fieldDelimiter}2${trailingFieldDelimiter}${recordDelimiter}`, + `3${fieldDelimiter}4${trailingFieldDelimiter}${recordDelimiter}`, + ]; + + const assertion = { + datasetNames: ["value 1", "value 2"], + parsedLines: [ + { "value 1": 1, "value 2": 2 }, + { "value 1": 3, "value 2": 4 }, + ], + }; + + expect(messageAggregator.parseSerialMessages(messages)).toEqual( + assertion + ); + }); + + test("labeled", () => { + const messages = [ + `0${trailingFieldDelimiter}${recordDelimiter}`, + `label_1:1${fieldDelimiter}label_2:2${trailingFieldDelimiter}${recordDelimiter}`, + `label_1:3${fieldDelimiter}label_2:4${trailingFieldDelimiter}${recordDelimiter}`, + ]; + + const assertion = { + datasetNames: ["label_1", "label_2"], + parsedLines: [ + { label_1: 1, label_2: 2 }, + { label_1: 3, label_2: 4 }, + ], + }; + + expect(messageAggregator.parseSerialMessages(messages)).toEqual( + assertion + ); + }); + + test("buffering", () => { + let messages = [ + `0${trailingFieldDelimiter}${recordDelimiter}`, + `1${fieldDelimiter}`, + ]; + + let assertion: { + datasetNames: string[]; + parsedLines: { [key: string]: number }[]; + } = { + datasetNames: [], + parsedLines: [], + }; + + expect(messageAggregator.parseSerialMessages(messages)).toEqual( + assertion + ); + + messages = [`2${trailingFieldDelimiter}${recordDelimiter}`]; + + assertion = { + datasetNames: ["value 1", "value 2"], + parsedLines: [{ "value 1": 1, "value 2": 2 }], + }; + + expect(messageAggregator.parseSerialMessages(messages)).toEqual( + assertion + ); + }); + }); + }); + }); +}); +// +export {};