From 424751d9e95d0e4b8c04323e7bb0a762cf40b19e Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Mon, 9 Sep 2019 10:10:10 +0300 Subject: [PATCH] Migrate Counter visualization to React (#4106) * Migrate Counter to React: Renderer * Migrate Counter to React: Editor * Cleanup * Review and fix rows indexing algorithm * Counter not properly scaled in editor * Fix wrong label for/input id pair * Tests * Tests * Fix vendor prefixes * Remove unnecessary useEffect dependencies * Update tests * Fix Percy snapshot names --- .../inc/visualizations/counter-render.less | 45 ---- client/app/assets/less/main.less | 1 - client/app/pages/dashboards/dashboard.less | 4 +- client/app/visualizations/counter/Editor.jsx | 236 ++++++++++++++++++ .../app/visualizations/counter/Renderer.jsx | 73 ++++++ .../counter/counter-editor.html | 95 ------- .../app/visualizations/counter/counter.html | 16 -- client/app/visualizations/counter/index.js | 216 +--------------- client/app/visualizations/counter/render.less | 46 ++++ client/app/visualizations/counter/utils.js | 141 +++++++++++ .../visualizations/counter_spec.js | 207 +++++++++++++++ 11 files changed, 716 insertions(+), 364 deletions(-) delete mode 100755 client/app/assets/less/inc/visualizations/counter-render.less create mode 100644 client/app/visualizations/counter/Editor.jsx create mode 100644 client/app/visualizations/counter/Renderer.jsx delete mode 100644 client/app/visualizations/counter/counter-editor.html delete mode 100644 client/app/visualizations/counter/counter.html create mode 100755 client/app/visualizations/counter/render.less create mode 100644 client/app/visualizations/counter/utils.js create mode 100644 client/cypress/integration/visualizations/counter_spec.js diff --git a/client/app/assets/less/inc/visualizations/counter-render.less b/client/app/assets/less/inc/visualizations/counter-render.less deleted file mode 100755 index c57c297a98..0000000000 --- a/client/app/assets/less/inc/visualizations/counter-render.less +++ /dev/null @@ -1,45 +0,0 @@ -counter-renderer { - display: block; - text-align: center; - padding: 15px 10px; - overflow: hidden; - - counter { - margin: 0; - padding: 0; - font-size: 80px; - line-height: normal; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - - value, - counter-target { - font-size: 1em; - display: block; - } - - counter-name { - font-size: 0.5em; - display: block; - } - - &.positive value { - color: #5cb85c; - } - - &.negative value { - color: #d9534f; - } - } - - counter-target { - color: #ccc; - } - - counter-name { - font-size: 0.5em; - display: block; - } -} diff --git a/client/app/assets/less/main.less b/client/app/assets/less/main.less index f0c92409fa..8b0b26f3e6 100644 --- a/client/app/assets/less/main.less +++ b/client/app/assets/less/main.less @@ -53,7 +53,6 @@ @import 'inc/schema-browser'; @import 'inc/toast'; @import 'inc/visualizations/box'; -@import 'inc/visualizations/counter-render'; @import 'inc/visualizations/sankey'; @import 'inc/visualizations/pivot-table'; @import 'inc/visualizations/map'; diff --git a/client/app/pages/dashboards/dashboard.less b/client/app/pages/dashboards/dashboard.less index a644de4ba8..e672fada81 100644 --- a/client/app/pages/dashboards/dashboard.less +++ b/client/app/pages/dashboards/dashboard.less @@ -91,7 +91,7 @@ overflow: hidden; } - counter { + .counter-visualization-content { position: absolute; left: 10px; top: 15px; @@ -145,7 +145,7 @@ .dashboard__control { margin: 8px 0; - + .save-status { vertical-align: middle; margin-right: 7px; diff --git a/client/app/visualizations/counter/Editor.jsx b/client/app/visualizations/counter/Editor.jsx new file mode 100644 index 0000000000..24827bb199 --- /dev/null +++ b/client/app/visualizations/counter/Editor.jsx @@ -0,0 +1,236 @@ +import { merge, map } from 'lodash'; +import React from 'react'; +import Tabs from 'antd/lib/tabs'; +import Input from 'antd/lib/input'; +import InputNumber from 'antd/lib/input-number'; +import Select from 'antd/lib/select'; +import Switch from 'antd/lib/switch'; +import * as Grid from 'antd/lib/grid'; +import { EditorPropTypes } from '@/visualizations'; + +import { isValueNumber } from './utils'; + +function GeneralSettings({ options, data, visualizationName, onOptionsChange }) { + return ( + + + + + + + onOptionsChange({ counterLabel: e.target.value })} + /> + + + + + + + + + + + + + + + + + + onOptionsChange({ rowNumber })} + /> + + + + + + + + + + + + + + + + + + onOptionsChange({ targetRowNumber })} + /> + + + + + + ); +} + +GeneralSettings.propTypes = EditorPropTypes; + +function FormatSettings({ options, data, onOptionsChange }) { + const inputsEnabled = isValueNumber(data.rows, options); + return ( + + + + + + + onOptionsChange({ stringDecimal })} + /> + + + + + + + + + onOptionsChange({ stringDecChar: e.target.value })} + /> + + + + + + + + + onOptionsChange({ stringThouSep: e.target.value })} + /> + + + + + + + + + onOptionsChange({ stringPrefix: e.target.value })} + /> + + + + + + + + + onOptionsChange({ stringSuffix: e.target.value })} + /> + + + + + + ); +} + +FormatSettings.propTypes = EditorPropTypes; + +export default function Editor(props) { + const { options, onOptionsChange } = props; + + const optionsChanged = (newOptions) => { + onOptionsChange(merge({}, options, newOptions)); + }; + + return ( + + General}> + + + Format}> + + + + ); +} + +Editor.propTypes = EditorPropTypes; diff --git a/client/app/visualizations/counter/Renderer.jsx b/client/app/visualizations/counter/Renderer.jsx new file mode 100644 index 0000000000..417ef24edb --- /dev/null +++ b/client/app/visualizations/counter/Renderer.jsx @@ -0,0 +1,73 @@ +import { isFinite } from 'lodash'; +import React, { useState, useEffect } from 'react'; +import cx from 'classnames'; +import resizeObserver from '@/services/resizeObserver'; +import { RendererPropTypes } from '@/visualizations'; + +import { getCounterData } from './utils'; + +import './render.less'; + +function getCounterStyles(scale) { + return { + msTransform: `scale(${scale})`, + MozTransform: `scale(${scale})`, + WebkitTransform: `scale(${scale})`, + transform: `scale(${scale})`, + }; +} + +function getCounterScale(container) { + const inner = container.firstChild; + const scale = Math.min(container.offsetWidth / inner.offsetWidth, container.offsetHeight / inner.offsetHeight); + return Number(isFinite(scale) ? scale : 1).toFixed(2); // keep only two decimal places +} + +export default function Renderer({ data, options, visualizationName }) { + const [scale, setScale] = useState('1.00'); + const [container, setContainer] = useState(null); + + useEffect(() => { + if (container) { + const unwatch = resizeObserver(container, () => { + setScale(getCounterScale(container)); + }); + return unwatch; + } + }, [container]); + + useEffect(() => { + if (container) { + // update scaling when options or data change (new formatting, values, etc. + // may change inner container dimensions which will not be tracked by `resizeObserver`); + setScale(getCounterScale(container)); + } + }, [data, options, container]); + + const { + showTrend, trendPositive, + counterValue, counterValueTooltip, + targetValue, targetValueTooltip, + counterLabel, + } = getCounterData(data.rows, options, visualizationName); + return ( +
+
+
+
{counterValue}
+ {targetValue && ( +
({targetValue})
+ )} +
{counterLabel}
+
+
+
+ ); +} + +Renderer.propTypes = RendererPropTypes; diff --git a/client/app/visualizations/counter/counter-editor.html b/client/app/visualizations/counter/counter-editor.html deleted file mode 100644 index f47238c4ac..0000000000 --- a/client/app/visualizations/counter/counter-editor.html +++ /dev/null @@ -1,95 +0,0 @@ -
- -
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
-
-
- -
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
-
-
-
diff --git a/client/app/visualizations/counter/counter.html b/client/app/visualizations/counter/counter.html deleted file mode 100644 index d7493f4378..0000000000 --- a/client/app/visualizations/counter/counter.html +++ /dev/null @@ -1,16 +0,0 @@ - -
- {{ counterValue }} - ({{ targetValue }}) - {{counterLabel}} -
-
diff --git a/client/app/visualizations/counter/index.js b/client/app/visualizations/counter/index.js index 2ece76b2b2..07bd18af5d 100644 --- a/client/app/visualizations/counter/index.js +++ b/client/app/visualizations/counter/index.js @@ -1,10 +1,7 @@ -import { isNumber, toString } from 'lodash'; -import numeral from 'numeral'; -import { angular2react } from 'angular2react'; import { registerVisualization } from '@/visualizations'; -import counterTemplate from './counter.html'; -import counterEditorTemplate from './counter-editor.html'; +import Renderer from './Renderer'; +import Editor from './Editor'; const DEFAULT_OPTIONS = { counterLabel: '', @@ -17,207 +14,16 @@ const DEFAULT_OPTIONS = { tooltipFormat: '0,0.000', // TODO: Show in editor }; -// TODO: allow user to specify number format string instead of delimiters only -// It will allow to remove this function (move all that weird formatting logic to a migration -// that will set number format for all existing counter visualization) -function numberFormat(value, decimalPoints, decimalDelimiter, thousandsDelimiter) { - // Temporarily update locale data (restore defaults after formatting) - const locale = numeral.localeData(); - const savedDelimiters = locale.delimiters; +export default function init() { + registerVisualization({ + type: 'COUNTER', + name: 'Counter', + getOptions: options => ({ ...DEFAULT_OPTIONS, ...options }), + Renderer, + Editor, - // Mimic old behavior - AngularJS `number` filter defaults: - // - `,` as thousands delimiter - // - `.` as decimal delimiter - // - three decimal points - locale.delimiters = { - thousands: ',', - decimal: '.', - }; - let formatString = '0,0.000'; - if ( - (Number.isFinite(decimalPoints) && (decimalPoints >= 0)) || - decimalDelimiter || - thousandsDelimiter - ) { - locale.delimiters = { - thousands: thousandsDelimiter, - decimal: decimalDelimiter || '.', - }; - - formatString = '0,0'; - if (decimalPoints > 0) { - formatString += '.'; - while (decimalPoints > 0) { - formatString += '0'; - decimalPoints -= 1; - } - } - } - const result = numeral(value).format(formatString); - - locale.delimiters = savedDelimiters; - return result; -} - -// TODO: Need to review this function, it does not properly handle edge cases. -function getRowNumber(index, size) { - if (index >= 0) { - return index - 1; - } - - if (Math.abs(index) > size) { - index %= size; - } - - return size + index; -} - -function formatValue(value, { stringPrefix, stringSuffix, stringDecimal, stringDecChar, stringThouSep }) { - if (isNumber(value)) { - value = numberFormat(value, stringDecimal, stringDecChar, stringThouSep); - return toString(stringPrefix) + value + toString(stringSuffix); - } - return toString(value); -} - -function formatTooltip(value, formatString) { - if (isNumber(value)) { - return numeral(value).format(formatString); - } - return toString(value); -} - -const CounterRenderer = { - template: counterTemplate, - bindings: { - data: '<', - options: '<', - visualizationName: '<', - }, - controller($scope, $element, $timeout) { - $scope.fontSize = '1em'; - - $scope.scale = 1; - const root = $element[0].querySelector('counter'); - const container = $element[0].querySelector('counter > div'); - $scope.handleResize = () => { - const scale = Math.min(root.offsetWidth / container.offsetWidth, root.offsetHeight / container.offsetHeight); - $scope.scale = Math.floor(scale * 100) / 100; // keep only two decimal places - }; - - const update = () => { - const options = this.options; - const data = this.data.rows; - - if (data.length > 0) { - const rowNumber = getRowNumber(options.rowNumber, data.length); - const targetRowNumber = getRowNumber(options.targetRowNumber, data.length); - const counterColName = options.counterColName; - const targetColName = options.targetColName; - const counterLabel = options.counterLabel; - - if (counterLabel) { - $scope.counterLabel = counterLabel; - } else { - $scope.counterLabel = this.visualizationName; - } - - if (options.countRow) { - $scope.counterValue = data.length; - } else if (counterColName) { - $scope.counterValue = data[rowNumber][counterColName]; - } - - $scope.showTrend = false; - if (targetColName) { - $scope.targetValue = data[targetRowNumber][targetColName]; - - if (Number.isFinite($scope.counterValue) && Number.isFinite($scope.targetValue)) { - const delta = $scope.counterValue - $scope.targetValue; - $scope.showTrend = true; - $scope.trendPositive = delta >= 0; - } - } else { - $scope.targetValue = null; - } - - $scope.counterValueTooltip = formatTooltip($scope.counterValue, options.tooltipFormat); - $scope.targetValueTooltip = formatTooltip($scope.targetValue, options.tooltipFormat); - - $scope.counterValue = formatValue($scope.counterValue, options); - - if (options.formatTargetValue) { - $scope.targetValue = formatValue($scope.targetValue, options); - } else { - if (Number.isFinite($scope.targetValue)) { - $scope.targetValue = numeral($scope.targetValue).format('0[.]00[0]'); - } - } - } - - $timeout(() => { - $scope.handleResize(); - }); - }; - - $scope.$watch('$ctrl.data', update); - $scope.$watch('$ctrl.options', update, true); - }, -}; - -const CounterEditor = { - template: counterEditorTemplate, - bindings: { - data: '<', - options: '<', - visualizationName: '<', - onOptionsChange: '<', - }, - controller($scope) { - this.currentTab = 'general'; - this.changeTab = (tab) => { - this.currentTab = tab; - }; - - this.isValueNumber = () => { - const options = this.options; - const data = this.data.rows; - - if (data.length > 0) { - const rowNumber = getRowNumber(options.rowNumber, data.length); - const counterColName = options.counterColName; - - if (options.countRow) { - this.counterValue = data.length; - } else if (counterColName) { - this.counterValue = data[rowNumber][counterColName]; - } - } - - return isNumber(this.counterValue); - }; - - $scope.$watch('$ctrl.options', (options) => { - this.onOptionsChange(options); - }, true); - }, -}; - -export default function init(ngModule) { - ngModule.component('counterRenderer', CounterRenderer); - ngModule.component('counterEditor', CounterEditor); - - ngModule.run(($injector) => { - registerVisualization({ - type: 'COUNTER', - name: 'Counter', - getOptions: options => ({ ...DEFAULT_OPTIONS, ...options }), - Renderer: angular2react('counterRenderer', CounterRenderer, $injector), - Editor: angular2react('counterEditor', CounterEditor, $injector), - - defaultColumns: 2, - defaultRows: 5, - }); + defaultColumns: 2, + defaultRows: 5, }); } diff --git a/client/app/visualizations/counter/render.less b/client/app/visualizations/counter/render.less new file mode 100755 index 0000000000..252d0c0242 --- /dev/null +++ b/client/app/visualizations/counter/render.less @@ -0,0 +1,46 @@ +.counter-visualization-container { + display: block; + text-align: center; + padding: 15px 10px; + overflow: hidden; + + .counter-visualization-content { + margin: 0; + padding: 0; + font-size: 80px; + line-height: normal; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .counter-visualization-value, + .counter-visualization-target { + font-size: 1em; + display: block; + } + + .counter-visualization-label { + font-size: 0.5em; + display: block; + } + + .counter-visualization-target { + color: #ccc; + } + + .counter-visualization-label { + font-size: 0.5em; + display: block; + } + } + + &.trend-positive .counter-visualization-value { + color: #5cb85c; + } + + &.trend-negative .counter-visualization-value { + color: #d9534f; + } +} diff --git a/client/app/visualizations/counter/utils.js b/client/app/visualizations/counter/utils.js new file mode 100644 index 0000000000..8a4bd1b0f6 --- /dev/null +++ b/client/app/visualizations/counter/utils.js @@ -0,0 +1,141 @@ +import { isNumber, isFinite, toString } from 'lodash'; +import numeral from 'numeral'; + +// TODO: allow user to specify number format string instead of delimiters only +// It will allow to remove this function (move all that weird formatting logic to a migration +// that will set number format for all existing counter visualization) +function numberFormat(value, decimalPoints, decimalDelimiter, thousandsDelimiter) { + // Temporarily update locale data (restore defaults after formatting) + const locale = numeral.localeData(); + const savedDelimiters = locale.delimiters; + + // Mimic old behavior - AngularJS `number` filter defaults: + // - `,` as thousands delimiter + // - `.` as decimal delimiter + // - three decimal points + locale.delimiters = { + thousands: ',', + decimal: '.', + }; + let formatString = '0,0.000'; + if ( + (Number.isFinite(decimalPoints) && (decimalPoints >= 0)) || + decimalDelimiter || + thousandsDelimiter + ) { + locale.delimiters = { + thousands: thousandsDelimiter, + decimal: decimalDelimiter || '.', + }; + + formatString = '0,0'; + if (decimalPoints > 0) { + formatString += '.'; + while (decimalPoints > 0) { + formatString += '0'; + decimalPoints -= 1; + } + } + } + const result = numeral(value).format(formatString); + + locale.delimiters = savedDelimiters; + return result; +} + +// 0 - special case, use first record +// 1..N - 1-based record number from beginning (wraps if greater than dataset size) +// -1..-N - 1-based record number from end (wraps if greater than dataset size) +function getRowNumber(index, rowsCount) { + index = parseInt(index, 10) || 0; + if (index === 0) { + return index; + } + const wrappedIndex = (Math.abs(index) - 1) % rowsCount; + return index > 0 ? wrappedIndex : rowsCount - wrappedIndex - 1; +} + +function formatValue(value, { stringPrefix, stringSuffix, stringDecimal, stringDecChar, stringThouSep }) { + if (isNumber(value)) { + value = numberFormat(value, stringDecimal, stringDecChar, stringThouSep); + return toString(stringPrefix) + value + toString(stringSuffix); + } + return toString(value); +} + +function formatTooltip(value, formatString) { + if (isNumber(value)) { + return numeral(value).format(formatString); + } + return toString(value); +} + +export function getCounterData(rows, options, visualizationName) { + const result = {}; + + const rowsCount = rows.length; + if (rowsCount > 0) { + const rowNumber = getRowNumber(options.rowNumber, rowsCount); + const targetRowNumber = getRowNumber(options.targetRowNumber, rowsCount); + const counterColName = options.counterColName; + const targetColName = options.targetColName; + const counterLabel = options.counterLabel; + + if (counterLabel) { + result.counterLabel = counterLabel; + } else { + result.counterLabel = visualizationName; + } + + if (options.countRow) { + result.counterValue = rowsCount; + } else if (counterColName) { + result.counterValue = rows[rowNumber][counterColName]; + } + + result.showTrend = false; + if (targetColName) { + result.targetValue = rows[targetRowNumber][targetColName]; + + if (Number.isFinite(result.counterValue) && isFinite(result.targetValue)) { + const delta = result.counterValue - result.targetValue; + result.showTrend = true; + result.trendPositive = delta >= 0; + } + } else { + result.targetValue = null; + } + + result.counterValueTooltip = formatTooltip(result.counterValue, options.tooltipFormat); + result.targetValueTooltip = formatTooltip(result.targetValue, options.tooltipFormat); + + result.counterValue = formatValue(result.counterValue, options); + + if (options.formatTargetValue) { + result.targetValue = formatValue(result.targetValue, options); + } else { + if (isFinite(result.targetValue)) { + result.targetValue = numeral(result.targetValue).format('0[.]00[0]'); + } + } + } + + return result; +} + +export function isValueNumber(rows, options) { + if (options.countRow) { + return true; // array length is always a number + } + + const rowsCount = rows.length; + if (rowsCount > 0) { + const rowNumber = getRowNumber(options.rowNumber, rowsCount); + const counterColName = options.counterColName; + if (counterColName) { + return isNumber(rows[rowNumber][counterColName]); + } + } + + return false; +} diff --git a/client/cypress/integration/visualizations/counter_spec.js b/client/cypress/integration/visualizations/counter_spec.js new file mode 100644 index 0000000000..dadfa68f49 --- /dev/null +++ b/client/cypress/integration/visualizations/counter_spec.js @@ -0,0 +1,207 @@ +/* global cy, Cypress */ + +import { createQuery } from '../../support/redash-api'; + +const SQL = ` + SELECT 27182.8182846 AS a, 20000 AS b, 'lorem' AS c UNION ALL + SELECT 31415.9265359 AS a, 40000 AS b, 'ipsum' AS c +`; + +describe('Counter', () => { + const viewportWidth = Cypress.config('viewportWidth'); + + beforeEach(() => { + cy.login(); + createQuery({ query: SQL }).then(({ id }) => { + cy.visit(`queries/${id}/source`); + cy.getByTestId('ExecuteButton').click(); + }); + }); + + it('creates simple Counter', () => { + cy.clickThrough(` + NewVisualization + VisualizationType + VisualizationType.COUNTER + + Counter.General.ValueColumn + Counter.General.ValueColumn.a + `); + + cy.getByTestId('VisualizationPreview').find('.counter-visualization-container').should('exist'); + + // wait a bit before taking snapshot + cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.percySnapshot('Visualizations - Counter (with defaults)', { widths: [viewportWidth] }); + }); + + it('creates Counter with custom label', () => { + cy.clickThrough(` + NewVisualization + VisualizationType + VisualizationType.COUNTER + + Counter.General.ValueColumn + Counter.General.ValueColumn.a + `); + + cy.fillInputs({ + 'Counter.General.Label': 'Custom Label', + }); + + cy.getByTestId('VisualizationPreview').find('.counter-visualization-container').should('exist'); + + // wait a bit before taking snapshot + cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.percySnapshot('Visualizations - Counter (custom label)', { widths: [viewportWidth] }); + }); + + it('creates Counter with non-numeric value', () => { + cy.clickThrough(` + NewVisualization + VisualizationType + VisualizationType.COUNTER + + Counter.General.ValueColumn + Counter.General.ValueColumn.c + + Counter.General.TargetValueColumn + Counter.General.TargetValueColumn.c + `); + + cy.fillInputs({ + 'Counter.General.TargetValueRowNumber': '2', + }); + + cy.getByTestId('VisualizationPreview').find('.counter-visualization-container').should('exist'); + + // wait a bit before taking snapshot + cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.percySnapshot('Visualizations - Counter (non-numeric value)', { widths: [viewportWidth] }); + }); + + it('creates Counter with target value (trend positive)', () => { + cy.clickThrough(` + NewVisualization + VisualizationType + VisualizationType.COUNTER + + Counter.General.ValueColumn + Counter.General.ValueColumn.a + + Counter.General.TargetValueColumn + Counter.General.TargetValueColumn.b + `); + + cy.getByTestId('VisualizationPreview').find('.counter-visualization-container').should('exist'); + + // wait a bit before taking snapshot + cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.percySnapshot('Visualizations - Counter (target value + trend positive)', { widths: [viewportWidth] }); + }); + + it('creates Counter with custom row number (trend negative)', () => { + cy.clickThrough(` + NewVisualization + VisualizationType + VisualizationType.COUNTER + + Counter.General.ValueColumn + Counter.General.ValueColumn.a + + Counter.General.TargetValueColumn + Counter.General.TargetValueColumn.b + `); + + cy.fillInputs({ + 'Counter.General.ValueRowNumber': '2', + 'Counter.General.TargetValueRowNumber': '2', + }); + + cy.getByTestId('VisualizationPreview').find('.counter-visualization-container').should('exist'); + + // wait a bit before taking snapshot + cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.percySnapshot('Visualizations - Counter (row number + trend negative)', { widths: [viewportWidth] }); + }); + + it('creates Counter with count rows', () => { + cy.clickThrough(` + NewVisualization + VisualizationType + VisualizationType.COUNTER + + Counter.General.ValueColumn + Counter.General.ValueColumn.a + + Counter.General.CountRows + `); + + cy.getByTestId('VisualizationPreview').find('.counter-visualization-container').should('exist'); + + // wait a bit before taking snapshot + cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.percySnapshot('Visualizations - Counter (count rows)', { widths: [viewportWidth] }); + }); + + it('creates Counter with formatting', () => { + cy.clickThrough(` + NewVisualization + VisualizationType + VisualizationType.COUNTER + + Counter.General.ValueColumn + Counter.General.ValueColumn.a + + Counter.General.TargetValueColumn + Counter.General.TargetValueColumn.b + + Counter.EditorTabs.Formatting + `); + + cy.fillInputs({ + 'Counter.Formatting.DecimalPlace': '4', + 'Counter.Formatting.DecimalCharacter': ',', + 'Counter.Formatting.ThousandsSeparator': '`', + 'Counter.Formatting.StringPrefix': '$', + 'Counter.Formatting.StringSuffix': '%', + }); + + cy.getByTestId('VisualizationPreview').find('.counter-visualization-container').should('exist'); + + // wait a bit before taking snapshot + cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.percySnapshot('Visualizations - Counter (custom formatting)', { widths: [viewportWidth] }); + }); + + it('creates Counter with target value formatting', () => { + cy.clickThrough(` + NewVisualization + VisualizationType + VisualizationType.COUNTER + + Counter.General.ValueColumn + Counter.General.ValueColumn.a + + Counter.General.TargetValueColumn + Counter.General.TargetValueColumn.b + + Counter.EditorTabs.Formatting + Counter.Formatting.FormatTargetValue + `); + + cy.fillInputs({ + 'Counter.Formatting.DecimalPlace': '4', + 'Counter.Formatting.DecimalCharacter': ',', + 'Counter.Formatting.ThousandsSeparator': '`', + 'Counter.Formatting.StringPrefix': '$', + 'Counter.Formatting.StringSuffix': '%', + }); + + cy.getByTestId('VisualizationPreview').find('.counter-visualization-container').should('exist'); + + // wait a bit before taking snapshot + cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting + cy.percySnapshot('Visualizations - Counter (format target value)', { widths: [viewportWidth] }); + }); +});