Compute state display tests (#643)

* Move computeDomain and format functions to js

* Add tests for computeStateDisplay

* Always recalculate state display

* Remove LANGUAGE from hassUtils object

* Move AppLocalizeBehavior import to mixins

* Import mixins to state-card-display

* Safety check on computeStateDisplay

* Don't store computed domains on stateObj

* Integration tests for state-card-display

* Include extractDomain code in polymer repo

* Remove util function null checking

* Dont render test element without hass and stateObj

* Revert "Don't store computed domains on stateObj"

This reverts commit e3509d7182.

* Revert "Always recalculate state display"

This reverts commit 27c24e2694.
This commit is contained in:
Adam Mills 2017-11-21 00:46:36 -05:00 committed by Paulus Schoutsen
parent 7e77a7c32c
commit 3412edb843
20 changed files with 476 additions and 141 deletions

View File

@ -0,0 +1,7 @@
export default function computeDomain(stateObj) {
if (!stateObj._domain) {
stateObj._domain = stateObj.entity_id.substr(0, stateObj.entity_id.indexOf('.'));
}
return stateObj._domain;
}

View File

@ -0,0 +1,59 @@
import computeDomain from './compute_domain';
import formatDateTime from './format_date_time';
import formatDate from './format_date';
import formatTime from './format_time';
export default function computeStateDisplay(haLocalize, stateObj, language) {
if (!stateObj._stateDisplay) {
const domain = computeDomain(stateObj);
if (domain === 'binary_sensor') {
// Try device class translation, then default binary sensor translation
if (stateObj.attributes.device_class) {
stateObj._stateDisplay =
haLocalize(`state.${domain}.${stateObj.attributes.device_class}`, stateObj.state);
}
if (!stateObj._stateDisplay) {
stateObj._stateDisplay = haLocalize(`state.${domain}.default`, stateObj.state);
}
} else if (stateObj.attributes.unit_of_measurement) {
stateObj._stateDisplay = stateObj.state + ' ' + stateObj.attributes.unit_of_measurement;
} else if (domain === 'input_datetime') {
let date;
if (!stateObj.attributes.has_time) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day
);
stateObj._stateDisplay = formatDate(date, language);
} else if (!stateObj.attributes.has_date) {
date = new Date(
1970, 0, 1,
stateObj.attributes.hour,
stateObj.attributes.minute
);
stateObj._stateDisplay = formatTime(date, language);
} else {
date = new Date(
stateObj.attributes.year, stateObj.attributes.month - 1,
stateObj.attributes.day, stateObj.attributes.hour,
stateObj.attributes.minute
);
stateObj._stateDisplay = formatDateTime(date, language);
}
} else if (domain === 'zwave') {
if (['initializing', 'dead'].includes(stateObj.state)) {
stateObj._stateDisplay = haLocalize('state.zwave.query_stage', stateObj.state, 'query_stage', stateObj.attributes.query_stage);
} else {
stateObj._stateDisplay = haLocalize('state.zwave.default', stateObj.state);
}
} else {
stateObj._stateDisplay = haLocalize(`state.${domain}`, stateObj.state);
}
// Fall back to default or raw state if nothing else matches.
stateObj._stateDisplay = stateObj._stateDisplay
|| haLocalize('state.default', stateObj.state) || stateObj.state;
}
return stateObj._stateDisplay;
}

View File

@ -0,0 +1,19 @@
// Check for support of native locale string options
function toLocaleDateStringSupportsOptions() {
try {
new Date().toLocaleDateString('i');
} catch (e) {
return e.name === 'RangeError';
}
return false;
}
export default (toLocaleDateStringSupportsOptions() ?
function (dateObj, locales) {
return dateObj.toLocaleDateString(
locales,
{ year: 'numeric', month: 'long', day: 'numeric' },
);
} : function (dateObj, locales) { // eslint-disable-line no-unused-vars
return window.fecha.format(dateObj, 'mediumDate');
});

