diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/etalons/dataGrid-filterPanel-popup (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/etalons/dataGrid-filterPanel-popup (fluent.blue.light).png similarity index 100% rename from e2e/testcafe-devextreme/tests/dataGrid/common/etalons/dataGrid-filterPanel-popup (fluent.blue.light).png rename to e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/etalons/dataGrid-filterPanel-popup (fluent.blue.light).png diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/etalons/dataGrid-filterPanel-popup-focused (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/etalons/dataGrid-filterPanel-popup-focused (fluent.blue.light).png similarity index 100% rename from e2e/testcafe-devextreme/tests/dataGrid/common/etalons/dataGrid-filterPanel-popup-focused (fluent.blue.light).png rename to e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/etalons/dataGrid-filterPanel-popup-focused (fluent.blue.light).png diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/etalons/dataGrid-filterPanel-popup.-with-editor-popup (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/etalons/dataGrid-filterPanel-popup.-with-editor-popup (fluent.blue.light).png similarity index 100% rename from e2e/testcafe-devextreme/tests/dataGrid/common/etalons/dataGrid-filterPanel-popup.-with-editor-popup (fluent.blue.light).png rename to e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/etalons/dataGrid-filterPanel-popup.-with-editor-popup (fluent.blue.light).png diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/functional.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/functional.ts new file mode 100644 index 000000000000..62ca30bb4aa0 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/functional.ts @@ -0,0 +1,158 @@ +import DataGrid from 'devextreme-testcafe-models/dataGrid'; +import Popup from 'devextreme-testcafe-models/popup'; +import FilterBuilder from 'devextreme-testcafe-models/filterBuilder'; +import FilterTextBox from 'devextreme-testcafe-models/dataGrid/editors/filterTextBox'; + +import url from '../../../../helpers/getPageUrl'; +import { createWidget } from '../../../../helpers/createWidget'; + +fixture.disablePageReloads`Filtering` + .page(url(__dirname, '../../../container.html')); + +const GRID_CONTAINER = '#container'; + +// T1319193, T1311486 +test('Proper handle custom filter operations for dates with non-date values', async (t) => { + const dataGrid = new DataGrid(GRID_CONTAINER); + const filterPanel = dataGrid.getFilterPanel(); + + let filterBuilderPopup = await filterPanel.openFilterBuilderPopup(t); + let filterBuilder = filterBuilderPopup.getFilterBuilder(); + + await t + .click(filterBuilder.getAddButton()) + .expect(FilterBuilder.getPopupTreeView().visible).ok() + .click(FilterBuilder.getPopupTreeViewNodeByText('Add Condition')) + .click(filterBuilder.getField(0, 'item').element) + .click(FilterBuilder.getPopupTreeViewNodeByText('Order Date')) + .click(filterBuilder.getField(0, 'itemOperation').element) + .click(FilterBuilder.getPopupTreeViewNodeByText('Is any of')) + .click(filterBuilder.getField(0, 'itemValue').element) + .click(FilterBuilder.getPopupTreeViewNodeCheckboxByText('Weekends')) + .click(new Popup(FilterBuilder.getPopupTreeView()).getOkButton().element) + .click(filterBuilderPopup.asPopup().getOkButton().element); + + await t + .expect(dataGrid.getRows().count) + .eql(3) + .expect(filterPanel.getFilterText().element.innerText) + .eql('[Order Date] Is any of(\'Weekends\')'); + + filterBuilderPopup = await filterPanel.openFilterBuilderPopup(t); + filterBuilder = filterBuilderPopup.getFilterBuilder(); + + await t + .click(filterBuilder.getField(0, 'itemOperation').element) + .click(FilterBuilder.getPopupTreeViewNodeByText('Weekends')) + .click(filterBuilderPopup.asPopup().getOkButton().element); + + await t + .expect(dataGrid.getRows().count) + .eql(3) + .expect(filterPanel.getFilterText().element.innerText) + .eql('[Order Date] Weekends'); + + const dateFilterCell = dataGrid.getFilterEditor(1, FilterTextBox); + + await t + .click(dateFilterCell.menuButton) + .click(dateFilterCell.menu.getItemByText('Between')) + .expect(dataGrid.getFilterRangeOverlay().exists).ok() + .typeText(dataGrid.getFilterRangeStartEditor().input, '2/1/2017') + .typeText(dataGrid.getFilterRangeEndEditor().input, '2/28/2017') + .pressKey('enter'); + + await t + .expect(dataGrid.getRows().count) + .eql(4) + .expect(filterPanel.getFilterText().element.innerText) + .eql('[Order Date] Is between(\'2/1/2017\', \'2/28/2017\')'); +}).before(async () => { + const dataSource = [{ + ID: 1, + OrderNumber: 35711, + OrderDate: '2017/01/12', + Employee: 'Jim Packard', + }, { + ID: 5, + OrderNumber: 35714, + OrderDate: '2017/01/22', + Employee: 'Harv Mudd', + }, { + ID: 7, + OrderNumber: 35983, + OrderDate: '2017/02/07', + Employee: 'Todd Hoffman', + }, { + ID: 14, + OrderNumber: 39420, + OrderDate: '2017/02/15', + Employee: 'Jim Packard', + }, { + ID: 15, + OrderNumber: 39874, + OrderDate: '2017/02/04', + Employee: 'Harv Mudd', + }]; + + return createWidget('dxDataGrid', { + dataSource, + keyExpr: 'ID', + filterRow: { visible: true }, + filterPanel: { visible: true }, + headerFilter: { visible: true }, + filterBuilder: { + customOperations: [ + { + name: 'weekends', + caption: 'Weekends', + dataTypes: ['date'], + icon: 'check', + hasValue: false, + calculateFilterExpression() { + function getOrderDay(rowData: { OrderDate: string }) { + return (new Date(rowData.OrderDate)).getDay(); + } + + return [[getOrderDay, '=', 0], 'or', [getOrderDay, '=', 6]]; + }, + }, + ], + }, + columns: [ + 'OrderNumber', + { + dataField: 'OrderDate', + dataType: 'date', + calculateFilterExpression(value, selectedFilterOperations, target) { + if (target === 'headerFilter' && value === 'weekends') { + function getOrderDay(rowData: { OrderDate: string }) { + return (new Date(rowData.OrderDate)).getDay(); + } + + return [[getOrderDay, '=', 0], 'or', [getOrderDay, '=', 6]]; + } + return this.defaultCalculateFilterExpression?.( + value, + selectedFilterOperations, + target, + ) ?? []; + }, + headerFilter: { + dataSource(data) { + if (data.dataSource) { + data.dataSource.postProcess = (results) => { + results.push({ + text: 'Weekends', + value: 'weekends', + }); + return results; + }; + } + }, + }, + }, + 'Employee', + ], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel.ts b/e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/visual.ts similarity index 86% rename from e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel.ts rename to e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/visual.ts index fe2caaffebfe..49beddb8188d 100644 --- a/e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel.ts +++ b/e2e/testcafe-devextreme/tests/dataGrid/common/filterPanel/visual.ts @@ -1,11 +1,11 @@ import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; import DataGrid from 'devextreme-testcafe-models/dataGrid'; -import url from '../../../helpers/getPageUrl'; -import { createWidget } from '../../../helpers/createWidget'; -import { testScreenshot } from '../../../helpers/themeUtils'; +import url from '../../../../helpers/getPageUrl'; +import { createWidget } from '../../../../helpers/createWidget'; +import { testScreenshot } from '../../../../helpers/themeUtils'; fixture.disablePageReloads`filterPanel` - .page(url(__dirname, '../../container.html')); + .page(url(__dirname, '../../../container.html')); // T1182854 test('editor\'s popup inside filterBuilder is opening & closing right (T1182854)', async (t) => { diff --git a/packages/devextreme/js/__internal/filter_builder/__tests__/utils.test.ts b/packages/devextreme/js/__internal/filter_builder/__tests__/utils.test.ts new file mode 100644 index 000000000000..6ec3ca1ec562 --- /dev/null +++ b/packages/devextreme/js/__internal/filter_builder/__tests__/utils.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from '@jest/globals'; +import type { CustomOperation, Field } from '@js/ui/filter_builder'; + +import { getCurrentValueText } from '../m_utils'; + +describe('Formatting', () => { + it('empty string', () => { + const field = {}; + const value = ''; + + expect(getCurrentValueText(field, value, null)).toBe(''); + }); + + it('string', () => { + const field = {}; + const value = 'Text'; + + expect(getCurrentValueText(field, value, null)).toBe('Text'); + }); + + it('shortDate', () => { + const field = { format: 'shortDate' }; + const value = new Date(2017, 8, 5); + + expect(getCurrentValueText(field, value, null)).toBe('9/5/2017'); + }); + + it('invalid date string (T1319193)', () => { + const field = { format: 'shortDate' }; + const dateString = 'Weekend'; + + expect(getCurrentValueText(field, dateString, null)).toBe(dateString); + }); + + it('boolean', () => { + const field: Field = { dataType: 'boolean' }; + let value = true; + + expect(getCurrentValueText(field, value, null)).toBe('true'); + + value = false; + expect(getCurrentValueText(field, value, null)).toBe('false'); + + field.falseText = 'False Text'; + expect(getCurrentValueText(field, value, null)).toBe('False Text'); + }); + + it('field.customizeText', () => { + const field: Field = { + customizeText(conditionInfo) { + return `${conditionInfo.valueText}Test`; + }, + }; + const value = 'MyValue'; + + expect(getCurrentValueText(field, value, null)).toBe('MyValueTest'); + }); + + it('customOperation.customizeText', () => { + const field: Field = { + customizeText(conditionInfo) { + return `${conditionInfo.valueText}Test`; + }, + }; + const value = 'MyValue'; + const customOperation: CustomOperation = { + customizeText(conditionInfo) { + return `${conditionInfo.valueText}CustomOperation`; + }, + }; + + expect(getCurrentValueText(field, value, customOperation)).toBe('MyValueTestCustomOperation'); + }); + + it('customOperation.customizeText for array', async () => { + const field: Field = { dataType: 'string' }; + + const customOperation = { customizeText: (): string => '(Blanks)' }; + let text = await getCurrentValueText(field, '', customOperation); + + expect(text).toBe(''); + + text = await getCurrentValueText(field, [null], customOperation); + expect(text).toEqual(['(Blanks)']); + + const field2: Field = { dataType: 'number' }; + + text = await getCurrentValueText(field2, null, customOperation); + + expect(text).toBe(''); + + text = await getCurrentValueText(field, [null], customOperation); + expect(text).toEqual(['(Blanks)']); + }); + + it('default format for date', () => { + const field: Field = { dataType: 'date' }; + const value = new Date(2017, 8, 5, 12, 30, 0); + + expect(getCurrentValueText(field, value, null)).toBe('9/5/2017'); + }); + + it('default format for datetime', () => { + const field: Field = { dataType: 'datetime' }; + const value = new Date(2017, 8, 5, 12, 30, 0); + + expect(getCurrentValueText(field, value, null)).toBe('9/5/2017, 12:30 PM'); + }); +}); diff --git a/packages/devextreme/js/__internal/filter_builder/m_utils.ts b/packages/devextreme/js/__internal/filter_builder/m_utils.ts index 8fbbbbe909a6..0a09c98fb812 100644 --- a/packages/devextreme/js/__internal/filter_builder/m_utils.ts +++ b/packages/devextreme/js/__internal/filter_builder/m_utils.ts @@ -1,19 +1,26 @@ +import type { Format } from '@js/common/core/localization'; import messageLocalization from '@js/common/core/localization/message'; import { DataSource } from '@js/common/data/data_source/data_source'; import { errors as dataErrors } from '@js/common/data/errors'; import $ from '@js/core/renderer'; import { compileGetter } from '@js/core/utils/data'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred, when } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { captionize } from '@js/core/utils/inflector'; -import { isBoolean, isDefined, isFunction } from '@js/core/utils/type'; +import { + isBoolean, isDefined, isFunction, isNumeric, isString, +} from '@js/core/utils/type'; import formatHelper from '@js/format_helper'; +import type { CustomOperation, DataType, Field } from '@js/ui/filter_builder'; import filterUtils from '@js/ui/shared/filtering'; import errors from '@js/ui/widget/ui.errors'; import { getConfig } from './m_between'; import filterOperationsDictionary from './m_filter_operations_dictionary'; +type FieldValue = string | number | boolean | Date | null | undefined; + const DEFAULT_DATA_TYPE = 'string'; const EMPTY_MENU_ICON = 'icon-none'; const AND_GROUP_OPERATION = 'and'; @@ -27,7 +34,7 @@ const DATATYPE_OPERATIONS = { boolean: ['=', '<>', 'isblank', 'isnotblank'], object: ['isblank', 'isnotblank'], }; -const DEFAULT_FORMAT = { +const DEFAULT_FORMAT: Partial> = { date: 'shortDate', datetime: 'shortDateShortTime', }; @@ -54,19 +61,24 @@ const FILTER_BUILDER_ITEM_TEXT_PART_CLASS = `${FILTER_BUILDER_ITEM_TEXT_CLASS}-p const FILTER_BUILDER_ITEM_TEXT_SEPARATOR_CLASS = `${FILTER_BUILDER_ITEM_TEXT_CLASS}-separator`; const FILTER_BUILDER_ITEM_TEXT_SEPARATOR_EMPTY_CLASS = `${FILTER_BUILDER_ITEM_TEXT_SEPARATOR_CLASS}-empty`; -function getFormattedValueText(field, value): string { - const fieldFormat = field.format || DEFAULT_FORMAT[field.dataType]; +function getDateFormat(dataType: DataType | undefined): Format | undefined { + return dataType ? DEFAULT_FORMAT[dataType] : undefined; +} + +function getFormattedValueText(field: Field, value: FieldValue): string { + const fieldFormat = field.format ?? getDateFormat(field.dataType); if (isBoolean(value)) { - const trueText: string = field.trueText || messageLocalization.format('dxDataGrid-trueText'); - const falseText: string = field.falseText || messageLocalization.format('dxDataGrid-falseText'); + const trueText = field.trueText ?? messageLocalization.format('dxDataGrid-trueText'); + const falseText = field.falseText ?? messageLocalization.format('dxDataGrid-falseText'); return value ? trueText : falseText; } if (field.dataType === 'date' || field.dataType === 'datetime') { - // value can be string or number, we need to convert it to Date object - return formatHelper.format(new Date(value), fieldFormat); + if (isString(value) || isNumeric(value)) { + return formatHelper.format(new Date(value), fieldFormat); + } } return formatHelper.format(value, fieldFormat); @@ -574,11 +586,18 @@ export function getCurrentLookupValueText(field, value, handler) { } } -function getPrimitiveValueText(field, value, customOperation, target, options?) { +function getPrimitiveValueText( + field: Field, + value: FieldValue, + customOperation: CustomOperation | null, + target: string, + options?, +): string { let valueText = getFormattedValueText(field, value); if (field.customizeText) { valueText = field.customizeText.call(field, { + // @ts-expect-error value, valueText, target, @@ -591,32 +610,43 @@ function getPrimitiveValueText(field, value, customOperation, target, options?) valueText, field, target, + // @ts-expect-error }, options); } return valueText; } -function getArrayValueText(field, value, customOperation, target) { +function getArrayValueText( + field: Field, + value: FieldValue[], + customOperation: CustomOperation | null, + target: string, +): string[] { const options = { values: value }; return value.map((v) => getPrimitiveValueText(field, v, customOperation, target, options)); } -function checkDefaultValue(value) { +function checkDefaultValue(value: FieldValue | FieldValue[]): value is '' | null { return value === '' || value === null; } -export function getCurrentValueText(field, value, customOperation, target = 'filterBuilder') { +export function getCurrentValueText( + field: Field, + value: FieldValue | FieldValue[], + customOperation: CustomOperation | null, + target = 'filterBuilder', +): string | DeferredObj { if (checkDefaultValue(value)) { return ''; } if (Array.isArray(value)) { // @ts-expect-error Deferred has badly typed ctor function - const result = new Deferred(); + const result: DeferredObj = new Deferred(); when.apply(this, getArrayValueText(field, value, customOperation, target)).done((...args) => { - const text = args.some((item) => !checkDefaultValue(item)) - ? args.map((item) => (!checkDefaultValue(item) ? item : '?')) + const text: string | string[] = (args as string[]).some((item) => !checkDefaultValue(item)) + ? (args as string[]).map((item) => (!checkDefaultValue(item) ? item : '?')) : ''; result.resolve(text); }); diff --git a/packages/devextreme/js/format_helper.js b/packages/devextreme/js/format_helper.js index de05bc7ec2a0..c748112c1b69 100644 --- a/packages/devextreme/js/format_helper.js +++ b/packages/devextreme/js/format_helper.js @@ -16,8 +16,7 @@ import './common/core/localization/currency'; export default dependencyInjector({ format: function(value, format) { const formatIsValid = isString(format) && format !== '' || isPlainObject(format) || isFunction(format); - const valueIsValid = isNumeric(value) || isDate(value); - + const valueIsValid = isNumeric(value) || (isDate(value) && !isNaN(value.getTime())); if(!formatIsValid || !valueIsValid) { return isDefined(value) ? value.toString() : ''; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/filterBuilderParts/utilsTests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/filterBuilderParts/utilsTests.js index 94ace09cb88d..135475be34e1 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/filterBuilderParts/utilsTests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/filterBuilderParts/utilsTests.js @@ -1506,101 +1506,6 @@ QUnit.module('Custom filter expressions', { }); }); -QUnit.module('Formatting', function() { - QUnit.test('empty string', function(assert) { - const field = {}; - const value = ''; - - assert.equal(utils.getCurrentValueText(field, value), ''); - }); - - QUnit.test('string', function(assert) { - const field = {}; - const value = 'Text'; - assert.equal(utils.getCurrentValueText(field, value), 'Text'); - }); - - QUnit.test('shortDate', function(assert) { - const field = { format: 'shortDate' }; - const value = new Date(2017, 8, 5); - assert.equal(utils.getCurrentValueText(field, value), '9/5/2017'); - }); - - QUnit.test('boolean', function(assert) { - const field = { dataType: 'boolean' }; - let value = true; - assert.equal(utils.getCurrentValueText(field, value), 'true'); - - value = false; - assert.equal(utils.getCurrentValueText(field, value), 'false'); - - field.falseText = 'False Text'; - assert.equal(utils.getCurrentValueText(field, value), 'False Text'); - }); - - QUnit.test('field.customizeText', function(assert) { - const field = { - customizeText: function(conditionInfo) { - return conditionInfo.valueText + 'Test'; - } - }; - const value = 'MyValue'; - assert.equal(utils.getCurrentValueText(field, value), 'MyValueTest'); - }); - - QUnit.test('customOperation.customizeText', function(assert) { - const field = { - customizeText: function(conditionInfo) { - return conditionInfo.valueText + 'Test'; - } - }; - const value = 'MyValue'; - const customOperation = { - customizeText: function(conditionInfo) { - return conditionInfo.valueText + 'CustomOperation'; - } - }; - assert.equal(utils.getCurrentValueText(field, value, customOperation), 'MyValueTestCustomOperation'); - }); - - QUnit.test('customOperation.customizeText for array', function(assert) { - const field = { dataType: 'string' }; - - const customOperation = { customizeText: (fieldInfo) => '(Blanks)' }; - let text = utils.getCurrentValueText(field, '', customOperation); - - assert.equal(text, ''); - - text = utils.getCurrentValueText(field, [null], customOperation).done(text => { - assert.equal(text, '(Blanks)'); - }); - - const field2 = { dataType: 'number' }; - - text = utils.getCurrentValueText(field2, null, customOperation); - - assert.equal(text, ''); - - text = utils.getCurrentValueText(field, [null], customOperation).done(text => { - assert.equal(text, '(Blanks)'); - }); - }); - - QUnit.test('default format for date', function(assert) { - const field = { dataType: 'date' }; - const value = new Date(2017, 8, 5, 12, 30, 0); - - assert.equal(utils.getCurrentValueText(field, value), '9/5/2017'); - }); - - QUnit.test('default format for datetime', function(assert) { - const field = { dataType: 'datetime' }; - const value = new Date(2017, 8, 5, 12, 30, 0); - - assert.equal(utils.getCurrentValueText(field, value), '9/5/2017, 12:30 PM'); - }); -}); - QUnit.module('Lookup Value', function() { QUnit.test('array of strings & value=empty', function(assert) { const field = { diff --git a/packages/devextreme/testing/tests/DevExpress.utils/utils.formatHelper.tests.js b/packages/devextreme/testing/tests/DevExpress.utils/utils.formatHelper.tests.js index f7a1e1f7aedf..abaf4dbdfcf5 100644 --- a/packages/devextreme/testing/tests/DevExpress.utils/utils.formatHelper.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.utils/utils.formatHelper.tests.js @@ -1,4 +1,5 @@ -const formatHelper = require('format_helper'); +import formatHelper from 'format_helper'; + const getDateFormatByTickInterval = formatHelper.getDateFormatByTickInterval; function checkDateWithFormat(date, format, expected, assert) { @@ -418,3 +419,86 @@ QUnit.test('Case insensitive', function(assert) { checkDateWithFormat(date1, getDateFormatByTickInterval(date1, date2, 'seCond'), '8:22:30 AM', assert); checkDateWithFormat(date1, getDateFormatByTickInterval(date1, date2, 'miLLisecond'), '333', assert); }); + +QUnit.module('Format method', function() { + QUnit.test('format with invalid format should return value as string', function(assert) { + assert.equal(formatHelper.format(123, ''), '123', 'Empty string format returns value.toString()'); + assert.equal(formatHelper.format(123, null), '123', 'Null format returns value.toString()'); + assert.equal(formatHelper.format(123, undefined), '123', 'Undefined format returns value.toString()'); + }); + + QUnit.test('format with invalid value should return value as string or empty string', function(assert) { + assert.equal(formatHelper.format('text', 'decimal'), 'text', 'String value with numeric format returns value.toString()'); + assert.equal(formatHelper.format(null, 'decimal'), '', 'Null value returns empty string'); + assert.equal(formatHelper.format(undefined, 'decimal'), '', 'Undefined value returns empty string'); + }); + + QUnit.test('format with invalid Date should return value as string', function(assert) { + const invalidDate = new Date('weekend'); + assert.equal(formatHelper.format(invalidDate, 'shortDate'), 'Invalid Date', 'Invalid Date returns "Invalid Date" string'); + }); + + QUnit.test('format numeric value with string format', function(assert) { + assert.equal(formatHelper.format(1234.5, 'decimal'), '1234.5', 'Decimal format'); + assert.equal(formatHelper.format(0.45, 'percent'), '45%', 'Percent format'); + assert.equal(formatHelper.format(1234.5, 'currency'), '$1,235', 'Currency format'); + }); + + QUnit.test('format numeric value with object format', function(assert) { + assert.equal(formatHelper.format(1234.567, { type: 'fixedPoint', precision: 2 }), '1,234.57', 'FixedPoint with precision'); + assert.equal(formatHelper.format(0.123, { type: 'percent', precision: 1 }), '12.3%', 'Percent with precision'); + }); + + QUnit.test('format numeric value with function format', function(assert) { + const customFormat = function(value) { + return 'Custom: ' + value; + }; + assert.equal(formatHelper.format(123, customFormat), 'Custom: 123', 'Function format'); + }); + + QUnit.test('format date value with string format', function(assert) { + const date = new Date(2017, 8, 5, 14, 30, 45); + assert.equal(formatHelper.format(date, 'shortDate'), '9/5/2017', 'Short date format'); + assert.equal(formatHelper.format(date, 'shortTime'), '2:30 PM', 'Short time format'); + }); + + QUnit.test('format date value with object format', function(assert) { + const date = new Date(2017, 8, 5, 14, 30, 45); + assert.equal(formatHelper.format(date, { type: 'shortDate' }), '9/5/2017', 'Short date format object'); + assert.equal(formatHelper.format(date, { type: 'longDate' }), 'Tuesday, September 5, 2017', 'Long date format object'); + }); + + QUnit.test('format date value with function format', function(assert) { + const date = new Date(2017, 8, 5); + const customFormat = function(value) { + return 'Year: ' + value.getFullYear(); + }; + assert.equal(formatHelper.format(date, customFormat), 'Year: 2017', 'Function format for date'); + }); + + QUnit.test('format zero value', function(assert) { + assert.equal(formatHelper.format(0, 'decimal'), '0', 'Zero with decimal format'); + assert.equal(formatHelper.format(0, 'percent'), '0%', 'Zero with percent format'); + }); + + QUnit.test('format negative value', function(assert) { + assert.equal(formatHelper.format(-123.45, 'fixedPoint'), '-123', 'Negative number with fixedPoint'); + assert.equal(formatHelper.format(-0.5, 'percent'), '-50%', 'Negative percent'); + }); + + QUnit.test('format with custom format pattern', function(assert) { + assert.equal(formatHelper.format(1234.5, '#,##0.00'), '1,234.50', 'Custom number pattern'); + const date = new Date(2017, 8, 5); + assert.equal(formatHelper.format(date, 'yyyy-MM-dd'), '2017-09-05', 'Custom date pattern'); + }); + + QUnit.test('format empty string value', function(assert) { + assert.equal(formatHelper.format('', 'decimal'), '', 'Empty string returns empty string'); + }); + + QUnit.test('format boolean value should return string', function(assert) { + assert.equal(formatHelper.format(true, 'decimal'), 'true', 'Boolean true returns "true"'); + assert.equal(formatHelper.format(false, 'decimal'), 'false', 'Boolean false returns "false"'); + }); +}); + diff --git a/packages/testcafe-models/dataGrid/index.ts b/packages/testcafe-models/dataGrid/index.ts index 0b85751dc5f6..a2c9bfe1c3fc 100644 --- a/packages/testcafe-models/dataGrid/index.ts +++ b/packages/testcafe-models/dataGrid/index.ts @@ -18,6 +18,7 @@ import MasterRow from './masterRow'; import AdaptiveDetailRow from './adaptiveDetailRow'; import ColumnChooser from './columnChooser'; import TextBox from '../textBox'; +import DateBox from '../dateBox'; import { GroupPanel } from './groupPanel'; import GridCore from '../gridCore'; import { CLASS as CLASS_BASE } from '../gridCore'; @@ -37,6 +38,8 @@ export const CLASS = { focusedRow: 'dx-row-focused', filterRow: 'filter-row', filterRangeOverlay: 'filter-range-overlay', + filterRangeStartEditor: 'filter-range-start', + filterRangeEndEditor: 'filter-range-end', focusOverlay: 'focus-overlay', editFormRow: 'edit-form', button: 'dx-button', @@ -245,6 +248,15 @@ export default class DataGrid extends GridCore { getFilterRangeOverlay(): Selector { return this.getHeadersContainer().child(`.${this.addWidgetPrefix(CLASS.filterRangeOverlay)}`); } + + getFilterRangeStartEditor(): DateBox { + return new DateBox(this.body.find(`.${this.addWidgetPrefix(CLASS.filterRangeStartEditor)}`)); + } + + getFilterRangeEndEditor(): DateBox { + return new DateBox(this.body.find(`.${this.addWidgetPrefix(CLASS.filterRangeEndEditor)}`)); + } + getFocusOverlay() { return this.body.find(`.${this.addWidgetPrefix(CLASS.focusOverlay)}`); } diff --git a/packages/testcafe-models/filterBuilder/index.ts b/packages/testcafe-models/filterBuilder/index.ts index 425eba2f14cb..b974174cb2f7 100644 --- a/packages/testcafe-models/filterBuilder/index.ts +++ b/packages/testcafe-models/filterBuilder/index.ts @@ -18,6 +18,7 @@ const CLASS = { itemOperation: 'dx-filterbuilder-item-operation', itemValue: 'dx-filterbuilder-item-value-text', itemAction: 'dx-filterbuilder-action', + checkbox: 'dx-checkbox', }; export default class FilterBuilder extends Widget { @@ -29,6 +30,14 @@ export default class FilterBuilder extends Widget { return Selector(`.${CLASS.treeViewLeaf}`).nth(index); } + static getPopupTreeViewNodeByText(text: string): Selector { + return Selector(`.${CLASS.treeViewLeaf}`).withText(text).filterVisible(); + } + + static getPopupTreeViewNodeCheckboxByText(text: string): Selector { + return this.getPopupTreeViewNodeByText(text).find(`.${CLASS.checkbox}`); + } + // eslint-disable-next-line class-methods-use-this getName(): WidgetName { return 'dxFilterBuilder'; }