You've already forked homehub
Release 1.3.0
This commit is contained in:
1
packages/homehub_core/.eslintignore
Executable file
1
packages/homehub_core/.eslintignore
Executable file
@@ -0,0 +1 @@
|
||||
lib/**/*.js
|
||||
27
packages/homehub_core/.eslintrc.json
Normal file
27
packages/homehub_core/.eslintrc.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": [
|
||||
"@bthlabs",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"modules": true
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"PRODUCTION": "readonly"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"react/prop-types": "off",
|
||||
"no-console": ["error", {"allow": ["error"]}]
|
||||
}
|
||||
}
|
||||
66
packages/homehub_core/.gitignore
vendored
Executable file
66
packages/homehub_core/.gitignore
vendored
Executable file
@@ -0,0 +1,66 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.envrc
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# homehub_frontend
|
||||
/lib/
|
||||
test-results.xml
|
||||
201
packages/homehub_core/LICENSE.txt
Normal file
201
packages/homehub_core/LICENSE.txt
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
19
packages/homehub_core/Makefile
Normal file
19
packages/homehub_core/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
PHONY: clean dev lint test build publish
|
||||
|
||||
clean:
|
||||
rm -rf lib/
|
||||
|
||||
dev:
|
||||
yarn dev
|
||||
|
||||
lint:
|
||||
yarn lint
|
||||
|
||||
test:
|
||||
yarn test --single-run
|
||||
|
||||
build: clean
|
||||
yarn build
|
||||
|
||||
publish:
|
||||
npm publish
|
||||
42
packages/homehub_core/NOTICE.txt
Normal file
42
packages/homehub_core/NOTICE.txt
Normal file
@@ -0,0 +1,42 @@
|
||||
BTHLabs HomeHub - Core
|
||||
Copyright 2021-present BTHLabs <contact@bthlabs.pl> (https://bthlabs.pl/)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
---
|
||||
|
||||
BTHLabs HomeHub - Core includes the following third party software
|
||||
|
||||
AWS SDK JS Crypto Helpers
|
||||
opyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
Licensed under terms of the Apache License, Version 2.0
|
||||
|
||||
Browser-compatible JavaScript MD5
|
||||
Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
|
||||
Licensed under terms of the BSD License
|
||||
|
||||
classnames
|
||||
Copyright (c) 2017 Jed Watson
|
||||
Licensed under terms of the MIT License
|
||||
|
||||
luxon
|
||||
Copyright 2019 JS Foundation and other contributors
|
||||
Licensed under terms of the MIT License
|
||||
|
||||
lodash
|
||||
Copyright JS Foundation and other contributors
|
||||
Licensed under terms of the MIT License
|
||||
|
||||
prop-types
|
||||
Copyright (c) 2013-present, Facebook, Inc.
|
||||
Licensed under terms of the MIT License
|
||||
3
packages/homehub_core/README.md
Normal file
3
packages/homehub_core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @bthlabs/homehub-core
|
||||
|
||||
BTHLabs HomeHub - Core
|
||||
22
packages/homehub_core/babel.config.js
Normal file
22
packages/homehub_core/babel.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
module.exports = {
|
||||
'presets': [
|
||||
[
|
||||
'@babel/preset-env', {
|
||||
'targets': {
|
||||
'chrome': 81,
|
||||
'edge': 18,
|
||||
'firefox': 75,
|
||||
'ie': 11,
|
||||
'opera': 68,
|
||||
'safari': 13,
|
||||
},
|
||||
'modules': 'commonjs',
|
||||
},
|
||||
],
|
||||
'@babel/preset-react',
|
||||
],
|
||||
'plugins': [
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
],
|
||||
};
|
||||
70
packages/homehub_core/karma.conf.js
Normal file
70
packages/homehub_core/karma.conf.js
Normal file
@@ -0,0 +1,70 @@
|
||||
var path = require('path');
|
||||
|
||||
const CONTEXT = path.resolve(__dirname);
|
||||
const SCSS_DIR = path.resolve(CONTEXT, 'scss');
|
||||
const PROJECT_DIR = path.resolve(CONTEXT, 'src');
|
||||
const TESTS_DIR = path.resolve(CONTEXT, 'tests');
|
||||
|
||||
const ENTRY_POINT = path.resolve(TESTS_DIR, '__entry__.js');
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '.',
|
||||
browsers: ['jsdom'],
|
||||
frameworks: ['jasmine'],
|
||||
files: [ENTRY_POINT],
|
||||
junitReporter: {
|
||||
outputDir: '',
|
||||
outputFile: 'test-results.xml',
|
||||
suite: 'homehub_frontend',
|
||||
useBrowserName: false,
|
||||
},
|
||||
mochaReporter: {
|
||||
ignoreSkipped: true,
|
||||
},
|
||||
preprocessors: {
|
||||
[ENTRY_POINT]: ['webpack', 'sourcemap'],
|
||||
},
|
||||
reporters: ['mocha'],
|
||||
singleRun: false,
|
||||
webpack: {
|
||||
mode: 'development',
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js?/,
|
||||
include: PROJECT_DIR,
|
||||
use: ['babel-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
include: TESTS_DIR,
|
||||
use: ['babel-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ['null-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(png|svg)$/,
|
||||
use: ['null-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'scss': SCSS_DIR,
|
||||
'src': PROJECT_DIR,
|
||||
'tests': TESTS_DIR,
|
||||
},
|
||||
},
|
||||
externals: {
|
||||
'cheerio': 'window',
|
||||
'react/addons': 'react',
|
||||
'react/lib/ExecutionEnvironment': 'react',
|
||||
'react/lib/ReactContext': 'react',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
72
packages/homehub_core/package.json
Normal file
72
packages/homehub_core/package.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "@bthlabs/homehub-core",
|
||||
"version": "1.3.0",
|
||||
"description": "BTHLabs HomeHub - Core",
|
||||
"main": "lib/index.js",
|
||||
"author": "BTHLabs <contact@bthlabs.pl> (https://bthlabs.pl/)",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production npx webpack-cli",
|
||||
"dev": "npx webpack-cli",
|
||||
"start": "npx webpack-cli --watch",
|
||||
"lint": "npx eslint src/ tests/ *.js",
|
||||
"test": "npx karma start",
|
||||
"test:dist": "npx karma start --single-run --reporters junit"
|
||||
},
|
||||
"files": [
|
||||
"lib/"
|
||||
],
|
||||
"publishConfig": {
|
||||
"registry": "https://nexus.bthlabs.pl/repository/npm-hosted/"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prop-types": ">=15.7.2",
|
||||
"react": ">=16.11.0",
|
||||
"react-dom": ">=16.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-crypto/sha256-js": "1.0.0",
|
||||
"@babel/core": "7.7.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.8.3",
|
||||
"@babel/plugin-proposal-export-default-from": "7.5.2",
|
||||
"@babel/preset-env": "7.7.1",
|
||||
"@babel/preset-react": "7.7.0",
|
||||
"@bthlabs/eslint-config": "1.0.3",
|
||||
"babel-eslint": "10.0.3",
|
||||
"babel-loader": "8.0.6",
|
||||
"classnames": "2.2.6",
|
||||
"clean-webpack-plugin": "3.0.0",
|
||||
"css-loader": "3.5.3",
|
||||
"enzyme": "3.10.0",
|
||||
"enzyme-adapter-react-16": "1.15.1",
|
||||
"eslint": "6.6.0",
|
||||
"eslint-plugin-react": "7.20.4",
|
||||
"eslint-plugin-react-hooks": "4.0.8",
|
||||
"jasmine-core": "3.5.0",
|
||||
"jsdom": "16.4.0",
|
||||
"karma": "4.4.1",
|
||||
"karma-jasmine": "2.0.1",
|
||||
"karma-jsdom-launcher": "9.0.0",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-sourcemap-loader": "0.3.7",
|
||||
"karma-webpack": "4.0.2",
|
||||
"lodash": "4.17.15",
|
||||
"luxon": "1.24.1",
|
||||
"node-sass": "4.14.1",
|
||||
"null-loader": "4.0.0",
|
||||
"polyfill-crypto.getrandomvalues": "1.0.0",
|
||||
"prop-types": "15.7.2",
|
||||
"react": "16.11.0",
|
||||
"react-dom": "16.11.0",
|
||||
"react-svg-loader": "3.0.3",
|
||||
"regenerator-runtime": "0.13.5",
|
||||
"sass-loader": "8.0.2",
|
||||
"shallow-with-context": "0.4.1",
|
||||
"style-loader": "1.2.1",
|
||||
"uuid": "8.2.0",
|
||||
"webpack": "4.41.2",
|
||||
"webpack-cli": "3.3.11",
|
||||
"whatwg-fetch": "3.6.2"
|
||||
}
|
||||
}
|
||||
7
packages/homehub_core/src/api/index.js
Normal file
7
packages/homehub_core/src/api/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as State from './state';
|
||||
import * as Services from './services';
|
||||
|
||||
export default {
|
||||
State: State,
|
||||
Services: Services,
|
||||
};
|
||||
20
packages/homehub_core/src/api/services.js
Normal file
20
packages/homehub_core/src/api/services.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as RPC from 'src/lib/rpc';
|
||||
|
||||
export const start = async (kind, instance, characteristics) => {
|
||||
const result = await RPC.callMethod(
|
||||
'services.start', [kind, instance, characteristics]
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const stop = async (kind, instance) => {
|
||||
const result = await RPC.callMethod('services.stop', [kind, instance]);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const use = async (kind, instance, capability, params) => {
|
||||
const result = await RPC.callMethod(
|
||||
'services.use', [kind, instance, capability, params]
|
||||
);
|
||||
return result;
|
||||
};
|
||||
11
packages/homehub_core/src/api/state.js
Normal file
11
packages/homehub_core/src/api/state.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import {callMethod} from 'src/lib/rpc';
|
||||
|
||||
export const get = async () => {
|
||||
const result = await callMethod('state.get_frontend');
|
||||
return result;
|
||||
};
|
||||
|
||||
export const save = async (state) => {
|
||||
const result = await callMethod('state.save_frontend', [state]);
|
||||
return result;
|
||||
};
|
||||
136
packages/homehub_core/src/containers/ServiceContainer.js
Normal file
136
packages/homehub_core/src/containers/ServiceContainer.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import isNull from 'lodash/isNull';
|
||||
import isUndefined from 'lodash/isUndefined';
|
||||
import React from 'react';
|
||||
|
||||
import {DashboardsContext} from '../context';
|
||||
import {ServiceState, lookupService} from '../lib';
|
||||
|
||||
export class ServiceContainer extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.serviceUnsubscribe = null;
|
||||
|
||||
this.state = {
|
||||
serviceState: null,
|
||||
showSettingsModal: false,
|
||||
nextCharacteristics: null,
|
||||
};
|
||||
}
|
||||
service = () => {
|
||||
return lookupService(
|
||||
this.context.dashboards, this.props.kind, this.props.instance
|
||||
);
|
||||
}
|
||||
setServiceState = (payload, {reset}) => {
|
||||
let nextServiceState = null;
|
||||
if (reset || isNull(this.state.serviceState)) {
|
||||
nextServiceState = new ServiceState(payload);
|
||||
} else {
|
||||
nextServiceState = this.state.serviceState.update(payload);
|
||||
}
|
||||
|
||||
this.setState({serviceState: nextServiceState});
|
||||
}
|
||||
setNextCharacteristics = (nextCharacteristics) => {
|
||||
this.setState({nextCharacteristics: nextCharacteristics});
|
||||
}
|
||||
setNextCharacteristicsFromService = (fromService) => {
|
||||
let nextCharacteristics = null;
|
||||
if (fromService && this.service().widgetComponent && !fromService.isDummy()) {
|
||||
nextCharacteristics = {...fromService.characteristics};
|
||||
}
|
||||
|
||||
this.setState({nextCharacteristics: nextCharacteristics});
|
||||
}
|
||||
showHideSettingsModal = (show) => {
|
||||
if (show) {
|
||||
this.setNextCharacteristicsFromService(this.service());
|
||||
} else {
|
||||
this.setNextCharacteristicsFromService(null);
|
||||
}
|
||||
|
||||
this.setState({showSettingsModal: show});
|
||||
}
|
||||
onSettingsButtonClick = () => {
|
||||
this.showHideSettingsModal(true);
|
||||
}
|
||||
onSettingsModalClose = () => {
|
||||
this.showHideSettingsModal(false);
|
||||
}
|
||||
onSettingsModalSaveButtonClick = () => {
|
||||
this.context.saveServiceCharacteristics(
|
||||
this.props.kind, this.props.instance, this.state.nextCharacteristics
|
||||
);
|
||||
this.onSettingsModalClose();
|
||||
}
|
||||
onSettingsModalNukeButtonClick = () => {
|
||||
this.context.nukeService(this.props.kind, this.props.instance);
|
||||
this.onSettingsModalClose();
|
||||
}
|
||||
onAppearancePopupColorChange = (color) => {
|
||||
const currentCharacteristics = this.service().characteristics;
|
||||
this.context.saveServiceCharacteristics(
|
||||
this.props.kind, this.props.instance, {
|
||||
...currentCharacteristics,
|
||||
appearance: {
|
||||
...(currentCharacteristics.appearance || {}),
|
||||
color: color,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
onServiceRestartButtonClick = () => {
|
||||
this.service().restart();
|
||||
}
|
||||
unsubscribeIfNeeded = () => {
|
||||
if (!isNull(this.serviceUnsubscribe)) {
|
||||
this.serviceUnsubscribe();
|
||||
}
|
||||
}
|
||||
componentDidMount () {
|
||||
const service = this.service();
|
||||
if (service && service.widgetComponent && !service.isDummy()) {
|
||||
this.setServiceState(service.initialState(), true);
|
||||
service.start();
|
||||
|
||||
this.unsubscribeIfNeeded();
|
||||
this.serviceUnsubscribe = service.subscribe(this.setServiceState);
|
||||
}
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.unsubscribeIfNeeded();
|
||||
}
|
||||
render () {
|
||||
const WidgetComponent = this.service().widgetComponent;
|
||||
|
||||
const hasSettingsView = Boolean(
|
||||
!this.service().isDummy()
|
||||
&& WidgetComponent
|
||||
&& !isUndefined(WidgetComponent.settingsView)
|
||||
);
|
||||
|
||||
const settingsViewProps = {
|
||||
kind: this.props.kind,
|
||||
instance: this.props.instance,
|
||||
nextCharacteristics: this.state.nextCharacteristics,
|
||||
setNextCharacteristics: this.setNextCharacteristics,
|
||||
};
|
||||
|
||||
return this.props.children({
|
||||
hasSettingsView: hasSettingsView,
|
||||
service: this.service(),
|
||||
serviceState: this.state.serviceState,
|
||||
setServiceState: this.setServiceState,
|
||||
settingsViewProps: settingsViewProps,
|
||||
showSettingsModal: this.state.showSettingsModal,
|
||||
onAppearancePopupColorChange: this.onAppearancePopupColorChange,
|
||||
onServiceRestartButtonClick: this.onServiceRestartButtonClick,
|
||||
onSettingsButtonClick: this.onSettingsButtonClick,
|
||||
onSettingsModalClose: this.onSettingsModalClose,
|
||||
onSettingsModalNukeButtonClick: this.onSettingsModalNukeButtonClick,
|
||||
onSettingsModalSaveButtonClick: this.onSettingsModalSaveButtonClick,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ServiceContainer.contextType = DashboardsContext;
|
||||
1
packages/homehub_core/src/containers/index.js
Normal file
1
packages/homehub_core/src/containers/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export {ServiceContainer} from './ServiceContainer';
|
||||
24
packages/homehub_core/src/context/DashboardsContext.js
Executable file
24
packages/homehub_core/src/context/DashboardsContext.js
Executable file
@@ -0,0 +1,24 @@
|
||||
import noop from 'lodash/noop';
|
||||
import React from 'react';
|
||||
|
||||
export const DEFAULT_DASHBOARDS_CONTEXT = {
|
||||
currentDashboardId: null,
|
||||
dashboards: [],
|
||||
nukeService: noop,
|
||||
saveServiceCharacteristics: noop,
|
||||
saveServiceLayout: noop,
|
||||
addService: noop,
|
||||
isLoading: false,
|
||||
lastSaveTimestamp: null,
|
||||
lastSaveError: null,
|
||||
isSaving: false,
|
||||
isWebSocketConnected: false,
|
||||
setCurrentDashboardId: noop,
|
||||
dashboardsHash: null,
|
||||
addDashboard: noop,
|
||||
};
|
||||
|
||||
export const DashboardsContext = React.createContext(
|
||||
DEFAULT_DASHBOARDS_CONTEXT
|
||||
);
|
||||
DashboardsContext.displayName = 'DashboardsContext';
|
||||
1
packages/homehub_core/src/context/index.js
Normal file
1
packages/homehub_core/src/context/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './DashboardsContext';
|
||||
2
packages/homehub_core/src/defs/index.js
Normal file
2
packages/homehub_core/src/defs/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './services';
|
||||
export * from './widgets';
|
||||
5
packages/homehub_core/src/defs/services.js
Normal file
5
packages/homehub_core/src/defs/services.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const kServiceDummy = 'kServiceDummy';
|
||||
export const kServiceTime = 'kServiceTime';
|
||||
export const kServiceUptime = 'kServiceUptime';
|
||||
export const kServiceWeather = 'kServiceWeather';
|
||||
export const kServiceOfflineWeather = 'kServiceOfflineWeather';
|
||||
3
packages/homehub_core/src/defs/widgets.js
Normal file
3
packages/homehub_core/src/defs/widgets.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const kWidgetTime = 'kWidgetTime';
|
||||
export const kWidgetUptime = 'kWidgetUptime';
|
||||
export const kWidgetWeather = 'kWidgetWeather';
|
||||
133
packages/homehub_core/src/hooks.js
Executable file
133
packages/homehub_core/src/hooks.js
Executable file
@@ -0,0 +1,133 @@
|
||||
import {useContext, useDebugValue} from 'react';
|
||||
|
||||
import {DashboardsContext} from 'src/context';
|
||||
import {lookupService} from 'src/lib';
|
||||
|
||||
export const useServices = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
let result = [];
|
||||
if (context.currentDashboardId) {
|
||||
const currentDashboard = context.dashboards.find((dashboard) => {
|
||||
return dashboard.id === context.currentDashboardId;
|
||||
});
|
||||
result = currentDashboard.services;
|
||||
}
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useService = (kind, instance) => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = lookupService(context.dashboards, kind, instance);
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useSaveServiceCharacteristics = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = context.saveServiceCharacteristics;
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useSaveServiceLayout = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = context.saveServiceLayout;
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useNukeService = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = context.nukeService;
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useAddService = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = context.addService;
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useServicesSavingState = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = {
|
||||
lastSaveTimestamp: context.lastSaveTimestamp,
|
||||
lastSaveError: context.lastSaveError,
|
||||
isSaving: context.isSaving,
|
||||
};
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useWebSocketState = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = {
|
||||
isWebSocketConnected: context.isWebSocketConnected,
|
||||
};
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useDashboards = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = context.dashboards || [];
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useCurrentDashboardId = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = context.currentDashboardId;
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useSetCurrentDashboardId = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = context.setCurrentDashboardId;
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useDashboardsHash = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = context.dashboardsHash;
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useAddDashboard = () => {
|
||||
const context = useContext(DashboardsContext);
|
||||
|
||||
const result = context.addDashboard;
|
||||
useDebugValue(result);
|
||||
|
||||
return result;
|
||||
};
|
||||
13
packages/homehub_core/src/index.js
Normal file
13
packages/homehub_core/src/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* BTHLabs HomeHub - Core (https://bthlabs.pl/)
|
||||
* Copyright 2021-present BTHLabs
|
||||
* Apache License Version 2.0
|
||||
*/
|
||||
|
||||
export {default as API} from 'src/api';
|
||||
export * from 'src/containers';
|
||||
export * from 'src/context';
|
||||
export * from 'src/defs';
|
||||
export * from 'src/hooks';
|
||||
export * from 'src/lib';
|
||||
export * from 'src/providers';
|
||||
70
packages/homehub_core/src/lib/base.js
Normal file
70
packages/homehub_core/src/lib/base.js
Normal file
@@ -0,0 +1,70 @@
|
||||
export class HomeHubBaseClass {
|
||||
constructor () {
|
||||
this.__mixins__ = [];
|
||||
}
|
||||
}
|
||||
|
||||
export const SubscribableMixin = (Base) => {
|
||||
return class extends Base {
|
||||
constructor (...args) {
|
||||
super(...args);
|
||||
this.__mixins__.push('SubscribableMixin');
|
||||
|
||||
this.subscribers = [];
|
||||
}
|
||||
notify (payload, userInfo) {
|
||||
userInfo = userInfo || {};
|
||||
|
||||
this.subscribers.forEach((subscriber) => {
|
||||
if (subscriber !== null) {
|
||||
subscriber(payload, userInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
unsubscriberFactory (subscriberIndex) {
|
||||
return () => {
|
||||
this.subscribers.splice(subscriberIndex, 1, null);
|
||||
};
|
||||
}
|
||||
subscribe (callback) {
|
||||
const subscriberIndex = this.subscribers.push(callback) - 1;
|
||||
return this.unsubscriberFactory(subscriberIndex);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const EventSourceMixin = (Base, knownEvents = []) => {
|
||||
return class extends Base {
|
||||
constructor (...args) {
|
||||
super(...args);
|
||||
this.__mixins__.push('EventSourceMixin');
|
||||
|
||||
this.eventListeners = {};
|
||||
knownEvents.forEach((event) => {
|
||||
this.eventListeners[event] = [];
|
||||
});
|
||||
}
|
||||
addEventListener (event, listener) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.eventListeners, event)) {
|
||||
this.eventListeners[event].push(listener);
|
||||
}
|
||||
}
|
||||
removeEventListener (event, listener) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.eventListeners, event)) {
|
||||
const listenerIndex = this.eventListeners[event].indexOf(listener);
|
||||
if (listenerIndex !== -1) {
|
||||
this.eventListeners[event].splice(listenerIndex, 1, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
fireEvent (event) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.eventListeners, event)) {
|
||||
this.eventListeners[event].forEach((listener) => {
|
||||
if (listener !== null) {
|
||||
listener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
9
packages/homehub_core/src/lib/dashboards.js
Normal file
9
packages/homehub_core/src/lib/dashboards.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
export const DashboardFactory = (name = 'Default') => {
|
||||
return {
|
||||
id: uuid.v4(),
|
||||
name: name,
|
||||
services: [],
|
||||
};
|
||||
};
|
||||
12
packages/homehub_core/src/lib/hashlib.js
Normal file
12
packages/homehub_core/src/lib/hashlib.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import {Sha256} from '@aws-crypto/sha256-js';
|
||||
|
||||
export const sha256 = async (message) => {
|
||||
const hash = new Sha256(message);
|
||||
const digest = await hash.digest();
|
||||
|
||||
let result = '';
|
||||
digest.forEach((byte) => {
|
||||
result += byte.toString(16).padStart(2, '0');
|
||||
});
|
||||
return result;
|
||||
};
|
||||
12
packages/homehub_core/src/lib/index.js
Normal file
12
packages/homehub_core/src/lib/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as localStorage from './localStorage';
|
||||
|
||||
export * from './base';
|
||||
export * from './dashboards';
|
||||
export * from './hashlib';
|
||||
export * from './services';
|
||||
export * from './websocket';
|
||||
|
||||
export const LocalStorage = {
|
||||
getItem: localStorage.getItem,
|
||||
setItem: localStorage.setItem,
|
||||
};
|
||||
22
packages/homehub_core/src/lib/localStorage.js
Normal file
22
packages/homehub_core/src/lib/localStorage.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export const getItem = (key, def = null) => {
|
||||
let item = null;
|
||||
try {
|
||||
item = window.localStorage.getItem(key);
|
||||
} catch (err) {
|
||||
console.error('Unhandled exception when setting local storage item!', err);
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return def;
|
||||
}
|
||||
|
||||
return JSON.parse(item);
|
||||
};
|
||||
|
||||
export const setItem = (key, item) => {
|
||||
try {
|
||||
window.localStorage.setItem(key, JSON.stringify(item));
|
||||
} catch (err) {
|
||||
console.error('Unhandled exception when setting local storage item!', err);
|
||||
}
|
||||
};
|
||||
42
packages/homehub_core/src/lib/rpc.js
Normal file
42
packages/homehub_core/src/lib/rpc.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
|
||||
export const callMethod = async (method, params, notification = false) => {
|
||||
const payload = {
|
||||
jsonrpc: '2.0',
|
||||
method: method,
|
||||
};
|
||||
|
||||
if (params) {
|
||||
payload.params = params;
|
||||
}
|
||||
|
||||
if (!notification) {
|
||||
payload.id = uuidv4();
|
||||
}
|
||||
|
||||
const response = await window.fetch('/backend/rpc', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const result = {};
|
||||
try {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
throw new Error(`RPC Error: ${data.error.message} ${data.error.data}`);
|
||||
}
|
||||
|
||||
result.data = data.result;
|
||||
} catch (error) {
|
||||
result.error = error;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
160
packages/homehub_core/src/lib/services.js
Normal file
160
packages/homehub_core/src/lib/services.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import clone from 'lodash/clone';
|
||||
import isNull from 'lodash/isNull';
|
||||
|
||||
import {kServiceDummy} from 'src/defs';
|
||||
import {HomeHubBaseClass, SubscribableMixin} from 'src/lib/base';
|
||||
|
||||
export class ServiceState extends HomeHubBaseClass {
|
||||
static DEFAULT_PAYLOAD = {
|
||||
data: null,
|
||||
error: null,
|
||||
};
|
||||
constructor (payload) {
|
||||
super(payload);
|
||||
|
||||
this.payload = {
|
||||
...ServiceState.DEFAULT_PAYLOAD,
|
||||
...(payload || {}),
|
||||
};
|
||||
}
|
||||
isLoading () {
|
||||
return (isNull(this.payload.data) && isNull(this.payload.error));
|
||||
}
|
||||
hasData () {
|
||||
return (
|
||||
!this.isLoading() && !isNull(this.payload.data)
|
||||
);
|
||||
}
|
||||
hasError () {
|
||||
return (
|
||||
!this.isLoading() && !isNull(this.payload.error)
|
||||
);
|
||||
}
|
||||
hasFatalError () {
|
||||
return (!this.hasData() && this.hasError());
|
||||
}
|
||||
update (nextPayload) {
|
||||
return new ServiceState({
|
||||
...this.payload,
|
||||
...nextPayload,
|
||||
});
|
||||
}
|
||||
data () {
|
||||
return this.payload.data;
|
||||
}
|
||||
error () {
|
||||
return this.payload.error;
|
||||
}
|
||||
}
|
||||
|
||||
export class BaseService extends SubscribableMixin(HomeHubBaseClass) {
|
||||
static availableOffline = false;
|
||||
static availableOnline = true;
|
||||
static kind = null;
|
||||
static widget = null;
|
||||
static emptyCharacteristics () {
|
||||
return {};
|
||||
}
|
||||
constructor (spec) {
|
||||
super(spec);
|
||||
|
||||
this.kind = this.constructor.kind;
|
||||
this.instance = spec.instance;
|
||||
this.characteristics = spec.characteristics;
|
||||
this.widget = this.constructor.widget;
|
||||
this.widgetComponent = spec.widgetComponent || null;
|
||||
this.layout = spec.layout;
|
||||
}
|
||||
async start () {
|
||||
throw new Error('NOT IMPLEMENTED');
|
||||
}
|
||||
async stop () {
|
||||
throw new Error('NOT IMPLEMENTED');
|
||||
}
|
||||
async restart () {
|
||||
this.notify(this.initialState(), {reset: true});
|
||||
|
||||
await this.stop();
|
||||
await this.start();
|
||||
}
|
||||
onMessage (data) {
|
||||
const payload = {
|
||||
...data,
|
||||
};
|
||||
|
||||
if (data.error) {
|
||||
payload.error = new Error(data.error);
|
||||
}
|
||||
|
||||
this.notify(payload);
|
||||
}
|
||||
isDummy () {
|
||||
return false;
|
||||
}
|
||||
initialState () {
|
||||
return null;
|
||||
}
|
||||
setCharacteristics (newCharacteristics) {
|
||||
this.characteristics = clone(newCharacteristics);
|
||||
}
|
||||
setLayout (newLayout) {
|
||||
this.layout = clone(newLayout);
|
||||
}
|
||||
toJSON () {
|
||||
return {
|
||||
kind: this.kind,
|
||||
instance: this.instance,
|
||||
characteristics: this.characteristics,
|
||||
layout: this.layout,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class DummyService extends BaseService {
|
||||
kind = kServiceDummy;
|
||||
constructor (spec) {
|
||||
super({
|
||||
...spec,
|
||||
instance: null,
|
||||
widgetComponent: null,
|
||||
});
|
||||
}
|
||||
isDummy = () => {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const lookupService = (dashboards, kind, instance) => {
|
||||
let result = null;
|
||||
for (const dashboard of dashboards) {
|
||||
for (const service of dashboard.services) {
|
||||
if (service.kind === kind && service.instance === instance) {
|
||||
result = service;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (result !== null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
return new DummyService();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const lookupServices = (dashboards, kind) => {
|
||||
let result = [];
|
||||
for (const dashboard of dashboards) {
|
||||
for (const service of dashboard.services) {
|
||||
if (service.kind === kind) {
|
||||
result.push(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
85
packages/homehub_core/src/lib/websocket.js
Normal file
85
packages/homehub_core/src/lib/websocket.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
EventSourceMixin, HomeHubBaseClass, SubscribableMixin,
|
||||
} from 'src/lib/base';
|
||||
|
||||
export const kWebSocketMsgTypeServiceNotification = 'SERVICE_NOTIFICATION';
|
||||
export const kWebSocketMsgTypeStateNotification = 'STATE_NOTIFICATION';
|
||||
|
||||
const Base = SubscribableMixin(
|
||||
EventSourceMixin(HomeHubBaseClass, ['start', 'stop'])
|
||||
);
|
||||
|
||||
export class HomeHubWebSocket extends Base {
|
||||
constructor (debug, settings) {
|
||||
super(debug, settings);
|
||||
|
||||
this.debug = debug;
|
||||
this.settings = settings;
|
||||
|
||||
this.socket = null;
|
||||
this.reconnectTimeout = null;
|
||||
this.reconnectCounter = 0;
|
||||
}
|
||||
logDebug (...args) {
|
||||
if (this.debug) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
stopReconnect () {
|
||||
if (this.reconnectTimeout) {
|
||||
window.clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
|
||||
this.reconnectTimeout = null;
|
||||
this.reconnectCounter = 0;
|
||||
}
|
||||
startReconnect () {
|
||||
if (this.reconnectCounter < 30) {
|
||||
this.reconnectCounter += 1;
|
||||
this.reconnectTimeout = window.setTimeout(
|
||||
this.start, 1000
|
||||
);
|
||||
} else {
|
||||
this.stopReconnect();
|
||||
throw new Error('HomeHubWebSocket: Reconnect tries limit reached.');
|
||||
}
|
||||
}
|
||||
start = () => {
|
||||
this.socket = new WebSocket(this.settings.url, this.settings.protocols);
|
||||
this.socket.addEventListener('open', this.onSocketOpen);
|
||||
this.socket.addEventListener('close', this.onSocketClose);
|
||||
this.socket.addEventListener('message', this.onSocketMessage);
|
||||
}
|
||||
stop () {
|
||||
this.socket.close();
|
||||
}
|
||||
onSocketOpen = () => {
|
||||
this.logDebug('HomeHubWebSocket.onSocketOpen()', this.socket.readyState);
|
||||
|
||||
this.stopReconnect();
|
||||
|
||||
this.fireEvent('start');
|
||||
}
|
||||
onSocketClose = (event) => {
|
||||
this.logDebug('HomeHubWebSocket.onSocketClose()', event.code);
|
||||
|
||||
this.startReconnect();
|
||||
|
||||
this.fireEvent('stop');
|
||||
}
|
||||
onSocketMessage = (event) => {
|
||||
let message = null;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
this.logDebug('HomeHubWebSocket.onSocketMessage()', message);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'HomeHubWebSocket.onSocketMessage() JSON Parse Error', error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.notify(message);
|
||||
}
|
||||
}
|
||||
337
packages/homehub_core/src/providers/DashboardsProvider.js
Executable file
337
packages/homehub_core/src/providers/DashboardsProvider.js
Executable file
@@ -0,0 +1,337 @@
|
||||
import React from 'react';
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
|
||||
import {DEFAULT_DASHBOARDS_CONTEXT, DashboardsContext} from 'src/context';
|
||||
import {
|
||||
DashboardFactory, DummyService, kWebSocketMsgTypeServiceNotification,
|
||||
kWebSocketMsgTypeStateNotification, lookupService, lookupServices, sha256,
|
||||
} from 'src/lib';
|
||||
import * as WebSocketLib from 'src/lib/websocket';
|
||||
|
||||
export class DashboardsProvider extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.webSocket = null;
|
||||
this.webSocketUnsubscriber = null;
|
||||
this.dashboardsHash = null;
|
||||
|
||||
this.state = {
|
||||
currentDashboardId: DEFAULT_DASHBOARDS_CONTEXT.currentDashboardId,
|
||||
dashboards: DEFAULT_DASHBOARDS_CONTEXT.dashboards,
|
||||
isLoading: DEFAULT_DASHBOARDS_CONTEXT.isLoading,
|
||||
lastSaveTimestamp: DEFAULT_DASHBOARDS_CONTEXT.lastSaveTimestamp,
|
||||
lastSaveError: DEFAULT_DASHBOARDS_CONTEXT.lastSaveError,
|
||||
isSaving: DEFAULT_DASHBOARDS_CONTEXT.isSaving,
|
||||
isWebSocketConnected: DEFAULT_DASHBOARDS_CONTEXT.isWebSocketConnected,
|
||||
};
|
||||
}
|
||||
handleSaveError = (error) => {
|
||||
console.error('Dashboards save error!', error);
|
||||
return error.message;
|
||||
}
|
||||
onWebSocketStart = () => {
|
||||
this.setState({isWebSocketConnected: true});
|
||||
}
|
||||
onWebSocketStop = () => {
|
||||
this.setState({isWebSocketConnected: false});
|
||||
}
|
||||
onWebSocketMessage = async (message) => {
|
||||
if (message.type === kWebSocketMsgTypeServiceNotification) {
|
||||
if (message.instance) {
|
||||
const service = lookupService(
|
||||
this.state.dashboards, message.kind, message.instance
|
||||
);
|
||||
|
||||
if (!service.isDummy()) {
|
||||
service.onMessage(message.data);
|
||||
}
|
||||
} else {
|
||||
const services = lookupServices(this.state.dashboards, message.kind);
|
||||
services.forEach((service) => {
|
||||
if (!service.isDummy()) {
|
||||
service.onMessage(message.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (message.type == kWebSocketMsgTypeStateNotification) {
|
||||
const dashboardsHash = await sha256(JSON.stringify(message.data));
|
||||
if (this.dashboardsHash !== dashboardsHash) {
|
||||
this.setState({
|
||||
dashboards: await this.handleLoadedState(message.data),
|
||||
lastSaveTimestamp: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
currentDashboard = () => {
|
||||
return this.state.dashboards.find((dashboard) => {
|
||||
return dashboard.id === this.state.currentDashboardId;
|
||||
});
|
||||
}
|
||||
mutateCurrentDashboardServices = (mutator) => {
|
||||
return this.state.dashboards.map((dashboard) => {
|
||||
if (dashboard.id === this.state.currentDashboardId) {
|
||||
return {
|
||||
...dashboard,
|
||||
services: mutator(dashboard.services),
|
||||
};
|
||||
}
|
||||
|
||||
return dashboard;
|
||||
});
|
||||
}
|
||||
serviceFromSpec = (spec) => {
|
||||
const ServiceObject = this.props.settings.SERVICES[spec.kind] || DummyService;
|
||||
|
||||
const serviceObject = new ServiceObject({
|
||||
...spec,
|
||||
widgetComponent: (
|
||||
this.props.settings.WIDGETS[ServiceObject.widget] || null
|
||||
),
|
||||
});
|
||||
|
||||
return serviceObject;
|
||||
}
|
||||
saveDashboards = async (dashboards) => {
|
||||
const nextState = {
|
||||
isSaving: false,
|
||||
};
|
||||
|
||||
try {
|
||||
const dashboardsToSave = {
|
||||
dashboards: dashboards.map((dashboard) => {
|
||||
return {
|
||||
...dashboard,
|
||||
services: dashboard.services.map((service) => {
|
||||
return service.toJSON();
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const dashboardsHash = await sha256(JSON.stringify(dashboardsToSave));
|
||||
if (this.dashboardsHash !== dashboardsHash) {
|
||||
this.dashboardsHash = dashboardsHash;
|
||||
|
||||
this.setState({
|
||||
isSaving: true,
|
||||
});
|
||||
|
||||
await this.props.saveDashboards(dashboardsToSave);
|
||||
nextState.lastSaveTimestamp = new Date();
|
||||
nextState.lastSaveError = null;
|
||||
}
|
||||
} catch (error) {
|
||||
this.dashboardsHash = null;
|
||||
nextState.lastSaveTimestamp = null;
|
||||
nextState.lastSaveError = this.handleSaveError(error);
|
||||
}
|
||||
|
||||
this.setState(nextState);
|
||||
}
|
||||
nukeService = (kind, instance) => {
|
||||
const removedService = lookupService(
|
||||
this.state.dashboards, kind, instance
|
||||
);
|
||||
removedService.stop();
|
||||
|
||||
const nextDashboards = this.mutateCurrentDashboardServices((services) => {
|
||||
return services.filter((service) => {
|
||||
return !(service.kind === kind && service.instance === instance);
|
||||
});
|
||||
});
|
||||
|
||||
this.saveDashboards(nextDashboards);
|
||||
this.setState({
|
||||
dashboards: nextDashboards,
|
||||
});
|
||||
}
|
||||
saveServiceCharacteristics = (kind, instance, newCharacteristics) => {
|
||||
const updatedService = lookupService(
|
||||
this.state.dashboards, kind, instance
|
||||
);
|
||||
|
||||
const nextDashboards = this.mutateCurrentDashboardServices((services) => {
|
||||
return services.map((service) => {
|
||||
if (service === updatedService) {
|
||||
service.setCharacteristics(newCharacteristics);
|
||||
}
|
||||
|
||||
return service;
|
||||
});
|
||||
});
|
||||
|
||||
this.saveDashboards(nextDashboards);
|
||||
this.setState({
|
||||
dashboards: nextDashboards,
|
||||
});
|
||||
}
|
||||
saveServiceLayout = (instance, layout) => {
|
||||
const nextDashboards = this.mutateCurrentDashboardServices((services) => {
|
||||
return services.map((service) => {
|
||||
if (service.instance === instance) {
|
||||
service.setLayout(layout);
|
||||
}
|
||||
|
||||
return service;
|
||||
});
|
||||
});
|
||||
|
||||
this.saveDashboards(nextDashboards);
|
||||
this.setState({
|
||||
dashboards: nextDashboards,
|
||||
});
|
||||
}
|
||||
addService = (kind, characteristics) => {
|
||||
const ServiceObject = this.props.settings.SERVICES[kind];
|
||||
const WidgetComponent = this.props.settings.WIDGETS[ServiceObject.widget];
|
||||
|
||||
const spec = {
|
||||
kind: kind,
|
||||
instance: uuidv4(),
|
||||
characteristics: characteristics,
|
||||
};
|
||||
|
||||
const currentDashboard = this.currentDashboard();
|
||||
if (!currentDashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastLayout = {y: 0, h: 0};
|
||||
currentDashboard.services.forEach((service) => {
|
||||
if (service.layout.y >= lastLayout.y) {
|
||||
lastLayout = {
|
||||
...lastLayout,
|
||||
y: service.layout.y,
|
||||
};
|
||||
|
||||
if (service.layout.h > lastLayout.h) {
|
||||
lastLayout.h = service.layout.h;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
spec.layout = {
|
||||
...WidgetComponent.defaultLayout || {h: 1, w: 1},
|
||||
x: 0,
|
||||
y: lastLayout.y + lastLayout.h,
|
||||
};
|
||||
|
||||
const nextDashboards = this.mutateCurrentDashboardServices((services) => {
|
||||
return [...services, this.serviceFromSpec(spec)];
|
||||
});
|
||||
|
||||
this.saveDashboards(nextDashboards);
|
||||
this.setState({
|
||||
dashboards: nextDashboards,
|
||||
});
|
||||
}
|
||||
handleLoadedState = async (state) => {
|
||||
this.dashboardsHash = await sha256(JSON.stringify(state));
|
||||
return state.dashboards.map((dashboard) => {
|
||||
return {
|
||||
...dashboard,
|
||||
services: dashboard.services.map((service) => {
|
||||
return this.serviceFromSpec(service);
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
loadDashboards = async () => {
|
||||
const nextState = {
|
||||
isLoading: false,
|
||||
};
|
||||
try {
|
||||
const state = await this.props.loadDashboards();
|
||||
nextState.dashboards = await this.handleLoadedState(state);
|
||||
|
||||
if (this.state.currentDashboardId === null && nextState.dashboards.length > 0) {
|
||||
nextState.currentDashboardId = nextState.dashboards[0].id;
|
||||
}
|
||||
|
||||
nextState.lastSaveTimestamp = new Date();
|
||||
} catch (error) {
|
||||
nextState.lastSaveError = this.handleSaveError(error);
|
||||
}
|
||||
|
||||
this.setState(nextState);
|
||||
}
|
||||
setCurrentDashboardId = (dashboardId) => {
|
||||
this.setState({
|
||||
currentDashboardId: dashboardId,
|
||||
});
|
||||
}
|
||||
addDashboard = (name) => {
|
||||
const nextDashboards = [
|
||||
...this.state.dashboards,
|
||||
DashboardFactory(name),
|
||||
];
|
||||
|
||||
const nextState = {
|
||||
dashboards: nextDashboards,
|
||||
};
|
||||
if (nextDashboards.length == 1) {
|
||||
nextState.currentDashboardId = nextDashboards[0].id;
|
||||
}
|
||||
|
||||
this.saveDashboards(nextDashboards);
|
||||
this.setState(nextState);
|
||||
}
|
||||
componentDidMount () {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
this.webSocket = new WebSocketLib.HomeHubWebSocket(
|
||||
this.props.settings.DEBUG, this.props.settings.WEBSOCKET
|
||||
);
|
||||
this.webSocket.addEventListener('start', this.onWebSocketStart);
|
||||
this.webSocket.addEventListener('stop', this.onWebSocketStop);
|
||||
this.webSocketUnsubscriber = this.webSocket.subscribe(
|
||||
this.onWebSocketMessage
|
||||
);
|
||||
|
||||
this.loadDashboards();
|
||||
|
||||
if (!this.props.settings.OFFLINE_MODE) {
|
||||
this.webSocket.start();
|
||||
}
|
||||
}
|
||||
componentWillUnmount () {
|
||||
if (this.webSocketUnsubscriber) {
|
||||
this.webSocketUnsubscriber();
|
||||
this.webSocketUnsubscriber = null;
|
||||
}
|
||||
|
||||
if (this.webSocket) {
|
||||
this.webSocket.removeEventListener('start', this.onWebSocketStart);
|
||||
this.webSocket.removeEventListener('stop', this.onWebSocketStop);
|
||||
this.webSocket.stop();
|
||||
}
|
||||
}
|
||||
render () {
|
||||
const contextValue = {
|
||||
currentDashboardId: this.state.currentDashboardId,
|
||||
dashboards: this.state.dashboards,
|
||||
nukeService: this.nukeService,
|
||||
saveServiceCharacteristics: this.saveServiceCharacteristics,
|
||||
saveServiceLayout: this.saveServiceLayout,
|
||||
addService: this.addService,
|
||||
isLoading: this.state.isLoading,
|
||||
lastSaveTimestamp: this.state.lastSaveTimestamp,
|
||||
lastSaveError: this.state.lastSaveError,
|
||||
isSaving: this.state.isSaving,
|
||||
isWebSocketConnected: this.state.isWebSocketConnected,
|
||||
setCurrentDashboardId: this.setCurrentDashboardId,
|
||||
dashboardsHash: this.dashboardsHash,
|
||||
addDashboard: this.addDashboard,
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardsContext.Provider value={contextValue}>
|
||||
{this.state.isLoading && this.props.loader}
|
||||
{!this.state.isLoading && this.props.children}
|
||||
</DashboardsContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
1
packages/homehub_core/src/providers/index.js
Normal file
1
packages/homehub_core/src/providers/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './DashboardsProvider';
|
||||
11
packages/homehub_core/tests/__entry__.js
Normal file
11
packages/homehub_core/tests/__entry__.js
Normal file
@@ -0,0 +1,11 @@
|
||||
require('regenerator-runtime/runtime');
|
||||
require('whatwg-fetch');
|
||||
window.crypto = {
|
||||
getRandomValues: require('polyfill-crypto.getrandomvalues'),
|
||||
};
|
||||
|
||||
require('tests/__setup__/enzyme.setup.js');
|
||||
require('tests/__setup__/jasmine.setup.js');
|
||||
|
||||
let testsContext = require.context('.', true, /\.spec\.js$/);
|
||||
testsContext.keys().forEach(testsContext);
|
||||
51
packages/homehub_core/tests/__fixtures__/dashboards.js
Normal file
51
packages/homehub_core/tests/__fixtures__/dashboards.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import {FakeWidget, FakeService} from 'tests/__fixtures__/services';
|
||||
|
||||
export const DashboardsFactory = () => {
|
||||
return [
|
||||
{
|
||||
id: 'testing',
|
||||
name: 'Testing',
|
||||
services: [
|
||||
new FakeService({
|
||||
instance: 'fake_instance',
|
||||
widgetComponent: FakeWidget,
|
||||
characteristics: {
|
||||
spam: true,
|
||||
},
|
||||
layout: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 1,
|
||||
h: 1,
|
||||
},
|
||||
}),
|
||||
new FakeService({
|
||||
instance: 'other_fake_instance',
|
||||
widgetComponent: FakeWidget,
|
||||
characteristics: {
|
||||
spam: true,
|
||||
},
|
||||
layout: {
|
||||
x: 0,
|
||||
y: 1,
|
||||
w: 1,
|
||||
h: 1,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const DashboardsJSONFactory = (dashboards) => {
|
||||
dashboards = dashboards || DashboardsFactory();
|
||||
|
||||
return dashboards.map((dashboard) => {
|
||||
return {
|
||||
...dashboard,
|
||||
services: dashboard.services.map((service) => {
|
||||
return service.toJSON();
|
||||
}),
|
||||
};
|
||||
});
|
||||
};
|
||||
19
packages/homehub_core/tests/__fixtures__/services.js
Normal file
19
packages/homehub_core/tests/__fixtures__/services.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
import {BaseService} from 'src/lib/services';
|
||||
|
||||
export const FakeWidget = (props) => { // eslint-disable-line no-unused-vars
|
||||
return <span>FakeWidget</span>;
|
||||
};
|
||||
|
||||
export const FakeWidgetSettingsView = (props) => { // eslint-disable-line no-unused-vars
|
||||
return <span>FakeWidgetSettingsView</span>;
|
||||
};
|
||||
|
||||
FakeWidget.defaultLayout = {w: 1, h: 1};
|
||||
FakeWidget.settingsView = FakeWidgetSettingsView;
|
||||
|
||||
export class FakeService extends BaseService {
|
||||
static kind = 'FakeService';
|
||||
static widget = 'FakeWidget';
|
||||
}
|
||||
7
packages/homehub_core/tests/__setup__/enzyme.setup.js
Normal file
7
packages/homehub_core/tests/__setup__/enzyme.setup.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
Enzyme.configure({
|
||||
adapter: new Adapter(),
|
||||
disableLifecycleMethods: true,
|
||||
});
|
||||
22
packages/homehub_core/tests/__setup__/jasmine.setup.js
Normal file
22
packages/homehub_core/tests/__setup__/jasmine.setup.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
jasmine.getEnv().beforeAll(() => {
|
||||
jasmine.addMatchers({
|
||||
toBeUUIDv4: (util, customEqualityTesters) => {
|
||||
return {
|
||||
compare: (actual, expected) => {
|
||||
const reUUIDv4 = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
||||
|
||||
const result = {};
|
||||
result.pass = reUUIDv4.test(actual);
|
||||
|
||||
if (!result.pass) {
|
||||
result.message = `Expected ${actual} to be a UUIDv4`;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
51
packages/homehub_core/tests/api/services.spec.js
Normal file
51
packages/homehub_core/tests/api/services.spec.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as Services from 'src/api/services';
|
||||
import * as RPC from 'src/lib/rpc';
|
||||
|
||||
describe('src/api/services', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(RPC, 'callMethod').and.resolveTo('ok');
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('calls and RPC method to start a service', async () => {
|
||||
// When
|
||||
const result = await Services.start('FakeService', 'fake_instance', {
|
||||
'spam': true,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toEqual('ok');
|
||||
expect(RPC.callMethod).toHaveBeenCalledWith('services.start', [
|
||||
'FakeService', 'fake_instance', {'spam': true},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
it('calls and RPC method to stop a service', async () => {
|
||||
// When
|
||||
const result = await Services.stop('FakeService', 'fake_instance');
|
||||
|
||||
// Then
|
||||
expect(result).toEqual('ok');
|
||||
expect(RPC.callMethod).toHaveBeenCalledWith('services.stop', [
|
||||
'FakeService', 'fake_instance',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('use', () => {
|
||||
it('calls and RPC method to use a service capability', async () => {
|
||||
// When
|
||||
const result = await Services.use(
|
||||
'FakeService', 'fake_instance', 'testing', ['spam'],
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(result).toEqual('ok');
|
||||
expect(RPC.callMethod).toHaveBeenCalledWith('services.use', [
|
||||
'FakeService', 'fake_instance', 'testing', ['spam'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
32
packages/homehub_core/tests/api/state.spec.js
Normal file
32
packages/homehub_core/tests/api/state.spec.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as State from 'src/api/state';
|
||||
import * as RPC from 'src/lib/rpc';
|
||||
|
||||
describe('src/api/state', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(RPC, 'callMethod').and.resolveTo('ok');
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('calls and RPC method to get frontend state', async () => {
|
||||
// When
|
||||
const result = await State.get();
|
||||
|
||||
// Then
|
||||
expect(result).toEqual('ok');
|
||||
expect(RPC.callMethod).toHaveBeenCalledWith('state.get_frontend');
|
||||
});
|
||||
});
|
||||
|
||||
describe('save', () => {
|
||||
it('calls and RPC method to save frontend state', async () => {
|
||||
// When
|
||||
const result = await State.save('spam');
|
||||
|
||||
// Then
|
||||
expect(result).toEqual('ok');
|
||||
expect(RPC.callMethod).toHaveBeenCalledWith(
|
||||
'state.save_frontend', ['spam']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
750
packages/homehub_core/tests/containers/ServiceContainer.spec.js
Normal file
750
packages/homehub_core/tests/containers/ServiceContainer.spec.js
Normal file
@@ -0,0 +1,750 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {withContext} from 'shallow-with-context';
|
||||
|
||||
import {ServiceContainer} from 'src/containers/ServiceContainer';
|
||||
import {DEFAULT_DASHBOARDS_CONTEXT} from 'src/context/DashboardsContext';
|
||||
import {DummyService, ServiceState} from 'src/lib/services';
|
||||
|
||||
import {DashboardsFactory} from 'tests/__fixtures__/dashboards';
|
||||
|
||||
describe('src/containers/ServiceContainer', () => {
|
||||
const ServiceContainerWithContext = withContext(
|
||||
ServiceContainer, DEFAULT_DASHBOARDS_CONTEXT
|
||||
);
|
||||
|
||||
let context = null;
|
||||
|
||||
beforeEach(() => {
|
||||
context = {
|
||||
...DEFAULT_DASHBOARDS_CONTEXT,
|
||||
nukeService: jasmine.createSpy(),
|
||||
saveServiceCharacteristics: jasmine.createSpy(),
|
||||
saveServiceLayout: jasmine.createSpy(),
|
||||
addService: jasmine.createSpy(),
|
||||
setCurrentDashboardId: jasmine.createSpy(),
|
||||
addDashboard: jasmine.createSpy(),
|
||||
dashboards: DashboardsFactory(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('initializes the state', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(component.state('serviceState')).toBe(null);
|
||||
expect(component.state('showSettingsModal')).toBe(false);
|
||||
expect(component.state('nextCharacteristics')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('service', () => {
|
||||
it('looks up the bound service instance', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
// When
|
||||
const service = component.instance().service();
|
||||
|
||||
// Then
|
||||
expect(service).toEqual(context.dashboards[0].services[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setServiceState', () => {
|
||||
it('initializes new state object if the current state is `null`', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
// When
|
||||
component.instance().setServiceState({data: {spam: true}}, {});
|
||||
|
||||
// Then
|
||||
expect(component.state('serviceState')).toBeInstanceOf(ServiceState);
|
||||
expect(component.state('serviceState').data()).toEqual({spam: true});
|
||||
});
|
||||
|
||||
it('initializes new state object if reset is requested', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
const oldState = new ServiceState({data: {spam: false}});
|
||||
component.setState({serviceState: oldState});
|
||||
|
||||
// When
|
||||
component.instance().setServiceState(
|
||||
{data: {spam: true}}, {reset: true}
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(component.state('serviceState')).not.toBe(oldState);
|
||||
});
|
||||
|
||||
it('updates the existing state object with new payload', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
const oldState = new ServiceState({data: {spam: false}});
|
||||
component.setState({serviceState: oldState});
|
||||
|
||||
// When
|
||||
component.instance().setServiceState({data: {spam: true}}, {});
|
||||
|
||||
// Then
|
||||
expect(component.state('serviceState')).not.toBe(oldState);
|
||||
expect(component.state('serviceState').data()).toEqual({spam: true});
|
||||
});
|
||||
});
|
||||
|
||||
it('allows setting next characteristics', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
// When
|
||||
component.instance().setNextCharacteristics({spam: true});
|
||||
|
||||
// Then
|
||||
expect(component.state('nextCharacteristics')).toEqual({spam: true});
|
||||
});
|
||||
|
||||
describe('setNextCharacteristicsFromService', () => {
|
||||
it('resets the next characteristics if the service is null', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
component.setState({nextCharacteristics: {spam: false}});
|
||||
|
||||
// When
|
||||
component.instance().setNextCharacteristicsFromService(null);
|
||||
|
||||
// Then
|
||||
expect(component.state('nextCharacteristics')).toBe(null);
|
||||
});
|
||||
|
||||
it('resets the next characteristics if the service has no widget', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
component.setState({nextCharacteristics: {spam: false}});
|
||||
|
||||
const service = context.dashboards[0].services[0];
|
||||
service.widgetComponent = null;
|
||||
|
||||
// When
|
||||
component.instance().setNextCharacteristicsFromService(service);
|
||||
|
||||
// Then
|
||||
expect(component.state('nextCharacteristics')).toBe(null);
|
||||
});
|
||||
|
||||
it('resets the next characteristics if the service is dummy', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
component.setState({nextCharacteristics: {spam: false}});
|
||||
|
||||
const service = new DummyService();
|
||||
|
||||
// When
|
||||
component.instance().setNextCharacteristicsFromService(service);
|
||||
|
||||
// Then
|
||||
expect(component.state('nextCharacteristics')).toBe(null);
|
||||
});
|
||||
|
||||
it('sets the next characteristics from the service characteristics', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
const service = context.dashboards[0].services[0];
|
||||
|
||||
// When
|
||||
component.instance().setNextCharacteristicsFromService(service);
|
||||
|
||||
// Then
|
||||
expect(component.state('nextCharacteristics')).toEqual(
|
||||
service.characteristics
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showHideSettingsModal', () => {
|
||||
it('sets next characteristics from the service when modal is to be shown', () => {
|
||||
// Givem
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
spyOn(component.instance(), 'setNextCharacteristicsFromService');
|
||||
|
||||
// When
|
||||
component.instance().showHideSettingsModal(true);
|
||||
|
||||
// Then
|
||||
expect(component.instance().setNextCharacteristicsFromService).toHaveBeenCalledWith(
|
||||
context.dashboards[0].services[0]
|
||||
);
|
||||
});
|
||||
|
||||
it('resets next characteristics when modal is to be hidden', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
spyOn(component.instance(), 'setNextCharacteristicsFromService');
|
||||
|
||||
// When
|
||||
component.instance().showHideSettingsModal(false);
|
||||
|
||||
// Then
|
||||
expect(component.instance().setNextCharacteristicsFromService).toHaveBeenCalledWith(
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the state with new visibility flag', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
spyOn(component.instance(), 'setNextCharacteristicsFromService');
|
||||
|
||||
// When
|
||||
component.instance().showHideSettingsModal(true);
|
||||
|
||||
// Then
|
||||
expect(component.state('showSettingsModal')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSettingsButtonClick', () => {
|
||||
it('requests the settings modal to be shown', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
spyOn(component.instance(), 'showHideSettingsModal');
|
||||
|
||||
// When
|
||||
component.instance().onSettingsButtonClick();
|
||||
|
||||
// Then
|
||||
expect(component.instance().showHideSettingsModal).toHaveBeenCalledWith(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSettingsModalClose', () => {
|
||||
it('requests the settings modal to be hidden', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
spyOn(component.instance(), 'showHideSettingsModal');
|
||||
|
||||
// When
|
||||
component.instance().onSettingsModalClose();
|
||||
|
||||
// Then
|
||||
expect(component.instance().showHideSettingsModal).toHaveBeenCalledWith(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSettingsModalSaveButtonClick', () => {
|
||||
it('requests the next characteristics to be saved', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
component.setState({nextCharacteristics: {spam: true}});
|
||||
|
||||
// When
|
||||
component.instance().onSettingsModalSaveButtonClick();
|
||||
|
||||
// Then
|
||||
expect(context.saveServiceCharacteristics).toHaveBeenCalledWith(
|
||||
'FakeService', 'fake_instance', {spam: true}
|
||||
);
|
||||
});
|
||||
|
||||
it('closes the settings modal', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
spyOn(component.instance(), 'onSettingsModalClose');
|
||||
|
||||
// When
|
||||
component.instance().onSettingsModalSaveButtonClick();
|
||||
|
||||
// Then
|
||||
expect(component.instance().onSettingsModalClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSettingsModalNukeButtonClick', () => {
|
||||
it('requests the service to be nuked', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
// When
|
||||
component.instance().onSettingsModalNukeButtonClick();
|
||||
|
||||
// Then
|
||||
expect(context.nukeService).toHaveBeenCalledWith(
|
||||
'FakeService', 'fake_instance'
|
||||
);
|
||||
});
|
||||
|
||||
it('closes the settings modal', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
spyOn(component.instance(), 'onSettingsModalClose');
|
||||
|
||||
// When
|
||||
component.instance().onSettingsModalNukeButtonClick();
|
||||
|
||||
// Then
|
||||
expect(component.instance().onSettingsModalClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAppearancePopupColorChange', () => {
|
||||
it('requests the updated characteristics to be saved', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
// When
|
||||
component.instance().onAppearancePopupColorChange('red');
|
||||
|
||||
// Then
|
||||
expect(context.saveServiceCharacteristics).toHaveBeenCalledWith(
|
||||
'FakeService', 'fake_instance', {
|
||||
spam: true,
|
||||
appearance: {
|
||||
color: 'red',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onServiceRestartButtonClick', () => {
|
||||
it('requests the updated characteristics to be saved', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
const service = context.dashboards[0].services[0];
|
||||
spyOn(service, 'restart');
|
||||
|
||||
// When
|
||||
component.instance().onServiceRestartButtonClick();
|
||||
|
||||
// Then
|
||||
expect(service.restart).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsubscribeIfNeeded', () => {
|
||||
it('calls the unsubscribe callback', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
component.instance().serviceUnsubscribe = jasmine.createSpy();
|
||||
|
||||
// When
|
||||
component.instance().unsubscribeIfNeeded();
|
||||
|
||||
// Then
|
||||
expect(component.instance().serviceUnsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('componentDidMount', () => {
|
||||
it('is a noop if the service is null', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
spyOn(component.instance(), 'setServiceState');
|
||||
spyOn(component.instance(), 'unsubscribeIfNeeded');
|
||||
spyOn(component.instance(), 'service').and.returnValue(null);
|
||||
|
||||
// When
|
||||
component.instance().componentDidMount();
|
||||
|
||||
// Then
|
||||
expect(component.instance().setServiceState).not.toHaveBeenCalled();
|
||||
expect(component.instance().unsubscribeIfNeeded).not.toHaveBeenCalled();
|
||||
expect(component.instance().serviceUnsubscribe).toBe(null);
|
||||
});
|
||||
|
||||
it('is a noop if the service has no widget', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
spyOn(component.instance(), 'setServiceState');
|
||||
spyOn(component.instance(), 'unsubscribeIfNeeded');
|
||||
|
||||
context.dashboards[0].services[0].widgetComponent = null;
|
||||
|
||||
// When
|
||||
component.instance().componentDidMount();
|
||||
|
||||
// Then
|
||||
expect(component.instance().setServiceState).not.toHaveBeenCalled();
|
||||
expect(component.instance().unsubscribeIfNeeded).not.toHaveBeenCalled();
|
||||
expect(component.instance().serviceUnsubscribe).toBe(null);
|
||||
});
|
||||
|
||||
it('is a noop if the service is dummy', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
spyOn(component.instance(), 'setServiceState');
|
||||
spyOn(component.instance(), 'unsubscribeIfNeeded');
|
||||
spyOn(component.instance(), 'service').and.returnValue(
|
||||
new DummyService()
|
||||
);
|
||||
|
||||
// When
|
||||
component.instance().componentDidMount();
|
||||
|
||||
// Then
|
||||
expect(component.instance().setServiceState).not.toHaveBeenCalled();
|
||||
expect(component.instance().unsubscribeIfNeeded).not.toHaveBeenCalled();
|
||||
expect(component.instance().serviceUnsubscribe).toBe(null);
|
||||
});
|
||||
|
||||
it('sets itself up for the service', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
spyOn(component.instance(), 'setServiceState');
|
||||
spyOn(component.instance(), 'unsubscribeIfNeeded');
|
||||
|
||||
const service = context.dashboards[0].services[0];
|
||||
spyOn(service, 'start');
|
||||
|
||||
const initialState = new ServiceState({data: {initial: true}});
|
||||
spyOn(service, 'initialState').and.returnValue(initialState);
|
||||
|
||||
const fakeUnsubscribe = jasmine.createSpy();
|
||||
spyOn(service, 'subscribe').and.returnValue(fakeUnsubscribe);
|
||||
|
||||
// When
|
||||
component.instance().componentDidMount();
|
||||
|
||||
// Then
|
||||
expect(component.instance().setServiceState).toHaveBeenCalledWith(
|
||||
initialState, true
|
||||
);
|
||||
expect(service.start).toHaveBeenCalled();
|
||||
expect(component.instance().unsubscribeIfNeeded).toHaveBeenCalled();
|
||||
expect(service.subscribe).toHaveBeenCalledWith(
|
||||
component.instance().setServiceState
|
||||
);
|
||||
expect(component.instance().serviceUnsubscribe).toEqual(fakeUnsubscribe);
|
||||
});
|
||||
});
|
||||
|
||||
describe('componentWillUnmount', () => {
|
||||
it('unsubscribes from the service', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{(props) => <span>It works!</span>}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
const mockUnsubscribeIfNeeded = jasmine.createSpy();
|
||||
component.instance().unsubscribeIfNeeded = mockUnsubscribeIfNeeded;
|
||||
|
||||
// When
|
||||
component.unmount();
|
||||
|
||||
// Then
|
||||
expect(mockUnsubscribeIfNeeded).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
let childrenFunc = null;
|
||||
|
||||
beforeEach(() => {
|
||||
childrenFunc = jasmine.createSpy();
|
||||
childrenFunc.returnValue = <span>It works!</span>;
|
||||
});
|
||||
|
||||
it('calls the children func with props', () => {
|
||||
// Given
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{childrenFunc}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
const serviceState = new ServiceState({data: {spam: true}});
|
||||
component.setState({
|
||||
serviceState: serviceState,
|
||||
showSettingsModal: true,
|
||||
nextCharacteristics: {eggs: true},
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(childrenFunc).toHaveBeenCalledWith({
|
||||
hasSettingsView: true,
|
||||
service: context.dashboards[0].services[0],
|
||||
serviceState: serviceState,
|
||||
setServiceState: component.instance().setServiceState,
|
||||
settingsViewProps: {
|
||||
kind: 'FakeService',
|
||||
instance: 'fake_instance',
|
||||
nextCharacteristics: {eggs: true},
|
||||
setNextCharacteristics: component.instance().setNextCharacteristics,
|
||||
},
|
||||
showSettingsModal: true,
|
||||
onAppearancePopupColorChange: component.instance().onAppearancePopupColorChange,
|
||||
onServiceRestartButtonClick: component.instance().onServiceRestartButtonClick,
|
||||
onSettingsButtonClick: component.instance().onSettingsButtonClick,
|
||||
onSettingsModalClose: component.instance().onSettingsModalClose,
|
||||
onSettingsModalNukeButtonClick: component.instance().onSettingsModalNukeButtonClick,
|
||||
onSettingsModalSaveButtonClick: component.instance().onSettingsModalSaveButtonClick,
|
||||
});
|
||||
});
|
||||
|
||||
it('disables settings view if the service is dummy', () => {
|
||||
// Given
|
||||
const service = context.dashboards[0].services[0];
|
||||
spyOn(service, 'isDummy').and.returnValue(true);
|
||||
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{childrenFunc}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(childrenFunc).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||
hasSettingsView: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('disables settings view if the service has no widget', () => {
|
||||
// Given
|
||||
const service = context.dashboards[0].services[0];
|
||||
service.widgetComponent = null;
|
||||
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{childrenFunc}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(childrenFunc).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||
hasSettingsView: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('disables settings view if the service widget has no settings view', () => {
|
||||
// Given
|
||||
const service = context.dashboards[0].services[0];
|
||||
const oldSettingsView = service.widgetComponent.settingsView;
|
||||
service.widgetComponent.settingsView = undefined;
|
||||
|
||||
const component = shallow(
|
||||
<ServiceContainerWithContext kind="FakeService" instance="fake_instance">
|
||||
{childrenFunc}
|
||||
</ServiceContainerWithContext>,
|
||||
{
|
||||
context: context,
|
||||
}
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(childrenFunc).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||
hasSettingsView: false,
|
||||
}));
|
||||
|
||||
// After
|
||||
service.widgetComponent.settingsView = oldSettingsView;
|
||||
});
|
||||
});
|
||||
});
|
||||
304
packages/homehub_core/tests/hooks.spec.js
Normal file
304
packages/homehub_core/tests/hooks.spec.js
Normal file
@@ -0,0 +1,304 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import {mount} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
DEFAULT_DASHBOARDS_CONTEXT, DashboardsContext,
|
||||
} from 'src/context/DashboardsContext';
|
||||
import * as Hooks from 'src/hooks';
|
||||
|
||||
import {DashboardsFactory} from 'tests/__fixtures__/dashboards';
|
||||
|
||||
describe('src/hooks', () => {
|
||||
const HookWrapper = ({context, hook, hookArgs, output}) => {
|
||||
const Children = (props) => {
|
||||
output.result = hook(...hookArgs);
|
||||
return <span>It works!</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardsContext.Provider value={context}>
|
||||
<Children />
|
||||
</DashboardsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
let context = null;
|
||||
|
||||
beforeEach(() => {
|
||||
context = {
|
||||
...DEFAULT_DASHBOARDS_CONTEXT,
|
||||
dashboards: DashboardsFactory(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useServices', () => {
|
||||
it('returns the the current dashboard services', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
context = {
|
||||
...context,
|
||||
currentDashboardId: context.dashboards[0].id,
|
||||
};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useServices}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toEqual(context.dashboards[0].services);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useService', () => {
|
||||
it('returns the the specified service instance', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useService}
|
||||
hookArgs={['FakeService', 'fake_instance']}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toEqual(context.dashboards[0].services[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSaveServiceCharacteristics', () => {
|
||||
it('returns the saveServiceCharacteristics callback', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// WHen
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useSaveServiceCharacteristics}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toBe(context.saveServiceCharacteristics);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSaveServiceLayout', () => {
|
||||
it('returns the saveServiceLayout callback', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useSaveServiceLayout}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toBe(context.saveServiceLayout);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useNukeService', () => {
|
||||
it('returns the nukeService callback', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useNukeService}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toBe(context.nukeService);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAddService', () => {
|
||||
it('returns the addService callback', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useAddService}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toBe(context.addService);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useServicesSavingState', () => {
|
||||
it('returns the service saving state', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useServicesSavingState}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toEqual({
|
||||
lastSaveTimestamp: context.lastSaveTimestamp,
|
||||
lastSaveError: context.lastSaveError,
|
||||
isSaving: context.isSaving,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useWebSocketState', () => {
|
||||
it('returns the websocket state', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useWebSocketState}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toEqual({
|
||||
isWebSocketConnected: context.isWebSocketConnected,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDashboards', () => {
|
||||
it('returns the dashboards', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useDashboards}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toEqual(context.dashboards);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCurrentDashboardId', () => {
|
||||
it('returns the dashboards', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useCurrentDashboardId}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toEqual(context.currentDashboardId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSetCurrentDashboardId', () => {
|
||||
it('returns the setCurrentDashboardId callback', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useSetCurrentDashboardId}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toBe(context.setCurrentDashboardId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDashboardsHash', () => {
|
||||
it('returns the dashboards hash', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useDashboardsHash}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toEqual(context.dashboardsHash);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAddDashboard', () => {
|
||||
it('returns the addDashboard callback', () => {
|
||||
// Given
|
||||
const output = {};
|
||||
|
||||
// When
|
||||
const component = mount(
|
||||
<HookWrapper
|
||||
context={context}
|
||||
hook={Hooks.useAddDashboard}
|
||||
hookArgs={[]}
|
||||
output={output}
|
||||
/>
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(output.result).toBe(context.addDashboard);
|
||||
});
|
||||
});
|
||||
});
|
||||
242
packages/homehub_core/tests/lib/base.spec.js
Normal file
242
packages/homehub_core/tests/lib/base.spec.js
Normal file
@@ -0,0 +1,242 @@
|
||||
import * as BaseLib from 'src/lib/base';
|
||||
|
||||
describe('src/lib/base', () => {
|
||||
describe('SubscribableMixin', () => {
|
||||
const TestClass = class extends BaseLib.SubscribableMixin(BaseLib.HomeHubBaseClass) {
|
||||
};
|
||||
|
||||
describe('constructor', () => {
|
||||
it('initializes the instance', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
|
||||
// Then
|
||||
expect(mixin.subscribers).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('notify', () => {
|
||||
it('notifies the subscribers', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
|
||||
const mockSubscriber = jasmine.createSpy();
|
||||
mixin.subscribers.push(mockSubscriber);
|
||||
mixin.subscribers.push(null);
|
||||
|
||||
// When
|
||||
mixin.notify('test');
|
||||
|
||||
// Then
|
||||
expect(mockSubscriber).toHaveBeenCalledWith('test', {});
|
||||
});
|
||||
|
||||
it('allows sending a notification with user info', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
|
||||
const mockSubscriber = jasmine.createSpy();
|
||||
mixin.subscribers.push(mockSubscriber);
|
||||
|
||||
// When
|
||||
mixin.notify('test', {reset: true});
|
||||
|
||||
// Then
|
||||
expect(mockSubscriber).toHaveBeenCalledWith('test', {reset: true});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsubscriberFactory', () => {
|
||||
it('returns an unsubscriber function', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
|
||||
// When
|
||||
const result = mixin.unsubscriberFactory(0);
|
||||
|
||||
// Then
|
||||
expect(result).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it('configures the unsubscriber function to remove the bound subscriber', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
|
||||
const mockSubscriber1 = jasmine.createSpy();
|
||||
mixin.subscribers.push(mockSubscriber1);
|
||||
|
||||
const mockSubscriber2 = jasmine.createSpy();
|
||||
mixin.subscribers.push(mockSubscriber2);
|
||||
|
||||
const mockSubscriber3 = jasmine.createSpy();
|
||||
mixin.subscribers.push(mockSubscriber3);
|
||||
|
||||
const unsubscriber = mixin.unsubscriberFactory(1);
|
||||
|
||||
// When
|
||||
unsubscriber();
|
||||
|
||||
// Then
|
||||
expect(mixin.subscribers.length).toEqual(3);
|
||||
expect(mixin.subscribers[0]).toBe(mockSubscriber1);
|
||||
expect(mixin.subscribers[1]).toBe(null);
|
||||
expect(mixin.subscribers[2]).toBe(mockSubscriber3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
it('adds a subscriber and returns its unsubscriber', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
|
||||
const mockUnsubscriber = jasmine.createSpy();
|
||||
spyOn(mixin, 'unsubscriberFactory').and.returnValue = mockUnsubscriber;
|
||||
|
||||
const mockSubscriber = jasmine.createSpy();
|
||||
|
||||
// When
|
||||
mixin.subscribe(mockSubscriber);
|
||||
|
||||
// Then
|
||||
expect(mixin.subscribers.length).toEqual(1);
|
||||
expect(mixin.subscribers[0]).toBe(mockSubscriber);
|
||||
expect(mixin.unsubscriberFactory).toHaveBeenCalledWith(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('EventSouceMixin', () => {
|
||||
const Base = BaseLib.EventSourceMixin(BaseLib.HomeHubBaseClass, ['start']);
|
||||
const TestClass = class extends Base {
|
||||
};
|
||||
|
||||
describe('constructor', () => {
|
||||
it('initializes the instance', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
|
||||
// Then
|
||||
expect(mixin.eventListeners).toEqual({start: []});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addEventListener', () => {
|
||||
let mockListener = null;
|
||||
|
||||
beforeEach(() => {
|
||||
mockListener = jasmine.createSpy();
|
||||
});
|
||||
|
||||
it('ignores an uknown event', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
|
||||
// When
|
||||
mixin.addEventListener('testing', mockListener);
|
||||
|
||||
// Then
|
||||
expect(mixin.eventListeners.testing).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('adds a listener for an event', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
|
||||
// When
|
||||
mixin.addEventListener('start', mockListener);
|
||||
|
||||
// Then
|
||||
expect(mixin.eventListeners.start.length).toEqual(1);
|
||||
expect(mixin.eventListeners.start[0]).toBe(mockListener);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addEventListener', () => {
|
||||
let mockListener = null;
|
||||
|
||||
beforeEach(() => {
|
||||
mockListener = jasmine.createSpy();
|
||||
});
|
||||
|
||||
it('ignores an uknown event', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
mixin.addEventListener('start', mockListener);
|
||||
|
||||
// When
|
||||
mixin.removeEventListener('testing', mockListener);
|
||||
|
||||
// Then
|
||||
expect(mixin.eventListeners.testing).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('ignores an unknown listener', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
mixin.addEventListener('start', mockListener);
|
||||
|
||||
const mockListener2 = jasmine.createSpy();
|
||||
|
||||
// When
|
||||
mixin.removeEventListener('start', mockListener2);
|
||||
|
||||
// Then
|
||||
expect(mixin.eventListeners.start.length).toEqual(1);
|
||||
expect(mixin.eventListeners.start[0]).toBe(mockListener);
|
||||
});
|
||||
|
||||
it('removes a listener for an event', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
mixin.addEventListener('start', mockListener);
|
||||
|
||||
const mockListener2 = jasmine.createSpy();
|
||||
mixin.addEventListener('start', mockListener2);
|
||||
|
||||
const mockListener3 = jasmine.createSpy();
|
||||
mixin.addEventListener('start', mockListener3);
|
||||
|
||||
// When
|
||||
mixin.removeEventListener('start', mockListener2);
|
||||
|
||||
// Then
|
||||
expect(mixin.eventListeners.start.length).toEqual(3);
|
||||
expect(mixin.eventListeners.start[0]).toBe(mockListener);
|
||||
expect(mixin.eventListeners.start[1]).toBe(null);
|
||||
expect(mixin.eventListeners.start[2]).toBe(mockListener3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fireEvent', () => {
|
||||
let mockListener = null;
|
||||
|
||||
beforeEach(() => {
|
||||
mockListener = jasmine.createSpy();
|
||||
});
|
||||
|
||||
it('ignores an uknown event', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
mixin.addEventListener('start', mockListener);
|
||||
|
||||
// When
|
||||
mixin.fireEvent('testing', mockListener);
|
||||
|
||||
// Then
|
||||
expect(mockListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls event listeners for an event', () => {
|
||||
// Given
|
||||
const mixin = new TestClass();
|
||||
mixin.addEventListener('start', mockListener);
|
||||
|
||||
// When
|
||||
mixin.fireEvent('start', mockListener);
|
||||
|
||||
// Then
|
||||
expect(mockListener).toHaveBeenCalledWith(mixin);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
23
packages/homehub_core/tests/lib/dashboards.spec.js
Normal file
23
packages/homehub_core/tests/lib/dashboards.spec.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as DashboardsLib from 'src/lib/dashboards';
|
||||
|
||||
describe('src/lib/dashboards', () => {
|
||||
describe('DashboardFactory', () => {
|
||||
it('initializes the dashboard with defaults', () => {
|
||||
// Given
|
||||
const result = DashboardsLib.DashboardFactory();
|
||||
|
||||
// Then
|
||||
expect(result.name).toEqual(jasmine.any(String));
|
||||
expect(result.id).toBeUUIDv4();
|
||||
expect(result.services).toEqual([]);
|
||||
});
|
||||
|
||||
it('allows specifying an arbitrary name', () => {
|
||||
// Given
|
||||
const result = DashboardsLib.DashboardFactory('Testing');
|
||||
|
||||
// Then
|
||||
expect(result.name).toEqual('Testing');
|
||||
});
|
||||
});
|
||||
});
|
||||
15
packages/homehub_core/tests/lib/hashlib.spec.js
Normal file
15
packages/homehub_core/tests/lib/hashlib.spec.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as HashLib from 'src/lib/hashlib';
|
||||
|
||||
describe('src/lib/hashlib', () => {
|
||||
describe('sha256', () => {
|
||||
it('generates a hex digest of a message', async () => {
|
||||
// Given
|
||||
const result = await HashLib.sha256('spam');
|
||||
|
||||
// Then
|
||||
expect(result).toEqual(
|
||||
'f10c5a90947d4313c6af600facfb0c259444e4932a217fc341be0b3776f40933'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
48
packages/homehub_core/tests/lib/localStorage.spec.js
Normal file
48
packages/homehub_core/tests/lib/localStorage.spec.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as LocalStorageLib from 'src/lib/localStorage';
|
||||
|
||||
describe('src/lib/localStorage', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.setItem('test', '{"spam":true}');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.localStorage.removeItem('test');
|
||||
});
|
||||
|
||||
describe('getItem', () => {
|
||||
it('returns the parsed item', () => {
|
||||
// Given
|
||||
const result = LocalStorageLib.getItem('test');
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({'spam': true});
|
||||
});
|
||||
|
||||
it('returns default if the item is not present', () => {
|
||||
// Given
|
||||
const result = LocalStorageLib.getItem('test2', 'default');
|
||||
|
||||
// Then
|
||||
expect(result).toEqual('default');
|
||||
});
|
||||
|
||||
it('defaults the default to null', () => {
|
||||
// Given
|
||||
const result = LocalStorageLib.getItem('test3');
|
||||
|
||||
// Then
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setItem', () => {
|
||||
it('stores the serialized item', () => {
|
||||
// Given
|
||||
LocalStorageLib.setItem('test', {spam: false});
|
||||
|
||||
// Then
|
||||
const storedItem = window.localStorage.getItem('test');
|
||||
expect(storedItem).toEqual('{"spam":false}');
|
||||
});
|
||||
});
|
||||
});
|
||||
117
packages/homehub_core/tests/lib/rpc.spec.js
Normal file
117
packages/homehub_core/tests/lib/rpc.spec.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import * as RPCLib from 'src/lib/rpc';
|
||||
|
||||
describe('src/lib/rpc', () => {
|
||||
describe('callMethod', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(window, 'fetch');
|
||||
});
|
||||
|
||||
it('formats and sends a POST request to the backend RPC URL', async () => {
|
||||
// Given
|
||||
window.fetch.and.resolveTo({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: jasmine.createSpy().and.resolveTo({
|
||||
result: 'ok',
|
||||
}),
|
||||
});
|
||||
|
||||
// When
|
||||
await RPCLib.callMethod('testing', ['spam']);
|
||||
|
||||
// Then
|
||||
expect(window.fetch).toHaveBeenCalledWith('/backend/rpc', {
|
||||
method: 'POST',
|
||||
body: jasmine.any(String),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const callArgs = window.fetch.calls.argsFor(0);
|
||||
const body = JSON.parse(callArgs[1].body);
|
||||
expect(body.jsonrpc).toEqual('2.0');
|
||||
expect(body.method).toEqual('testing');
|
||||
expect(body.params).toEqual(['spam']);
|
||||
expect(body.id).toBeUUIDv4();
|
||||
});
|
||||
|
||||
it('formats a notification call', async () => {
|
||||
// Given
|
||||
window.fetch.and.resolveTo({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: jasmine.createSpy().and.resolveTo({
|
||||
result: 'ok',
|
||||
}),
|
||||
});
|
||||
|
||||
// When
|
||||
await RPCLib.callMethod('testing', ['spam'], true);
|
||||
|
||||
// Then
|
||||
const callArgs = window.fetch.calls.argsFor(0);
|
||||
const body = JSON.parse(callArgs[1].body);
|
||||
expect(body.id).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('returns an error is the response was not OK', async () => {
|
||||
// Given
|
||||
window.fetch.and.resolveTo({
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await RPCLib.callMethod('testing');
|
||||
|
||||
// Then
|
||||
expect(result).not.toContain('data');
|
||||
expect(result.error).toMatch('HTTP 400 Bad Request');
|
||||
});
|
||||
|
||||
it('returns an error is the response was a JSONRPC error', async () => {
|
||||
// Given
|
||||
window.fetch.and.resolveTo({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: jasmine.createSpy().and.resolveTo({
|
||||
error: {
|
||||
message: 'Internal error',
|
||||
data: 'Test',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await RPCLib.callMethod('testing');
|
||||
|
||||
// Then
|
||||
expect(result).not.toContain('data');
|
||||
expect(result.error).toMatch('RPC Error: Internal error Test');
|
||||
});
|
||||
|
||||
it('returns an result is the response was successful', async () => {
|
||||
// Given
|
||||
window.fetch.and.resolveTo({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: jasmine.createSpy().and.resolveTo({
|
||||
result: 'ok',
|
||||
}),
|
||||
});
|
||||
|
||||
// When
|
||||
const result = await RPCLib.callMethod('testing');
|
||||
|
||||
// Then
|
||||
expect(result.data).toEqual('ok');
|
||||
expect(result).not.toContain('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
495
packages/homehub_core/tests/lib/services.spec.js
Normal file
495
packages/homehub_core/tests/lib/services.spec.js
Normal file
@@ -0,0 +1,495 @@
|
||||
import * as ServicesLib from 'src/lib/services';
|
||||
|
||||
import {DashboardsFactory} from 'tests/__fixtures__/dashboards';
|
||||
import {FakeService, FakeWidget} from 'tests/__fixtures__/services';
|
||||
|
||||
describe('src/lib/services', () => {
|
||||
describe('ServiceState', () => {
|
||||
describe('constructor', () => {
|
||||
it('initializes the instance with a payload', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState({
|
||||
data: {
|
||||
spam: true,
|
||||
},
|
||||
error: {
|
||||
message: 'FIAL',
|
||||
},
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(serviceState.payload.data).toEqual({spam: true});
|
||||
expect(serviceState.payload.error).toEqual({message: 'FIAL'});
|
||||
});
|
||||
|
||||
it('initializes the instance with the default payload', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState();
|
||||
|
||||
// Then
|
||||
expect(serviceState.payload.data).toBe(null);
|
||||
expect(serviceState.payload.error).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLoading', () => {
|
||||
it('returns true if both data and error fields are null', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState();
|
||||
|
||||
// Then
|
||||
expect(serviceState.isLoading()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if data is null and error is not null', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState({
|
||||
error: {
|
||||
message: 'FIAL',
|
||||
},
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(serviceState.isLoading()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if data is not null and error is null', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState({
|
||||
data: {
|
||||
spam: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(serviceState.isLoading()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if both data and error fields are not null', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState({
|
||||
data: {
|
||||
spam: true,
|
||||
},
|
||||
error: {
|
||||
message: 'FIAL',
|
||||
},
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(serviceState.isLoading()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasData', () => {
|
||||
it('returns false if isLoading is true and data is not null', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState({
|
||||
data: {
|
||||
spam: true,
|
||||
},
|
||||
});
|
||||
spyOn(serviceState, 'isLoading').and.returnValue(true);
|
||||
|
||||
// Then
|
||||
expect(serviceState.hasData()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if isLoading is false and data is null', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState();
|
||||
spyOn(serviceState, 'isLoading').and.returnValue(true);
|
||||
|
||||
// Then
|
||||
expect(serviceState.hasData()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if isLoading is false and data is not null', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState({
|
||||
data: {
|
||||
spam: true,
|
||||
},
|
||||
});
|
||||
spyOn(serviceState, 'isLoading').and.returnValue(false);
|
||||
|
||||
// Then
|
||||
expect(serviceState.hasData()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasError', () => {
|
||||
it('returns false if isLoading is true and error is not null', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState({
|
||||
error: {
|
||||
message: 'FIAL',
|
||||
},
|
||||
});
|
||||
spyOn(serviceState, 'isLoading').and.returnValue(true);
|
||||
|
||||
// Then
|
||||
expect(serviceState.hasError()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if isLoading is false and error is null', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState();
|
||||
spyOn(serviceState, 'isLoading').and.returnValue(true);
|
||||
|
||||
// Then
|
||||
expect(serviceState.hasError()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if isLoading is false and error is not null', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState({
|
||||
error: {
|
||||
message: 'FIAL',
|
||||
},
|
||||
});
|
||||
spyOn(serviceState, 'isLoading').and.returnValue(false);
|
||||
|
||||
// Then
|
||||
expect(serviceState.hasError()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasFatalError', () => {
|
||||
it('returns true if hasData is false and hasError is true', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState();
|
||||
spyOn(serviceState, 'hasData').and.returnValue(false);
|
||||
spyOn(serviceState, 'hasError').and.returnValue(true);
|
||||
|
||||
// Then
|
||||
expect(serviceState.hasFatalError()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if hasData is true and hasError is true', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState();
|
||||
spyOn(serviceState, 'hasData').and.returnValue(true);
|
||||
spyOn(serviceState, 'hasError').and.returnValue(true);
|
||||
|
||||
// Then
|
||||
expect(serviceState.hasFatalError()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if hasData is true and hasError is false', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState();
|
||||
spyOn(serviceState, 'hasData').and.returnValue(true);
|
||||
spyOn(serviceState, 'hasError').and.returnValue(false);
|
||||
|
||||
// Then
|
||||
expect(serviceState.hasFatalError()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('returns a new ServiceState with updated payload', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState();
|
||||
|
||||
// When
|
||||
const result = serviceState.update({
|
||||
data: {
|
||||
spam: true,
|
||||
},
|
||||
error: {
|
||||
message: 'FIAL',
|
||||
},
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).not.toBe(serviceState);
|
||||
expect(result.payload.data).toEqual({spam: true});
|
||||
expect(result.payload.error).toEqual({message: 'FIAL'});
|
||||
expect(serviceState.payload).toEqual({
|
||||
data: null,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data', () => {
|
||||
it('returns the data payload field', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState({
|
||||
data: {
|
||||
spam: true,
|
||||
},
|
||||
error: {
|
||||
message: 'FIAL',
|
||||
},
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(serviceState.data()).toEqual(serviceState.payload.data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
it('returns the error payload field', () => {
|
||||
// Given
|
||||
const serviceState = new ServicesLib.ServiceState({
|
||||
data: {
|
||||
spam: true,
|
||||
},
|
||||
error: {
|
||||
message: 'FIAL',
|
||||
},
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(serviceState.error()).toEqual(serviceState.payload.error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('BaseService', () => {
|
||||
let spec = null;
|
||||
|
||||
beforeEach(() => {
|
||||
spec = {
|
||||
instance: 'fake_instance',
|
||||
characteristics: {
|
||||
spam: true,
|
||||
},
|
||||
widgetComponent: FakeWidget,
|
||||
layout: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 1,
|
||||
h: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('includes the subscribable mixin', () => {
|
||||
// Given
|
||||
const service = new FakeService(spec);
|
||||
|
||||
// Then
|
||||
expect(service.__mixins__).toContain('SubscribableMixin');
|
||||
});
|
||||
|
||||
describe('emptyCharacteristics', () => {
|
||||
it('returns the empty characteristics', () => {
|
||||
// Given
|
||||
const result = FakeService.emptyCharacteristics();
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('initializes the instance', () => {
|
||||
// Given
|
||||
const service = new FakeService(spec);
|
||||
|
||||
// Then
|
||||
expect(service.kind).toEqual(FakeService.kind);
|
||||
expect(service.instance).toEqual(spec.instance);
|
||||
expect(service.characteristics).toEqual(spec.characteristics);
|
||||
expect(service.widget).toEqual(FakeService.widget);
|
||||
expect(service.widgetComponent).toEqual(spec.widgetComponent);
|
||||
expect(service.layout).toEqual(spec.layout);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restart', () => {
|
||||
it('restarts the service', async () => {
|
||||
// Given
|
||||
const service = new FakeService(spec);
|
||||
spyOn(service, 'notify');
|
||||
spyOn(service, 'start').and.resolveTo(null);
|
||||
spyOn(service, 'stop').and.resolveTo(null);
|
||||
|
||||
// When
|
||||
await service.restart();
|
||||
|
||||
// Then
|
||||
expect(service.notify).toHaveBeenCalledWith(null, {reset: true});
|
||||
expect(service.stop).toHaveBeenCalledBefore(service.start);
|
||||
expect(service.start).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDummy', () => {
|
||||
it('returns false', () => {
|
||||
// Given
|
||||
const service = new FakeService(spec);
|
||||
|
||||
// Then
|
||||
expect(service.isDummy()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialState', () => {
|
||||
it('returns null', () => {
|
||||
// Given
|
||||
const service = new FakeService(spec);
|
||||
|
||||
// Then
|
||||
expect(service.initialState()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCharacteristics', () => {
|
||||
it('sets the new characteristics', () => {
|
||||
// Given
|
||||
const service = new FakeService(spec);
|
||||
|
||||
const newCharacteristics = {spam: false};
|
||||
|
||||
// When
|
||||
service.setCharacteristics(newCharacteristics);
|
||||
|
||||
// Then
|
||||
expect(service.characteristics).not.toBe(newCharacteristics);
|
||||
expect(service.characteristics).toEqual(newCharacteristics);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLayout', () => {
|
||||
it('sets the new layout', () => {
|
||||
// Given
|
||||
const service = new FakeService(spec);
|
||||
|
||||
const newLayout = {x: 1, y: 1, w: 2, h: 2};
|
||||
|
||||
// When
|
||||
service.setLayout(newLayout);
|
||||
|
||||
// Then
|
||||
expect(service.layout).not.toBe(newLayout);
|
||||
expect(service.layout).toEqual(newLayout);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toJSON', () => {
|
||||
it('returns a JSON-serializable representation of the service', () => {
|
||||
// Given
|
||||
const service = new FakeService(spec);
|
||||
|
||||
// When
|
||||
const result = service.toJSON();
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({
|
||||
kind: service.kind,
|
||||
instance: service.instance,
|
||||
characteristics: service.characteristics,
|
||||
layout: service.layout,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DummyService', () => {
|
||||
let spec = null;
|
||||
|
||||
beforeEach(() => {
|
||||
spec = {
|
||||
instance: 'fake_instance',
|
||||
characteristics: {
|
||||
spam: true,
|
||||
},
|
||||
widgetComponent: FakeWidget,
|
||||
layout: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 1,
|
||||
h: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('initializes the instance', () => {
|
||||
// Given
|
||||
const service = new ServicesLib.DummyService(spec);
|
||||
|
||||
// Then
|
||||
expect(service.instance).toBe(null);
|
||||
expect(service.widgetComponent).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDummy', () => {
|
||||
it('returns true', () => {
|
||||
// Given
|
||||
const service = new ServicesLib.DummyService(spec);
|
||||
|
||||
// Then
|
||||
expect(service.isDummy()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('lookupService', () => {
|
||||
let dashboards = null;
|
||||
|
||||
beforeEach(() => {
|
||||
dashboards = DashboardsFactory();
|
||||
});
|
||||
|
||||
it('returns DummyService instance if the specified kind does not exist', () => {
|
||||
// Given
|
||||
const result = ServicesLib.lookupService(
|
||||
dashboards, 'Testing', 'fake_instance'
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(result).toBeInstanceOf(ServicesLib.DummyService);
|
||||
expect(result.isDummy()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns DummyService instance if the specified instance does not exist', () => {
|
||||
// Given
|
||||
const result = ServicesLib.lookupService(
|
||||
dashboards, 'FakeService', 'testing'
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(result).toBeInstanceOf(ServicesLib.DummyService);
|
||||
expect(result.isDummy()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns the service that matches the specified kind and instace', () => {
|
||||
// Given
|
||||
const result = ServicesLib.lookupService(
|
||||
dashboards, 'FakeService', 'fake_instance'
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(result).toBe(dashboards[0].services[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lookupServices', () => {
|
||||
let dashboards = null;
|
||||
|
||||
beforeEach(() => {
|
||||
dashboards = DashboardsFactory();
|
||||
dashboards[0].services.push(new ServicesLib.DummyService());
|
||||
dashboards[0].services.push(new FakeService({
|
||||
instance: 'fake_instance2',
|
||||
}));
|
||||
});
|
||||
|
||||
it('returns all instances of services specified by kind', () => {
|
||||
// Givem
|
||||
const result = ServicesLib.lookupServices(dashboards, 'FakeService');
|
||||
|
||||
// Then
|
||||
expect(result.length).toEqual(3);
|
||||
expect(result[0]).toBe(dashboards[0].services[0]);
|
||||
expect(result[1]).toBe(dashboards[0].services[1]);
|
||||
expect(result[2]).toBe(dashboards[0].services[3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
268
packages/homehub_core/tests/lib/websocket.spec.js
Normal file
268
packages/homehub_core/tests/lib/websocket.spec.js
Normal file
@@ -0,0 +1,268 @@
|
||||
import * as WebSocketLib from 'src/lib/websocket';
|
||||
|
||||
describe('src/lib/websocket', () => {
|
||||
describe('HomeHubWebSocket', () => {
|
||||
const settings = {
|
||||
url: '/websocket',
|
||||
};
|
||||
|
||||
it('includes the subscribable mixin', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
|
||||
// Then
|
||||
expect(webSocket.__mixins__).toContain('SubscribableMixin');
|
||||
});
|
||||
|
||||
it('includes the event source mixin', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
|
||||
// Then
|
||||
expect(webSocket.__mixins__).toContain('EventSourceMixin');
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('initializes the instance', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
|
||||
// Then
|
||||
expect(webSocket.debug).toBe(false);
|
||||
expect(webSocket.settings).toEqual(settings);
|
||||
expect(webSocket.socket).toBe(null);
|
||||
expect(webSocket.reconnectTimeout).toBe(null);
|
||||
expect(webSocket.reconnectCounter).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logDebug', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(console, 'log');
|
||||
});
|
||||
|
||||
it('logs a message if debug is true', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(true, settings);
|
||||
|
||||
// When
|
||||
webSocket.logDebug('Testing');
|
||||
|
||||
// Then
|
||||
expect(console.log).toHaveBeenCalledWith('Testing'); // eslint-disable-line no-console
|
||||
});
|
||||
|
||||
it('does not log a message if debug is false', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
|
||||
// When
|
||||
webSocket.logDebug('Testing');
|
||||
|
||||
// Then
|
||||
expect(console.log).not.toHaveBeenCalled(); // eslint-disable-line no-console
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopReconnect', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(window, 'clearTimeout');
|
||||
});
|
||||
|
||||
it('stops the reconnect process', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
webSocket.reconnectTimeout = 123;
|
||||
webSocket.reconnectCounter = 5;
|
||||
|
||||
// When
|
||||
webSocket.stopReconnect();
|
||||
|
||||
// Then
|
||||
expect(webSocket.reconnectTimeout).toBe(null);
|
||||
expect(webSocket.reconnectCounter).toEqual(0);
|
||||
expect(window.clearTimeout).toHaveBeenCalledWith(123);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startReconnect', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(window, 'setTimeout').and.returnValue(123);
|
||||
});
|
||||
|
||||
it('starts the reconnect process', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
webSocket.reconnectCounter = 5;
|
||||
|
||||
// When
|
||||
webSocket.startReconnect();
|
||||
|
||||
// Then
|
||||
expect(webSocket.reconnectCounter).toEqual(6);
|
||||
expect(webSocket.reconnectTimeout).toEqual(123);
|
||||
expect(window.setTimeout).toHaveBeenCalledWith(webSocket.start, 1000);
|
||||
});
|
||||
|
||||
it('breaks the reconnect process when retry count reaches limit', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
webSocket.reconnectCounter = 30;
|
||||
spyOn(webSocket, 'stopReconnect');
|
||||
|
||||
// When
|
||||
let error = null;
|
||||
try {
|
||||
webSocket.startReconnect();
|
||||
} catch (exc) {
|
||||
error = exc;
|
||||
}
|
||||
|
||||
// Then
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(webSocket.stopReconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
let fakeWebSocket = null;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeWebSocket = jasmine.createSpyObj(['addEventListener', 'close']);
|
||||
spyOn(window, 'WebSocket').and.returnValue(fakeWebSocket);
|
||||
});
|
||||
|
||||
it('configures and opens the websocket connection', () => {
|
||||
// Given
|
||||
let fullSettings = {
|
||||
...settings,
|
||||
protocols: ['spam', 'eggs'],
|
||||
};
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(
|
||||
false, fullSettings
|
||||
);
|
||||
|
||||
// When
|
||||
webSocket.start();
|
||||
|
||||
// Then
|
||||
expect(window.WebSocket).toHaveBeenCalledWith(
|
||||
fullSettings.url, fullSettings.protocols
|
||||
);
|
||||
expect(fakeWebSocket.addEventListener).toHaveBeenCalledWith(
|
||||
'open', webSocket.onSocketOpen
|
||||
);
|
||||
expect(fakeWebSocket.addEventListener).toHaveBeenCalledWith(
|
||||
'close', webSocket.onSocketClose
|
||||
);
|
||||
expect(fakeWebSocket.addEventListener).toHaveBeenCalledWith(
|
||||
'message', webSocket.onSocketMessage
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
let fakeWebSocket = null;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeWebSocket = jasmine.createSpyObj(['addEventListener', 'close']);
|
||||
});
|
||||
|
||||
it('closes the websocket connection', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
webSocket.socket = fakeWebSocket;
|
||||
|
||||
// When
|
||||
webSocket.stop();
|
||||
|
||||
// Then
|
||||
expect(fakeWebSocket.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSocketOpen', () => {
|
||||
let fakeWebSocket = null;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeWebSocket = jasmine.createSpyObj(['addEventListener', 'close']);
|
||||
fakeWebSocket.readyState = 1;
|
||||
});
|
||||
|
||||
it('handles the open websocket event', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
webSocket.socket = fakeWebSocket;
|
||||
spyOn(webSocket, 'logDebug');
|
||||
spyOn(webSocket, 'stopReconnect');
|
||||
spyOn(webSocket, 'fireEvent');
|
||||
|
||||
// When
|
||||
webSocket.onSocketOpen();
|
||||
|
||||
// Then
|
||||
expect(webSocket.logDebug).toHaveBeenCalled();
|
||||
expect(webSocket.stopReconnect).toHaveBeenCalled();
|
||||
expect(webSocket.fireEvent).toHaveBeenCalledWith('start');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSocketClose', () => {
|
||||
it('handles the open websocket event', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
spyOn(webSocket, 'logDebug');
|
||||
spyOn(webSocket, 'startReconnect');
|
||||
spyOn(webSocket, 'fireEvent');
|
||||
|
||||
// When
|
||||
webSocket.onSocketClose({code: 1000});
|
||||
|
||||
// Then
|
||||
expect(webSocket.logDebug).toHaveBeenCalled();
|
||||
expect(webSocket.startReconnect).toHaveBeenCalled();
|
||||
expect(webSocket.fireEvent).toHaveBeenCalledWith('stop');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSocketMessage', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(console, 'error');
|
||||
});
|
||||
|
||||
it('gracefully handles JSON parse error', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
spyOn(webSocket, 'notify');
|
||||
|
||||
// When
|
||||
webSocket.onSocketMessage({data: 'spam'});
|
||||
|
||||
// Then
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
jasmine.any(String), jasmine.any(Error)
|
||||
);
|
||||
expect(webSocket.notify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('parses the event data and notifies the subscribers', () => {
|
||||
// Given
|
||||
const webSocket = new WebSocketLib.HomeHubWebSocket(false, settings);
|
||||
spyOn(webSocket, 'notify');
|
||||
|
||||
const message = {
|
||||
type: 'TESTING',
|
||||
data: {
|
||||
spam: true,
|
||||
},
|
||||
};
|
||||
|
||||
// When
|
||||
webSocket.onSocketMessage({data: JSON.stringify(message)});
|
||||
|
||||
// Then
|
||||
expect(webSocket.notify).toHaveBeenCalledWith(message);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
1280
packages/homehub_core/tests/providers/DashboardsProvider.spec.js
Normal file
1280
packages/homehub_core/tests/providers/DashboardsProvider.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
107
packages/homehub_core/webpack.config.js
Executable file
107
packages/homehub_core/webpack.config.js
Executable file
@@ -0,0 +1,107 @@
|
||||
const path = require('path');
|
||||
|
||||
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const CONTEXT = path.resolve(__dirname);
|
||||
const SCSS_DIR = path.resolve(CONTEXT, 'scss');
|
||||
const PROJECT_DIR = path.resolve(CONTEXT, 'src');
|
||||
const OUTPUT_DIR = path.resolve(CONTEXT, 'lib');
|
||||
|
||||
const IS_PRODUCTION = (process.env['NODE_ENV'] === 'production');
|
||||
|
||||
const config = {
|
||||
mode: 'development',
|
||||
devtool: 'source-map',
|
||||
context: CONTEXT,
|
||||
entry: {
|
||||
index: './src/index.js',
|
||||
},
|
||||
output: {
|
||||
path: OUTPUT_DIR,
|
||||
filename: '[name].js',
|
||||
library: 'homehub-core',
|
||||
libraryTarget: 'umd',
|
||||
umdNamedDefine: true,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js?$/,
|
||||
include: PROJECT_DIR,
|
||||
use: [
|
||||
'babel-loader',
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss?$/,
|
||||
include: PROJECT_DIR,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
'sass-loader',
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css?$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(svg)$/,
|
||||
include: PROJECT_DIR,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
{
|
||||
loader: 'react-svg-loader',
|
||||
options: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(png)$/,
|
||||
include: PROJECT_DIR,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
include: SCSS_DIR,
|
||||
use: [
|
||||
'null-loader',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'scss': SCSS_DIR,
|
||||
'src': PROJECT_DIR,
|
||||
},
|
||||
},
|
||||
externals: {
|
||||
'prop-types': 'prop-types',
|
||||
react: 'react',
|
||||
'react-dom': 'react-dom',
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
PRODUCTION: IS_PRODUCTION,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
7341
packages/homehub_core/yarn.lock
Normal file
7341
packages/homehub_core/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user