View File

@ -0,0 +1,22 @@
// Check for support of native locale string options
function toLocaleStringSupportsOptions() {
try {
new Date().toLocaleString('i');
} catch (e) {
return e.name === 'RangeError';
}
return false;
}
export default (toLocaleStringSupportsOptions() ?
function (dateObj, locales) {
return dateObj.toLocaleString(locales, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
} : function (dateObj, locales) { // eslint-disable-line no-unused-vars
return window.fecha.format(dateObj, 'haDateTime');
});

View File

@ -0,0 +1,19 @@
// Check for support of native locale string options
function toLocaleTimeStringSupportsOptions() {
try {
new Date().toLocaleTimeString('i');
} catch (e) {
return e.name === 'RangeError';
}
return false;
}
export default (toLocaleTimeStringSupportsOptions() ?
function (dateObj, locales) {
return dateObj.toLocaleTimeString(
locales,
{ hour: 'numeric', minute: '2-digit' }
);
} : function (dateObj, locales) { // eslint-disable-line no-unused-vars
return window.fecha.format(dateObj, 'shortTime');
});

View File

@ -7,7 +7,22 @@
*/
import attributeClassNames from './common/util/attribute_class_names';
import computeDomain from './common/util/compute_domain';
import computeStateDisplay from './common/util/compute_state_display';
import formatDate from './common/util/format_date';
import formatDateTime from './common/util/format_date_time';
import formatTime from './common/util/format_time';
window.hassUtil = window.hassUtil || {};
const language = navigator.languages ?
navigator.languages[0] : navigator.language || navigator.userLanguage;
window.fecha.masks.haDateTime = window.fecha.masks.shortTime + ' ' + window.fecha.masks.mediumDate;
window.hassUtil.attributeClassNames = attributeClassNames;
window.hassUtil.computeDomain = computeDomain;
window.hassUtil.computeStateDisplay = computeStateDisplay;
window.hassUtil.formatDate = dateObj => formatDate(dateObj, language);
window.hassUtil.formatDateTime = dateObj => formatDateTime(dateObj, language);
window.hassUtil.formatTime = dateObj => formatTime(dateObj, language);

View File

@ -7,6 +7,8 @@
<link rel="import" href="../cards/ha-badges-card.html">
<link rel="import" href="../cards/ha-card-chooser.html">
<link rel="import" href="../util/hass-util.html">
<dom-module id="ha-cards">
<template>
<style is="custom-style" include="iron-flex iron-flex-factors"></style>

View File

@ -2,7 +2,6 @@
<script>
Polymer.setPassiveTouchGestures(true);
</script>
<link rel='import' href='../bower_components/app-localize-behavior/app-localize-behavior.html'>
<link rel='import' href='./util/roboto.html'>
<link rel='import' href='../bower_components/paper-styles/typography.html'>
<link rel='import' href='../bower_components/iron-flex-layout/iron-flex-layout-classes.html'>

View File

@ -3,6 +3,8 @@
<link rel="import" href="../../bower_components/iron-flex-layout/iron-flex-layout-classes.html">
<link rel="import" href="../components/entity/state-info.html">
<link rel="import" href="../util/hass-mixins.html">
<link rel="import" href="../util/hass-util.html">
<dom-module id="state-card-display">
<template>
@ -20,7 +22,7 @@
<div class='horizontal justified layout'>
<state-info state-obj="[[stateObj]]" in-dialog='[[inDialog]]'></state-info>
<div class='state'>[[computeStateDisplay(haLocalize, stateObj)]]</div>
<div class='state'>[[computeStateDisplay(haLocalize, stateObj, language)]]</div>
</div>
</template>
</dom-module>
@ -40,55 +42,8 @@ class StateCardDisplay extends window.hassMixins.LocalizeMixin(Polymer.Element)
};
}
computeStateDisplay(haLocalize, stateObj) {
if (!stateObj._stateDisplay) {
const domain = window.hassUtil.computeDomain(stateObj);
if (domain === 'binary_sensor') {
// Try device class translation, then default binary sensor translation
stateObj._stateDisplay =
haLocalize(`state.${domain}.${stateObj.attributes.device_class}`, stateObj.state)
|| haLocalize(`state.${domain}.default`, stateObj.state);
} else if (stateObj.attributes.unit_of_measurement) {
stateObj._stateDisplay = stateObj.state + ' ' + stateObj.attributes.unit_of_measurement;
} else if (domain === 'input_datetime') {
let date;
if (!stateObj.attributes.has_time) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day
);
stateObj._stateDisplay = window.hassUtil.formatDate(date);
} else if (!stateObj.attributes.has_date) {
date = new Date(
1970, 0, 1,
stateObj.attributes.hour,
stateObj.attributes.minute
);
stateObj._stateDisplay = window.hassUtil.formatTime(date);
} else {
date = new Date(
stateObj.attributes.year, stateObj.attributes.month - 1,
stateObj.attributes.day, stateObj.attributes.hour,
stateObj.attributes.minute
);
stateObj._stateDisplay = window.hassUtil.formatDateTime(date);
}
} else if (domain === 'zwave') {
if (['initializing', 'dead'].includes(stateObj.state)) {
stateObj._stateDisplay = haLocalize('state.zwave.query_stage', stateObj.state, 'query_stage', stateObj.attributes.query_stage);
} else {
stateObj._stateDisplay = haLocalize('state.zwave.default', stateObj.state);
}
} else {
stateObj._stateDisplay = haLocalize(`state.${domain}`, stateObj.state);
}
// Fall back to default or raw state if nothing else matches.
stateObj._stateDisplay = stateObj._stateDisplay
|| haLocalize('state.default', stateObj.state) || stateObj.state;
}
return stateObj._stateDisplay;
computeStateDisplay(haLocalize, stateObj, language) {
return window.hassUtil.computeStateDisplay(haLocalize, stateObj, language);
}
}
customElements.define(StateCardDisplay.is, StateCardDisplay);

View File

@ -1,3 +1,5 @@
<link rel='import' href='../../bower_components/app-localize-behavior/app-localize-behavior.html'>
<script>
// Polymer legacy event helpers used courtesy of the Polymer project.

View File

@ -36,9 +36,6 @@ window.hassUtil.HIDE_MORE_INFO = [
'input_select', 'scene', 'input_number', 'input_text'
];
window.hassUtil.LANGUAGE = navigator.languages ?
navigator.languages[0] : navigator.language || navigator.userLanguage;
// Expects featureClassNames to be an object mapping feature-bit -> className
window.hassUtil.featureClassNames = function (stateObj, featureClassNames) {
if (!stateObj || !stateObj.attributes.supported_features) return '';
@ -107,77 +104,6 @@ window.hassUtil.dynamicContentUpdater = function (root, newElementTag, attribute
}
};
// Check for support of native locale string options
function toLocaleStringSupportsOptions() {
try {
new Date().toLocaleString('i');
} catch (e) {
return e.name === 'RangeError';
}
return false;
}
function toLocaleDateStringSupportsOptions() {
try {
new Date().toLocaleDateString('i');
} catch (e) {
return e.name === 'RangeError';
}
return false;
}
function toLocaleTimeStringSupportsOptions() {
try {
new Date().toLocaleTimeString('i');
} catch (e) {
return e.name === 'RangeError';
}
return false;
}
window.fecha.masks.haDateTime = (window.fecha.masks.shortTime + ' ' +
window.fecha.masks.mediumDate);
if (toLocaleStringSupportsOptions()) {
window.hassUtil.formatDateTime = function (dateObj) {
return dateObj.toLocaleString(window.hassUtil.LANGUAGE, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
};
} else {
window.hassUtil.formatDateTime = function (dateObj) {
return window.fecha.format(dateObj, 'haDateTime');
};
}
if (toLocaleDateStringSupportsOptions()) {
window.hassUtil.formatDate = function (dateObj) {
return dateObj.toLocaleDateString(
window.hassUtil.LANGUAGE,
{ year: 'numeric', month: 'long', day: 'numeric' }
);
};
} else {
window.hassUtil.formatDate = function (dateObj) {
return window.fecha.format(dateObj, 'mediumDate');
};
}
if (toLocaleTimeStringSupportsOptions()) {
window.hassUtil.formatTime = function (dateObj) {
return dateObj.toLocaleTimeString(
window.hassUtil.LANGUAGE,
{ hour: 'numeric', minute: '2-digit' }
);
};
} else {
window.hassUtil.formatTime = function (dateObj) {
return window.fecha.format(dateObj, 'shortTime');
};
}
window.hassUtil.relativeTime = function (dateObj) {
var delta = Math.abs(new Date() - dateObj) / 1000;
var format = new Date() > dateObj ? '%s ago' : 'in %s';
@ -437,14 +363,6 @@ window.hassUtil.stateIcon = function (state) {
return window.hassUtil.domainIcon(domain, state.state);
};
window.hassUtil.computeDomain = function (stateObj) {
if (!stateObj._domain) {
stateObj._domain = window.HAWS.extractDomain(stateObj.entity_id);
}
return stateObj._domain;
};
window.hassUtil.computeObjectId = function (stateObj) {
if (!stateObj._object_id) {
stateObj._object_id = window.HAWS.extractObjectId(stateObj.entity_id);

View File

@ -5,14 +5,14 @@ const assert = require('assert');
describe('attributeClassNames', function() {
const attrs = ['mock_attr1', 'mock_attr2'];
it('null state', function() {
it('Skips null states', function() {
const stateObj = null;
assert.strictEqual(
attributeClassNames(stateObj, attrs),
'');
});
it('matches no attrbutes', function() {
it('Matches no attrbutes', function() {
const stateObj = {
attributes: {
other_attr_1: 1,
@ -24,7 +24,7 @@ describe('attributeClassNames', function() {
'');
});
it('matches one attrbute', function() {
it('Matches one attrbute', function() {
const stateObj = {
attributes: {
other_attr_1: 1,
@ -37,7 +37,7 @@ describe('attributeClassNames', function() {
'has-mock_attr1');
});
it('matches two attrbutes', function() {
it('Matches two attrbutes', function() {
const stateObj = {
attributes: {
other_attr_1: 1,

View File

@ -0,0 +1,12 @@
import computeDomain from '../../../js/common/util/compute_domain';
const assert = require('assert');
describe('computeDomain', function() {
it('Detects sensor domain', function() {
const stateObj = {
entity_id: 'sensor.test',
};
assert.strictEqual(computeDomain(stateObj), 'sensor');
});
});

View File

@ -0,0 +1,184 @@
import computeStateDisplay from '../../../js/common/util/compute_state_display';
const assert = require('assert');
describe('computeStateDisplay', function() {
const haLocalize = function(namespace, message, ...args) {
// Mock Localize function for testing
return namespace + '.' + message + (args.length ? ': ' + args.join(',') : '');
};
it('Localizes binary sensor defaults', function() {
const stateObj = {
entity_id: 'binary_sensor.test',
state: 'off',
attributes: {
},
};
assert.strictEqual(computeStateDisplay(haLocalize, stateObj, 'en'), 'state.binary_sensor.default.off');
});
it('Localizes binary sensor device class', function() {
const stateObj = {
entity_id: 'binary_sensor.test',
state: 'off',
attributes: {
device_class: 'moisture',
},
};
assert.strictEqual(computeStateDisplay(haLocalize, stateObj, 'en'), 'state.binary_sensor.moisture.off');
});
it('Localizes binary sensor invalid device class', function() {
const altHaLocalize = function(namespace, message, ...args) {
if (namespace === 'state.binary_sensor.invalid_device_class') return null;
return haLocalize(namespace, message, ...args);
};
const stateObj = {
entity_id: 'binary_sensor.test',
state: 'off',
attributes: {
device_class: 'invalid_device_class',
},
};
assert.strictEqual(computeStateDisplay(altHaLocalize, stateObj, 'en'), 'state.binary_sensor.default.off');
});
it('Localizes sensor value with units', function() {
const stateObj = {
entity_id: 'sensor.test',
state: '123',
attributes: {
unit_of_measurement: 'm',
},
};
assert.strictEqual(computeStateDisplay(haLocalize, stateObj, 'en'), '123 m');
});
it('Localizes input_datetime with full date time', function() {
const stateObj = {
entity_id: 'input_datetime.test',
state: '123',
attributes: {
has_date: true,
has_time: true,
year: 2017,
month: 11,
day: 18,
hour: 11,
minute: 12,
second: 13,
},
};
assert.strictEqual(computeStateDisplay(haLocalize, stateObj, 'en'), 'November 18, 2017, 11:12 AM');
});
it('Localizes input_datetime with date', function() {
const stateObj = {
entity_id: 'input_datetime.test',
state: '123',
attributes: {
has_date: true,
has_time: false,
year: 2017,
month: 11,
day: 18,
hour: 11,
minute: 12,
second: 13,
},
};
assert.strictEqual(computeStateDisplay(haLocalize, stateObj, 'en'), 'November 18, 2017');
});
it('Localizes input_datetime with time', function() {
const stateObj = {
entity_id: 'input_datetime.test',
state: '123',
attributes: {
has_date: false,
has_time: true,
year: 2017,
month: 11,
day: 18,
hour: 11,
minute: 12,
second: 13,
},
};
assert.strictEqual(computeStateDisplay(haLocalize, stateObj, 'en'), '11:12 AM');
});
it('Localizes zwave ready', function() {
const stateObj = {
entity_id: 'zwave.test',
state: 'ready',
attributes: {
query_stage: 'Complete',
},
};
assert.strictEqual(computeStateDisplay(haLocalize, stateObj, 'en'), 'state.zwave.default.ready');
});
it('Localizes zwave initializing', function() {
const stateObj = {
entity_id: 'zwave.test',
state: 'initializing',
attributes: {
query_stage: 'Probe',
},
};
assert.strictEqual(computeStateDisplay(haLocalize, stateObj, 'en'), 'state.zwave.query_stage.initializing: query_stage,Probe');
});
it('Localizes cover open', function() {
const stateObj = {
entity_id: 'cover.test',
state: 'open',
attributes: {
},
};
assert.strictEqual(computeStateDisplay(haLocalize, stateObj, 'en'), 'state.cover.open');
});
it('Localizes unavailable', function() {
const altHaLocalize = function(namespace, message, ...args) {
if (namespace === 'state.sensor') return null;
return haLocalize(namespace, message, ...args);
};
const stateObj = {
entity_id: 'sensor.test',
state: 'unavailable',
attributes: {
},
};
assert.strictEqual(computeStateDisplay(altHaLocalize, stateObj, 'en'), 'state.default.unavailable');
});
it('Localizes custom state', function() {
const altHaLocalize = function(namespace, message, ...args) {
// No matches can be found
return null;
};
const stateObj = {
entity_id: 'sensor.test',
state: 'My Custom State',
attributes: {
},
};
assert.strictEqual(computeStateDisplay(altHaLocalize, stateObj, 'en'), 'My Custom State');
});
it('Only calculates state display once per immutable state object', function() {
const stateObj = {
entity_id: 'cover.test',
state: 'open',
attributes: {
},
};
assert.strictEqual(computeStateDisplay(haLocalize, stateObj, 'en'), 'state.cover.open');
stateObj.state = 'closing';
assert.strictEqual(computeStateDisplay(haLocalize, stateObj, 'en'), 'state.cover.open');
});
});

View File

@ -0,0 +1,20 @@
import formatDate from '../../../js/common/util/format_date';
const assert = require('assert');
describe('formatDate', function() {
const dateObj = new Date(
2017, 10, 18,
11, 12, 13, 1400,
);
it('Formats English dates', function() {
assert.strictEqual(formatDate(dateObj, 'en'), 'November 18, 2017');
});
// Node only contains intl support for english formats. This test at least ensures
// the fallback to a different locale
it('Formats other dates', function() {
assert.strictEqual(formatDate(dateObj, 'fr'), '2017 M11 18');
});
});

View File

@ -0,0 +1,20 @@
import formatDateTime from '../../../js/common/util/format_date_time';
const assert = require('assert');
describe('formatDateTime', function() {
const dateObj = new Date(
2017, 10, 18,
11, 12, 13, 1400,
);
it('Formats English date times', function() {
assert.strictEqual(formatDateTime(dateObj, 'en'), 'November 18, 2017, 11:12 AM');
});
// Node only contains intl support for english formats. This test at least ensures
// the fallback to a different locale
it('Formats other date times', function() {
assert.strictEqual(formatDateTime(dateObj, 'fr'), '2017 M11 18 11:12');
});
});

View File

@ -0,0 +1,20 @@
import formatTime from '../../../js/common/util/format_time';
const assert = require('assert');
describe('formatTime', function() {
const dateObj = new Date(
2017, 10, 18,
11, 12, 13, 1400,
);
it('Formats English times', function() {
assert.strictEqual(formatTime(dateObj, 'en'), '11:12 AM');
});
// Node only contains intl support for english formats. This test at least ensures
// the fallback to a different locale
it('Formats other times', function() {
assert.strictEqual(formatTime(dateObj, 'fr'), '11:12');
});
});

View File

@ -11,6 +11,8 @@
WCT.loadSuites([
'state-info-test.html?dom=shadow',
'state-info-test.html?dom=shady',
'state-card-display-test.html?dom=shadow',
'state-card-display-test.html?dom=shady',
]);
</script>
</body></html>

View File

@ -0,0 +1,64 @@
<!doctype html>
<html>
<head>
<script src="../../webcomponentsjs/webcomponents-lite.js"></script>
<script src="../../web-component-tester/browser.js"></script>
<!--
Temporarily load core.js here so window.HAWS is available. We can remove
this once hass-util includes the helper function directly.
-->
<script src="../build/core.js"></script>
<link rel="import" href="../src/state-summary/state-card-display.html">
</head>
<body>
<test-fixture id="stateCardDisplay">
<template>
<div />
</template>
</test-fixture>
<script>
function lightOrShadow(elem, selector) {
return elem.shadowRoot ?
elem.shadowRoot.querySelector(selector) :
elem.querySelector(selector);
}
suite('state-card-display', function() {
let wrapper;
let card;
setup(function() {
wrapper = fixture('stateCardDisplay');
card = new StateCardDisplay();
card.stateObj = {
entity_id: 'binary_sensor.demo',
state: 'off',
attributes: {
device_class: 'moisture',
},
};
card.hass = {
language: 'en',
resources: {
'en': {
'state.binary_sensor.moisture.off': 'Mock Off Text',
},
},
};
wrapper.appendChild(card);
});
test('state display text', function(done) {
flush(function() {
const stateDiv = lightOrShadow(card, '.state');
assert.isOk(stateDiv);
assert.deepEqual(stateDiv.innerText, 'Mock Off Text');
done();
});
});
});
</script>
</body>
</html>

View File

@ -29,10 +29,6 @@
setup(function() {
si = fixture('stateInfo');
window.HAWS = {};
window.HAWS.extractDomain = function (entityId) {
return entityId.substr(0, entityId.indexOf('.'));
};
});
test('default values', function() {