diff --git a/.github/workflows/testcafe_tests.yml b/.github/workflows/testcafe_tests.yml index 52b65c312b04..dbdecf0e4bcb 100644 --- a/.github/workflows/testcafe_tests.yml +++ b/.github/workflows/testcafe_tests.yml @@ -119,6 +119,9 @@ jobs: { componentFolder: "dataGrid/sticky/fixed", name: "dataGrid / sticky (1/3)", indices: "1/3" }, { componentFolder: "dataGrid/sticky/fixed", name: "dataGrid / sticky (2/3)", indices: "2/3" }, { componentFolder: "dataGrid/sticky/fixed", name: "dataGrid / sticky (3/3)", indices: "3/3" }, + { componentFolder: "cardView", name: "cardView" }, + { componentFolder: "cardView", name: "cardView - material", theme: 'material.blue.light' }, + { componentFolder: "cardView", name: "cardView - fluent", theme: 'fluent.blue.light' }, { componentFolder: "pivotGrid", name: "pivotGrid", concurrency: 1 }, { componentFolder: "pivotGrid", name: "pivotGrid - material", theme: 'material.blue.light', concurrency: 1 }, { componentFolder: "pivotGrid", name: "pivotGrid - fluent", theme: 'fluent.blue.light', concurrency: 1 }, diff --git a/apps/demos/configs/Angular/config.js b/apps/demos/configs/Angular/config.js index b104f15225e8..efa4356980aa 100644 --- a/apps/demos/configs/Angular/config.js +++ b/apps/demos/configs/Angular/config.js @@ -12,6 +12,7 @@ const componentNames = [ 'button-group', 'button', 'calendar', + 'card-view', 'chart', 'chat', 'check-box', diff --git a/apps/react-storybook/stories/card_view/Card.stories.tsx b/apps/react-storybook/stories/card_view/Card.stories.tsx new file mode 100644 index 000000000000..37b232724100 --- /dev/null +++ b/apps/react-storybook/stories/card_view/Card.stories.tsx @@ -0,0 +1,63 @@ +import React, { useState, useRef, useEffect } from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import {generatedData} from './generatedData'; + +import { Card as InfernoCard } from "devextreme/esm/__internal/grids/new/card_view/content_view/content/card/card"; +import { wrapInfernoWithReact } from "../utils"; +import { Footer } from "./templates"; + +interface Props { + allowSelectOnClick: boolean; + showSelectCheckBox: boolean; + showImage: boolean; + showFooter: boolean; +} + +const data = generatedData[0]; +const columns = Object.keys(data); + +const row = { + key: 1, + cells: columns.map((column) => ({ + column: { + caption: column, + }, + text: data[column] + })) +} + +const Card = wrapInfernoWithReact(InfernoCard); + +const meta: Meta = { + title: "Grids/CardView/Card", + component: Card, +}; + +export default meta; + +type Story = StoryObj; + +export const DefaultMode: Story = { + args: { + allowSelectOnClick: true, + showSelectCheckBox: true, + showImage: true, + showFooter: true, + }, + + render(props) { + const cover = props.showImage ? { + imageExpr: function(data) {console.log(arguments)}, + altExpr: function(data) {console.log(arguments)}, + } : {}; + + const footer = props.showFooter && Footer; + + return + } +}; diff --git a/apps/react-storybook/stories/card_view/CardView.stories.tsx b/apps/react-storybook/stories/card_view/CardView.stories.tsx new file mode 100644 index 000000000000..264f141a6268 --- /dev/null +++ b/apps/react-storybook/stories/card_view/CardView.stories.tsx @@ -0,0 +1,347 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import dxCardView from "devextreme/ui/card_view"; +import { wrapDxWithReact } from "../utils"; +import { store } from "./data"; +import { generatedData } from "./generatedData"; +import { renderFooter } from "./templates"; + +const CardView = wrapDxWithReact(dxCardView); + +const dataSources = { + empty: [], + local: generatedData, + remote: store, +} + +const columns = { + remote: [ + { + dataField: "OrderNumber", + alignment: 'right', + dataType: "number", + }, + { + dataField: "OrderDate", + visible: false, + }, + "StoreCity", + "StoreState", + "Employee", + { + dataField: "SaleAmount", + dataType: "number", + }, + ], + local: [ + 'firstName', + 'lastName', + 'gender', + { + dataField: 'birthDate', + dataType: 'date', + } + + ], + sortedRemote: [ + { + dataField: "OrderNumber", + alignment: 'right', + dataType: "number", + sortOrder: 'asc', + sortIndex: 1, + }, + { + dataField: "OrderDate", + visible: false, + }, + { + dataField: "StoreCity", + sortOrder: 'desc', + sortIndex: 0, + }, + "StoreState", + "Employee", + "SaleAmount", + ], + localHeaderFilter: [ + { + dataField: 'firstName', + headerFilter: { + allowSelectAll: false, + search: { + enabled: true, + }, + values: ['Anet', 'Annabela'], + }, + }, + { + dataField: 'lastName', + headerFilter: { + filterType: 'exclude', + values: ['Abbey'], + } + }, + { + dataField: 'gender', + allowHeaderFiltering: false, + }, + { + dataField: 'birthDate', + dataType: 'date', + calculateCellValue: (data) => { + return new Date(data.birthDate); + }, + calculateDisplayValue: (data) => { + return new Date(data.birthDate).toDateString(); + } + }, + ], + remoteHeaderFilter: [ + { + dataField: "OrderNumber", + alignment: 'right', + dataType: "number", + }, + { + dataField: "OrderDate", + dataType: 'date', + calculateCellValue: (data) => { + return new Date(data.OrderDate); + }, + calculateDisplayValue: (data) => { + return new Date(data.OrderDate).toDateString(); + } + }, + "StoreCity", + "StoreState", + "Employee", + { + dataField: "SaleAmount", + dataType: "number", + headerFilter: { + groupInterval: 1000, + } + }, +], +} + +const meta: Meta = { + title: "Grids/CardView", + component: CardView, + argTypes: { + dataSource: { + options: Object.keys(dataSources), + mapping: dataSources, + control: { type: 'radio' }, + }, + remoteOperations: { + control: 'radio', + options: [false, true, 'auto'], + }, + width: { + control: 'text', + }, + height: { + control: 'text', + }, + keyExpr: { + control: 'text', + }, + cardsPerRow: { + options: ['auto', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + control: { type: 'select' }, + }, + paging: { + pageSize: 12, + }, + // cardMinWidth: 250, + // cardMaxWidth: 350, + // filterPanel: { visible: true }, + columns: { + options: Object.keys(columns), + mapping: columns, + control: { type: 'radio' }, + }, + headerFilter: { + control: 'object', + }, + searchPanel: { + control: 'object', + }, + cardFooterTemplate: { + control: 'radio', + options: ['show (custom template)', 'undefined'], + mapping: { + 'show (custom template)': renderFooter, + 'undefined': undefined, + } + } + } +}; + +export default meta; + +type Story = StoryObj; + +export const DefaultMode: Story = { + args: { + dataSource: 'local', + width: "100%", + // TODO: Fix height limit + // height: '500px', + keyExpr: "OrderNumber", + cardsPerRow: "auto", + paging: { + pageSize: 12, + }, + cardMinWidth: 250, + cardMaxWidth: 350, + columns: 'local', + filterPanel: { visible: true }, + cardFooterTemplate: undefined, + }, +}; + +export const RawControls: Story = { + ...DefaultMode, + argTypes: { + ...meta.argTypes, + dataSource: { + control: 'object', + mapping: null, + }, + columns: { + control: 'object', + mapping: null, + }, + }, + args: { + ...DefaultMode.args, + dataSource: dataSources.local.slice(0, 10), + columns: columns.local, + } +}; + +export const FixatedCardsPerRow: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + cardsPerRow: 3 + }, +}; + +export const EmptyCardView: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + dataSource: 'empty', + }, +}; + +export const CardViewWithCover : Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + cardCover: { + imageExpr: (data) => `https://js.devexpress.com/jQuery/Demos/WidgetsGallery/JSDemos/${data.picture}`, + altExpr: 'FirstName', + // ratio: '2 / 1', + }, + }, +}; + +export const SortedCardView: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + dataSource: 'remote', + columns: 'sortedRemote', + }, +}; + +export const SearchCardView: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + dataSource: 'local', + columns: 'local', + searchPanel: { + highlightCaseSensitive: false, + highlightSearchText: true, + text: '', + visible: true, + placeholder: 'Search...', + searchVisibleColumnsOnly: false, + width: 160, + } + } +} + +export const HeaderFilterStory: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + headerFilter: { + visible: true, + width: 252, + height: 325, + allowSelectAll: true, + search: { + enabled: false, + timeout: 500, + mode: 'contains', + editorOptions: {}, + }, + texts: { + emptyValue: 'empty', + ok: 'ok', + cancel: 'cancel', + }, + } + } +} + +export const SelectionStory: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + keyExpr: 'id', + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + allowSelectAll: true, + selectAllMode: 'allPages', + } + } +} + +export const ContextMenuStory: Story = { + ...DefaultMode, + args: { + ...DefaultMode.args, + onContextMenuPreparing: (e) => { + e.items = e.items ?? []; + + if(e.target === 'toolbar') { + e.items.push({ + text: 'show column chooser', + onItemClick: () => e.component.showColumnChooser() + }); + } + else if(e.target === 'headerPanel' && e.column) { + e.items.push({ + text: `hide ${e.column.caption}`, + disabled: !e.column.visible, + icon: 'eyeclose', + onItemClick: () => e.component.columnOption(e.columnIndex, 'visible', false) + }); + } + else if(e.target === 'content' && e.card) { + e.items.push({ + text: 'do something with card' + }); + } + } + } +} diff --git a/apps/react-storybook/stories/card_view/data.ts b/apps/react-storybook/stories/card_view/data.ts new file mode 100644 index 000000000000..cdb7e5b23181 --- /dev/null +++ b/apps/react-storybook/stories/card_view/data.ts @@ -0,0 +1,39 @@ +import CustomStore from 'devextreme/data/custom_store'; + +export const items = new Array(1000).fill(null).map(() => ( + {column1: 1, column2: 2} +)); + +function isNotEmpty(value: string | undefined | null) { + return value !== undefined && value !== null && value !== ''; +} + +export const store = new CustomStore({ + key: 'OrderNumber', + async load(loadOptions) { + const paramNames = [ + 'skip', 'take', 'requireTotalCount', 'requireGroupCount', + 'sort', 'filter', 'totalSummary', 'group', 'groupSummary', + ]; + + const queryString = paramNames + .filter((paramName) => isNotEmpty(loadOptions[paramName])) + .map((paramName) => `${paramName}=${JSON.stringify(loadOptions[paramName])}`) + .join('&'); + + try { + const response = await fetch(`https://js.devexpress.com/Demos/WidgetsGalleryDataService/api/orders?${queryString}`); + + const result = await response.json(); + + return { + data: result.data, + totalCount: result.totalCount, + summary: result.summary, + groupCount: result.groupCount, + }; + } catch (err) { + throw new Error('Data Loading Error'); + } + }, +}); \ No newline at end of file diff --git a/apps/react-storybook/stories/card_view/generatedData.ts b/apps/react-storybook/stories/card_view/generatedData.ts new file mode 100644 index 000000000000..8cca191f4396 --- /dev/null +++ b/apps/react-storybook/stories/card_view/generatedData.ts @@ -0,0 +1,799 @@ +export const generatedData = [{ + "id": 1, + "firstName": "Curry", + "lastName": "Moynham", + "email": "cmoynham0@google.com.au", + "gender": "Male", + "birthDate": new Date("1983/11/07"), + "picture": "images/employees/01.png", +}, { + "id": 2, + "firstName": "Anya", + "lastName": "Le Claire", + "email": "aleclaire1@tinyurl.com", + "gender": "Female", + "birthDate": new Date("1988/03/27"), +}, { + "id": 3, + "firstName": "Raven", + "lastName": "Slayton", + "email": "rslayton2@scientificamerican.com", + "gender": "Female", + "birthDate": new Date("1981/04/29"), + "picture": "images/employees/03.png", +}, { + "id": 4, + "firstName": "Mireille", + "lastName": "Casini", + "email": "mcasini3@ted.com", + "gender": "Female", + "birthDate": new Date("1993/11/29"), + "picture": "images/employees/04.png", +}, { + "id": 5, + "firstName": "Gloriane", + "lastName": "Workes", + "email": "gworkes4@uol.com.br", + "gender": "Female", + "birthDate": new Date("1980/11/28"), +}, { + "id": 6, + "firstName": "Elvis", + "lastName": "Matthew", + "email": "ematthew5@qq.com", + "gender": "Male", + "birthDate": new Date("1981/07/24"), + "picture": "images/employees/06.png", +}, { + "id": 7, + "firstName": "Gerti", + "lastName": "Greneham", + "email": "ggreneham6@ucoz.ru", + "gender": "Female", + "birthDate": new Date("1994/12/18"), + "picture": "images/employees/07.png", +}, { + "id": 8, + "firstName": "Ken", + "lastName": "Collinge", + "email": "kcollinge7@friendfeed.com", + "gender": "Male", + "birthDate": new Date("1982/11/30"), + "picture": "images/employees/08.png", +}, { + "id": 9, + "firstName": "Georgina", + "lastName": "Warder", + "email": "gwarder8@salon.com", + "gender": "Female", + "birthDate": new Date("1999/01/14"), + "picture": "images/employees/09.png", +}, { + "id": 10, + "firstName": "Mose", + "lastName": "Vertey", + "email": "mvertey9@vinaora.com", + "gender": "Male", + "birthDate": new Date("1996/09/11"), + "picture": "images/employees/02.png", +}, { + "id": 11, + "firstName": "Karlik", + "lastName": "Windows", + "email": "kwindowsa@wunderground.com", + "gender": "Male", + "birthDate": new Date("1980/03/17"), + "picture": "images/employees/02.png", +}, { + "id": 12, + "firstName": "Konstanze", + "lastName": "Navan", + "email": "knavanb@hatena.ne.jp", + "gender": "Female", + "birthDate": new Date("1980/04/23"), + "picture": "images/employees/02.png", +}, { + "id": 13, + "firstName": "Anet", + "lastName": "Saberton", + "email": "asabertonc@quantcast.com", + "gender": "Female", + "birthDate": new Date("1992/05/05"), + "picture": "images/employees/02.png", +}, { + "id": 14, + "firstName": "Ruthy", + "lastName": "Casserly", + "email": "rcasserlyd@ezinearticles.com", + "gender": "Female", + "birthDate": new Date("1982/05/25"), + "picture": "images/employees/02.png", +}, { + "id": 15, + "firstName": "Hakim", + "lastName": "McCrainor", + "email": "hmccrainore@prnewswire.com", + "gender": "Polygender", + "birthDate": new Date("1989/02/26"), + "picture": "images/employees/02.png", +}, { + "id": 16, + "firstName": "Jennica", + "lastName": "Kinsell", + "email": "jkinsellf@prnewswire.com", + "gender": "Female", + "birthDate": new Date("1989/10/07"), + "picture": "images/employees/02.png", +}, { + "id": 17, + "firstName": "Reed", + "lastName": "Abramovitch", + "email": "rabramovitchg@psu.edu", + "gender": "Male", + "birthDate": new Date("1997/10/05"), + "picture": "images/employees/02.png", +}, { + "id": 18, + "firstName": "Arlena", + "lastName": "Heinlein", + "email": "aheinleinh@pen.io", + "gender": "Agender", + "birthDate": new Date("1987/05/08"), + "picture": "images/employees/02.png", +}, { + "id": 19, + "firstName": "Opaline", + "lastName": "Climar", + "email": "oclimari@histats.com", + "gender": "Female", + "birthDate": new Date("1986/04/10"), + "picture": "images/employees/02.png", +}, { + "id": 20, + "firstName": "Serge", + "lastName": "Mullan", + "email": "smullanj@senate.gov", + "gender": "Male", + "birthDate": new Date("1985/10/26"), + "picture": "images/employees/02.png", +}, { + "id": 21, + "firstName": "Lucy", + "lastName": "Congrave", + "email": "lcongravek@globo.com", + "gender": "Female", + "birthDate": new Date("1983/04/12"), + "picture": "images/employees/02.png", +}, { + "id": 22, + "firstName": "Harland", + "lastName": "Gerardot", + "email": "hgerardotl@comcast.net", + "gender": "Male", + "birthDate": new Date("1992/01/04"), + "picture": "images/employees/02.png", +}, { + "id": 23, + "firstName": "Maisey", + "lastName": "Boken", + "email": "mbokenm@economist.com", + "gender": "Female", + "birthDate": new Date("1985/07/03"), + "picture": "images/employees/02.png", +}, { + "id": 24, + "firstName": "Devlin", + "lastName": "Sayle", + "email": "dsaylen@squarespace.com", + "gender": "Male", + "birthDate": new Date("1980/06/24"), + "picture": "images/employees/02.png", +}, { + "id": 25, + "firstName": "Cleve", + "lastName": "Schuler", + "email": "cschulero@reference.com", + "gender": "Male", + "birthDate": new Date("1995/05/02"), + "picture": "images/employees/02.png", +}, { + "id": 26, + "firstName": "Jorgan", + "lastName": "Navan", + "email": "jnavanp@buzzfeed.com", + "gender": "Male", + "birthDate": new Date("1985/03/16"), + "picture": "images/employees/02.png", +}, { + "id": 27, + "firstName": "Worth", + "lastName": "Hellens", + "email": "whellensq@wikispaces.com", + "gender": "Male", + "birthDate": new Date("1986/01/20"), + "picture": "images/employees/02.png", +}, { + "id": 28, + "firstName": "Annnora", + "lastName": "Garahan", + "email": "agarahanr@sina.com.cn", + "gender": "Female", + "birthDate": new Date("1986/06/09"), + "picture": "images/employees/02.png", +}, { + "id": 29, + "firstName": "Thaxter", + "lastName": "Pembridge", + "email": "tpembridges@about.com", + "gender": "Male", + "birthDate": new Date("1991/03/06"), + "picture": "images/employees/02.png", +}, { + "id": 30, + "firstName": "Annabela", + "lastName": "Hannaway", + "email": "ahannawayt@dion.ne.jp", + "gender": "Non-binary", + "birthDate": new Date("1996/01/15"), + "picture": "images/employees/02.png", +}, { + "id": 31, + "firstName": "Nelson", + "lastName": "Geerdts", + "email": "ngeerdtsu@abc.net.au", + "gender": "Male", + "birthDate": new Date("1999/03/12"), + "picture": "images/employees/02.png", +}, { + "id": 32, + "firstName": "Viviyan", + "lastName": "Collet", + "email": "vcolletv@ox.ac.uk", + "gender": "Female", + "birthDate": new Date("1984/09/20"), + "picture": "images/employees/02.png", +}, { + "id": 33, + "firstName": "Pammy", + "lastName": "Sneyd", + "email": "psneydw@nyu.edu", + "gender": "Female", + "birthDate": new Date("1998/04/30"), + "picture": "images/employees/02.png", +}, { + "id": 34, + "firstName": "Sofia", + "lastName": "Yekel", + "email": "syekelx@netscape.com", + "gender": "Agender", + "birthDate": new Date("1990/03/27"), + "picture": "images/employees/02.png", +}, { + "id": 35, + "firstName": "Mose", + "lastName": "Crowcombe", + "email": "mcrowcombey@yahoo.com", + "gender": "Male", + "birthDate": new Date("1993/11/14"), + "picture": "images/employees/02.png", +}, { + "id": 36, + "firstName": "Marcello", + "lastName": "Squibb", + "email": "msquibbz@themeforest.net", + "gender": "Male", + "birthDate": new Date("1997/12/06"), + "picture": "images/employees/02.png", +}, { + "id": 37, + "firstName": "Tiebold", + "lastName": "Tippings", + "email": "ttippings10@shop-pro.jp", + "gender": "Male", + "birthDate": new Date("1987/06/28"), + "picture": "images/employees/02.png", +}, { + "id": 38, + "firstName": "Rosemarie", + "lastName": "Saiens", + "email": "rsaiens11@163.com", + "gender": "Female", + "birthDate": new Date("1980/08/30"), + "picture": "images/employees/02.png", +}, { + "id": 39, + "firstName": "Dylan", + "lastName": "Lugton", + "email": "dlugton12@elpais.com", + "gender": "Male", + "birthDate": new Date("1997/01/18"), + "picture": "images/employees/02.png", +}, { + "id": 40, + "firstName": "Maxim", + "lastName": "Laphorn", + "email": "mlaphorn13@vinaora.com", + "gender": "Male", + "birthDate": new Date("1990/10/14"), + "picture": "images/employees/02.png", +}, { + "id": 41, + "firstName": "Charity", + "lastName": "Lorking", + "email": "clorking14@surveymonkey.com", + "gender": "Female", + "birthDate": new Date("1998/07/07"), + "picture": "images/employees/02.png", +}, { + "id": 42, + "firstName": "Stevie", + "lastName": "Wagenen", + "email": "swagenen15@archive.org", + "gender": "Male", + "birthDate": new Date("1989/12/13"), + "picture": "images/employees/02.png", +}, { + "id": 43, + "firstName": "Julita", + "lastName": "Hopfner", + "email": "jhopfner16@google.ca", + "gender": "Female", + "birthDate": new Date("1998/10/11"), + "picture": "images/employees/02.png", +}, { + "id": 44, + "firstName": "Ethel", + "lastName": "Murdy", + "email": "emurdy17@typepad.com", + "gender": "Female", + "birthDate": new Date("1987/05/19"), + "picture": "images/employees/02.png", +}, { + "id": 45, + "firstName": "Freeland", + "lastName": "Brimham", + "email": "fbrimham18@army.mil", + "gender": "Male", + "birthDate": new Date("1994/01/18"), + "picture": "images/employees/02.png", +}, { + "id": 46, + "firstName": "Fons", + "lastName": "Mangeon", + "email": "fmangeon19@bravesites.com", + "gender": "Male", + "birthDate": new Date("1998/04/22"), + "picture": "images/employees/02.png", +}, { + "id": 47, + "firstName": "Gregoor", + "lastName": "Disney", + "email": "gdisney1a@disqus.com", + "gender": "Male", + "birthDate": new Date("1991/06/04"), + "picture": "images/employees/02.png", +}, { + "id": 48, + "firstName": "Thorny", + "lastName": "Descroix", + "email": "tdescroix1b@wunderground.com", + "gender": "Male", + "birthDate": new Date("1981/03/15"), + "picture": "images/employees/02.png", +}, { + "id": 49, + "firstName": "Olwen", + "lastName": "Clewett", + "email": "oclewett1c@merriam-webster.com", + "gender": "Female", + "birthDate": new Date("1986/09/28"), + "picture": "images/employees/02.png", +}, { + "id": 50, + "firstName": "Kendal", + "lastName": "Scrauniage", + "email": "kscrauniage1d@cmu.edu", + "gender": "Male", + "birthDate": new Date("1982/09/25"), + "picture": "images/employees/02.png", +}, { + "id": 51, + "firstName": "Mendie", + "lastName": "Bonallack", + "email": "mbonallack1e@amazon.com", + "gender": "Male", + "birthDate": new Date("1985/05/07"), + "picture": "images/employees/02.png", +}, { + "id": 52, + "firstName": "Nellie", + "lastName": "Brave", + "email": "nbrave1f@freewebs.com", + "gender": "Bigender", + "birthDate": new Date("1997/06/10"), + "picture": "images/employees/02.png", +}, { + "id": 53, + "firstName": "Shir", + "lastName": "Lipson", + "email": "slipson1g@nydailynews.com", + "gender": "Female", + "birthDate": new Date("1994/11/29"), + "picture": "images/employees/02.png", +}, { + "id": 54, + "firstName": "Brittney", + "lastName": "Barley", + "email": "bbarley1h@jigsy.com", + "gender": "Female", + "birthDate": new Date("1999/04/17"), + "picture": "images/employees/02.png", +}, { + "id": 55, + "firstName": "Pedro", + "lastName": "Spurrett", + "email": "pspurrett1i@mysql.com", + "gender": "Genderfluid", + "birthDate": new Date("1981/08/31"), + "picture": "images/employees/02.png", +}, { + "id": 56, + "firstName": "Merrielle", + "lastName": "Davana", + "email": "mdavana1j@booking.com", + "gender": "Female", + "birthDate": new Date("1985/07/25"), + "picture": "images/employees/02.png", +}, { + "id": 57, + "firstName": "Tildie", + "lastName": "Sacaze", + "email": "tsacaze1k@livejournal.com", + "gender": "Female", + "birthDate": new Date("1998/08/26"), + "picture": "images/employees/02.png", +}, { + "id": 58, + "firstName": "Hurley", + "lastName": "Loomis", + "email": "hloomis1l@phoca.cz", + "gender": "Male", + "birthDate": new Date("1980/06/23"), + "picture": "images/employees/02.png", +}, { + "id": 59, + "firstName": "Rowland", + "lastName": "Keeling", + "email": "rkeeling1m@umn.edu", + "gender": "Male", + "birthDate": new Date("1989/05/13"), + "picture": "images/employees/02.png", +}, { + "id": 60, + "firstName": "Bonita", + "lastName": "Harwin", + "email": "bharwin1n@adobe.com", + "gender": "Female", + "birthDate": new Date("1988/09/03"), + "picture": "images/employees/02.png", +}, { + "id": 61, + "firstName": "Elva", + "lastName": "Doswell", + "email": "edoswell1o@amazonaws.com", + "gender": "Female", + "birthDate": new Date("1985/08/25"), + "picture": "images/employees/02.png", +}, { + "id": 62, + "firstName": "Zeb", + "lastName": "McQuode", + "email": "zmcquode1p@berkeley.edu", + "gender": "Male", + "birthDate": new Date("1998/03/24"), + "picture": "images/employees/02.png", +}, { + "id": 63, + "firstName": "Pearce", + "lastName": "Yannikov", + "email": "pyannikov1q@weibo.com", + "gender": "Male", + "birthDate": new Date("1988/09/19"), + "picture": "images/employees/02.png", +}, { + "id": 64, + "firstName": "Keeley", + "lastName": "Starford", + "email": "kstarford1r@huffingtonpost.com", + "gender": "Female", + "birthDate": new Date("1998/11/29"), + "picture": "images/employees/02.png", +}, { + "id": 65, + "firstName": "Kurt", + "lastName": "Surby", + "email": "ksurby1s@hostgator.com", + "gender": "Male", + "birthDate": new Date("1981/09/19"), + "picture": "images/employees/02.png", +}, { + "id": 66, + "firstName": "Dwight", + "lastName": "Sickling", + "email": "dsickling1t@constantcontact.com", + "gender": "Male", + "birthDate": new Date("1988/10/24"), + "picture": "images/employees/02.png", +}, { + "id": 67, + "firstName": "Vin", + "lastName": "Tawse", + "email": "vtawse1u@cornell.edu", + "gender": "Female", + "birthDate": new Date("1995/10/13"), + "picture": "images/employees/02.png", +}, { + "id": 68, + "firstName": "Lyda", + "lastName": "Edgcombe", + "email": "ledgcombe1v@yelp.com", + "gender": "Female", + "birthDate": new Date("1983/07/23"), + "picture": "images/employees/02.png", +}, { + "id": 69, + "firstName": "Oliviero", + "lastName": "Fewell", + "email": "ofewell1w@creativecommons.org", + "gender": "Male", + "birthDate": new Date("1987/10/25"), + "picture": "images/employees/02.png", +}, { + "id": 70, + "firstName": "Erick", + "lastName": "Hatchett", + "email": "ehatchett1x@wufoo.com", + "gender": "Male", + "birthDate": new Date("1994/12/04"), + "picture": "images/employees/02.png", +}, { + "id": 71, + "firstName": "Magdalena", + "lastName": "Jex", + "email": "mjex1y@friendfeed.com", + "gender": "Female", + "birthDate": new Date("1989/09/03"), + "picture": "images/employees/02.png", +}, { + "id": 72, + "firstName": "Carver", + "lastName": "Kisting", + "email": "ckisting1z@netvibes.com", + "gender": "Male", + "birthDate": new Date("1996/06/13"), + "picture": "images/employees/02.png", +}, { + "id": 73, + "firstName": "Dore", + "lastName": "Carff", + "email": "dcarff20@about.com", + "gender": "Male", + "birthDate": new Date("1997/09/16"), + "picture": "images/employees/02.png", +}, { + "id": 74, + "firstName": "Antonius", + "lastName": "John", + "email": "ajohn21@amazon.co.uk", + "gender": "Male", + "birthDate": new Date("1981/01/23"), + "picture": "images/employees/02.png", +}, { + "id": 75, + "firstName": "Bone", + "lastName": "Lourenco", + "email": "blourenco22@sogou.com", + "gender": "Male", + "birthDate": new Date("1995/06/10"), + "picture": "images/employees/02.png", +}, { + "id": 76, + "firstName": "Wakefield", + "lastName": "Tandey", + "email": "wtandey23@elpais.com", + "gender": "Male", + "birthDate": new Date("1996/02/08"), + "picture": "images/employees/02.png", +}, { + "id": 77, + "firstName": "Juditha", + "lastName": "Gumm", + "email": "jgumm24@goo.ne.jp", + "gender": "Female", + "birthDate": new Date("1991/06/21"), + "picture": "images/employees/02.png", +}, { + "id": 78, + "firstName": "Wilone", + "lastName": "Lafaye", + "email": "wlafaye25@pcworld.com", + "gender": "Female", + "birthDate": new Date("1985/02/17"), + "picture": "images/employees/02.png", +}, { + "id": 79, + "firstName": "Cece", + "lastName": "Gifkins", + "email": "cgifkins26@slate.com", + "gender": "Male", + "birthDate": new Date("1997/11/10"), + "picture": "images/employees/02.png", +}, { + "id": 80, + "firstName": "Darin", + "lastName": "Lyddy", + "email": "dlyddy27@infoseek.co.jp", + "gender": "Male", + "birthDate": new Date("1984/07/07"), + "picture": "images/employees/02.png", +}, { + "id": 81, + "firstName": "Trevar", + "lastName": "Knowller", + "email": "tknowller28@sciencedirect.com", + "gender": "Male", + "birthDate": new Date("1992/12/14"), + "picture": "images/employees/02.png", +}, { + "id": 82, + "firstName": "Milli", + "lastName": "Coppeard", + "email": "mcoppeard29@ted.com", + "gender": "Female", + "birthDate": new Date("1992/03/29"), + "picture": "images/employees/02.png", +}, { + "id": 83, + "firstName": "Cayla", + "lastName": "Davidwitz", + "email": "cdavidwitz2a@yandex.ru", + "gender": "Female", + "birthDate": new Date("1982/02/10"), + "picture": "images/employees/02.png", +}, { + "id": 84, + "firstName": "Laura", + "lastName": "Pherps", + "email": "lpherps2b@tripadvisor.com", + "gender": "Female", + "birthDate": new Date("1980/01/12"), + "picture": "images/employees/02.png", +}, { + "id": 85, + "firstName": "Lianne", + "lastName": "Jennaway", + "email": "ljennaway2c@parallels.com", + "gender": "Female", + "birthDate": new Date("1993/10/12"), + "picture": "images/employees/02.png", +}, { + "id": 86, + "firstName": "Rollin", + "lastName": "Besnardeau", + "email": "rbesnardeau2d@php.net", + "gender": "Agender", + "birthDate": new Date("1999/05/24"), + "picture": "images/employees/02.png", +}, { + "id": 87, + "firstName": "Augustine", + "lastName": "Keaton", + "email": "akeaton2e@surveymonkey.com", + "gender": "Female", + "birthDate": new Date("1985/08/16"), + "picture": "images/employees/02.png", +}, { + "id": 88, + "firstName": "Wynn", + "lastName": "Couthard", + "email": "wcouthard2f@webmd.com", + "gender": "Male", + "birthDate": new Date("1990/08/06"), + "picture": "images/employees/02.png", +}, { + "id": 89, + "firstName": "Stirling", + "lastName": "Cleevely", + "email": "scleevely2g@java.com", + "gender": "Agender", + "birthDate": new Date("1999/04/28"), + "picture": "images/employees/02.png", +}, { + "id": 90, + "firstName": "Tracee", + "lastName": "Maitland", + "email": "tmaitland2h@ning.com", + "gender": "Female", + "birthDate": new Date("1999/12/21"), + "picture": "images/employees/02.png", +}, { + "id": 91, + "firstName": "Joscelin", + "lastName": "Dregan", + "email": "jdregan2i@ucla.edu", + "gender": "Female", + "birthDate": new Date("1999/01/30"), + "picture": "images/employees/02.png", +}, { + "id": 92, + "firstName": "Rolfe", + "lastName": "Tilt", + "email": "rtilt2j@drupal.org", + "gender": "Male", + "birthDate": new Date("1996/02/03"), + "picture": "images/employees/02.png", +}, { + "id": 93, + "firstName": "Bordy", + "lastName": "Beyne", + "email": "bbeyne2k@zdnet.com", + "gender": "Male", + "birthDate": new Date("1992/07/18"), + "picture": "images/employees/02.png", +}, { + "id": 94, + "firstName": "Grier", + "lastName": "Spaducci", + "email": "gspaducci2l@bravesites.com", + "gender": "Female", + "birthDate": new Date("1995/11/06"), + "picture": "images/employees/02.png", +}, { + "id": 95, + "firstName": "Licha", + "lastName": "Witcherley", + "email": "lwitcherley2m@latimes.com", + "gender": "Female", + "birthDate": new Date("1987/01/09"), + "picture": "images/employees/02.png", +}, { + "id": 96, + "firstName": "Cori", + "lastName": "Gwatkins", + "email": "cgwatkins2n@marriott.com", + "gender": "Male", + "birthDate": new Date("1984/08/26"), + "picture": "images/employees/02.png", +}, { + "id": 97, + "firstName": "Annecorinne", + "lastName": "Abbey", + "email": "aabbey2o@miitbeian.gov.cn", + "gender": "Agender", + "birthDate": new Date("1999/05/03"), + "picture": "images/employees/02.png", +}, { + "id": 98, + "firstName": "Denice", + "lastName": "Isaksen", + "email": "disaksen2p@indiatimes.com", + "gender": "Female", + "birthDate": new Date("1994/05/10"), + "picture": "images/employees/02.png", +}, { + "id": 99, + "firstName": "Sabrina", + "lastName": "Oulett", + "email": "soulett2q@ifeng.com", + "gender": "Female", + "birthDate": new Date("1985/01/14"), + "picture": "images/employees/02.png", +}, { + "id": 100, + "firstName": "Jarrod", + "lastName": "Hellewell", + "email": "jhellewell2r@springer.com", + "gender": "Male", + "birthDate": new Date("1985/08/20"), + "picture": "images/employees/02.png", +}]; diff --git a/apps/react-storybook/stories/card_view/templates.tsx b/apps/react-storybook/stories/card_view/templates.tsx new file mode 100644 index 000000000000..0a980d103cb0 --- /dev/null +++ b/apps/react-storybook/stories/card_view/templates.tsx @@ -0,0 +1,14 @@ +import Button from "devextreme/ui/button" + +export function renderFooter() { + const container = document.createElement('div'); + const button1 = document.createElement('div'); + const button2 = document.createElement('div'); + + container.append(button1, button2); + + new Button(button1, {text: 'button 1'}); + new Button(button2, {text: 'button 2'}); + + return container; +} \ No newline at end of file diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/a11y.functional.ts b/e2e/testcafe-devextreme/tests/cardView/columnChooser/a11y.functional.ts new file mode 100644 index 000000000000..b8a7ed48ca1a --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/columnChooser/a11y.functional.ts @@ -0,0 +1,22 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`CardView - ColumnChooser.A11y.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('column chooser popup should have aria-label attribute', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const columnChooser = cardView.getColumnChooser(); + + await cardView.apiShowColumnChooser(); + + await t.expect(columnChooser.content.getAttribute('aria-label')).ok(); +}).before(async () => createWidget('dxCardView', { + columnChooser: { + enabled: true, + }, + columns: ['Column 1'], +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/api.functional.ts b/e2e/testcafe-devextreme/tests/cardView/columnChooser/api.functional.ts new file mode 100644 index 000000000000..5f1a73eeb508 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/columnChooser/api.functional.ts @@ -0,0 +1,44 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`CardView - ColumnChooser.Functional` + .page(url(__dirname, '../../container.html')); + +// TODO: refator this when cardView is merged to 25.1 . This should be in ColumnChooser POM +const isOpened = (columnChooser) => columnChooser.element.exists; + +test('public method showColumnChooser', async (t) => { + const cardView = new CardView('#container'); + const columnChooser = cardView.getColumnChooser(); + + await t.expect(isOpened(columnChooser)).notOk(); + + await cardView.apiShowColumnChooser(); + await t.expect(isOpened(columnChooser)).ok(); +}).before(async () => { + await createWidget('dxCardView', { + columns: ['Column 1'], + columnChooser: { + enabled: true, + }, + }); +}); + +test('public method hideColumnChooser', async (t) => { + const cardView = new CardView('#container'); + const columnChooser = cardView.getColumnChooser(); + + await t.click(cardView.getColumnChooserButton()); + await t.expect(isOpened(columnChooser)).ok(); + + await cardView.apiHideColumnChooser(); + await t.expect(isOpened(columnChooser)).notOk(); +}).before(async () => { + await createWidget('dxCardView', { + columns: ['Column 1'], + columnChooser: { + enabled: true, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (fluent-blue-light).png new file mode 100644 index 000000000000..50f1a51a6783 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (generic-light).png new file mode 100644 index 000000000000..c11f6eb3b6f4 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (material-blue-light).png new file mode 100644 index 000000000000..e02c71ffb903 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/columnChooser/etalons/card-view_column-chooser (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/columnChooser/visual.ts b/e2e/testcafe-devextreme/tests/cardView/columnChooser/visual.ts new file mode 100644 index 000000000000..f41bca9eea68 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/columnChooser/visual.ts @@ -0,0 +1,43 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import url from '../../../helpers/getPageUrl'; +import { testScreenshot } from '../../../helpers/themeUtils'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture`CardView - ColumnChooser.Visual` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('column chooser in select mode', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + const columnChooser = cardView.getColumnChooser(); + + await cardView.apiShowColumnChooser(); + + await testScreenshot(t, takeScreenshot, 'card-view_column-chooser.png', { element: columnChooser.content }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + columnChooser: { + enabled: true, + mode: 'select', + height: 400, + width: 400, + search: { + enabled: true, + }, + selection: { + allowSelectAll: true, + }, + }, + columns: [ + { dataField: 'Column 1', visible: false }, + { dataField: 'Column 2', allowHiding: false }, + { dataField: 'Column 3', showInColumnChooser: false }, + { dataField: 'Column 4' }, + ], +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/cover.visual.ts b/e2e/testcafe-devextreme/tests/cardView/cover.visual.ts new file mode 100644 index 000000000000..49183b3d7956 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/cover.visual.ts @@ -0,0 +1,60 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../helpers/getPageUrl'; +import { createWidget } from '../../helpers/createWidget'; +import { testScreenshot } from '../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - HeaderPanel` + .page(url(__dirname, '../container.html')); + +test('default render', async (t) => { + const cardView = new CardView('#container'); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await testScreenshot(t, takeScreenshot, 'cover-default-render.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + width: 1000, + height: 600, + columns: ['Customer', 'Order Date'], + cardCover: { + imageExpr: (data) => data.Picture && `https://js.devexpress.com/jQuery/Demos/WidgetsGallery/JSDemos/${data.Picture}`, + altExpr: 'FirstName', + }, + dataSource: [{ + ID: 1, + FirstName: 'John', + LastName: 'Heart', + Prefix: 'Mr.', + Position: 'CEO', + Picture: 'images/employees/01.png', + BirthDate: '1964/03/16', + HireDate: '1995/01/15', + Notes: 'John has been in the Audio/Video industry since 1990. He has led DevAv as its CEO since 2003. When not working hard as the CEO, John loves to golf and bowl. He once bowled a perfect game of 300.', + Address: '351 S Hill St.', + }, { + ID: 2, + FirstName: 'Olivia', + LastName: 'Peyton', + Prefix: 'Mrs.', + Position: 'Sales Assistant', + BirthDate: '1981/06/03', + HireDate: '2012/05/14', + Notes: 'Olivia loves to sell. She has been selling DevAV products since 2012. Olivia was homecoming queen in high school. She is expecting her first child in 6 months. Good Luck Olivia.', + Address: '807 W Paseo Del Mar', + }, { + ID: 3, + FirstName: 'Robert', + LastName: 'Reagan', + Prefix: 'Mr.', + Position: 'CMO', + Picture: 'images/employees/03.png', + BirthDate: '1974/09/07', + HireDate: '2002/11/08', + Notes: 'Robert was recently voted the CMO of the year by CMO Magazine. He is a proud member of the DevAV Management Team. Robert is a championship BBQ chef, so when you get the chance ask him for his secret recipe.', + Address: '4 Westmoreland Pl.', + }], +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/content-no-data (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/content-no-data (fluent-blue-light).png new file mode 100644 index 000000000000..f79cf45ee4fa Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/content-no-data (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/content-no-data (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/content-no-data (generic-light).png new file mode 100644 index 000000000000..10f8a6b00144 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/content-no-data (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/content-no-data (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/content-no-data (material-blue-light).png new file mode 100644 index 000000000000..30fefb9e57cf Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/content-no-data (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (fluent-blue-light).png new file mode 100644 index 000000000000..7fa98a1283b9 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (generic-light).png new file mode 100644 index 000000000000..76f103f97e1c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (material-blue-light).png new file mode 100644 index 000000000000..1edeb1120d85 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/cover-default-render (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (fluent-blue-light).png new file mode 100644 index 000000000000..a57856ecb50c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (generic-light).png new file mode 100644 index 000000000000..b910aeaa46d8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (material-blue-light).png new file mode 100644 index 000000000000..7fedb7e297a9 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/header-panel (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/etalons/headers.png b/e2e/testcafe-devextreme/tests/cardView/etalons/headers.png new file mode 100644 index 000000000000..f867d818c52a Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/etalons/headers.png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilder.functional.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilder.functional.ts new file mode 100644 index 000000000000..78e03d467938 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilder.functional.ts @@ -0,0 +1,55 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - FilterBuilder API` + .page(url(__dirname, '../../container.html')); + +test('filterBuilder.height API', async (t) => { + const cardView = new CardView('#container'); + const filterBuilderPopup = await cardView.getFilterPanel().openFilterBuilderPopup(t); + + await t + .expect(filterBuilderPopup.getFilterBuilder().element.clientHeight) + .eql(500); + + await cardView.apiOption('filterBuilder.height', 700); + + await t + .expect(filterBuilderPopup.getFilterBuilder().element.clientHeight) + .eql(700); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterBuilder: { + height: 500, + }, + }, + }); +}); + +test('filterBuilder.hint API', async (t) => { + const cardView = new CardView('#container'); + const filterBuilderPopup = await cardView.getFilterPanel().openFilterBuilderPopup(t); + + await t + .expect(filterBuilderPopup.getFilterBuilder().element.getAttribute('title')) + .eql('Test'); + + await cardView.apiOption('filterBuilder.hint', 'Test2'); + + await t + .expect(filterBuilderPopup.getFilterBuilder().element.getAttribute('title')) + .eql('Test2'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterBuilder: { + hint: 'Test', + }, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilderPopup.functional.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilderPopup.functional.ts new file mode 100644 index 000000000000..ef7fffb7cb4d --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.filterBuilderPopup.functional.ts @@ -0,0 +1,55 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - FilterBuilderPopup API` + .page(url(__dirname, '../../container.html')); + +test('filterBuilderPopup.height API', async (t) => { + const cardView = new CardView('#container'); + const filterBuilderPopup = await cardView.getFilterPanel().openFilterBuilderPopup(t); + + await t + .expect(filterBuilderPopup.asPopup().content.offsetHeight) + .eql(500); + + await cardView.apiOption('filterBuilderPopup.height', 700); + + await t + .expect(filterBuilderPopup.asPopup().content.offsetHeight) + .eql(700); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterBuilderPopup: { + height: 500, + }, + }, + }); +}); + +test('filterBuilderPopup.title API', async (t) => { + const cardView = new CardView('#container'); + const filterBuilderPopup = await cardView.getFilterPanel().openFilterBuilderPopup(t); + + await t + .expect(filterBuilderPopup.asPopup().getToolbar().innerText) + .eql('Test'); + + await cardView.apiOption('filterBuilderPopup.title', 'Test2'); + + await t + .expect(filterBuilderPopup.asPopup().getToolbar().innerText) + .eql('Test2'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterBuilderPopup: { + title: 'Test', + }, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.functional.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.functional.ts new file mode 100644 index 000000000000..946a11305ff7 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/api.functional.ts @@ -0,0 +1,194 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - FilterPanel API` + .page(url(__dirname, '../../container.html')); + +test('filterPanel.customizeText API', async (t) => { + const cardView = new CardView('#container'); + + await t + .expect(cardView.getFilterPanel().getFilterText().element.innerText) + .eql('Men'); + + await cardView.apiOption('filterPanel.customizeText', (e) => { + if (e.text === '[Title] Equals \'Mr.\'') { + return 'Not women'; + } + if (e.text === '[Title] Equals \'Mrs.\'') { + return 'Not men'; + } + return e.text; + }); + + await t + .expect(cardView.getFilterPanel().getFilterText().element.innerText) + .eql('Not women'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + customizeText(e) { + if (e.text === '[Title] Equals \'Mr.\'') { + return 'Men'; + } + if (e.text === '[Title] Equals \'Mrs.\'') { + return 'Women'; + } + return e.text; + }, + }, + }, + }); +}); + +test('filterEnabled API', async (t) => { + const cardView = new CardView('#container'); + await t + .expect(cardView.getFilterPanel().getFilterEnabledCheckbox().isChecked) + .notOk() + .expect(cardView.getCards().count) + .eql(4); + + await cardView.apiOption('filterPanel.filterEnabled', true); + + await t + .expect(cardView.getFilterPanel().getFilterEnabledCheckbox().isChecked) + .ok() + .expect(cardView.getCards().count) + .eql(3); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + filterEnabled: false, + }, + }, + }); +}); + +test('filterPanel.texts API', async (t) => { + const cardView = new CardView('#container'); + const filterPanel = cardView.getFilterPanel(); + await t + .expect(filterPanel.getFilterEnabledCheckbox().element.getAttribute('title')) + .eql('Custom Filter Enabled Hint') + .expect(filterPanel.getClearFilterButton().element.innerText) + .eql('Custom Clear Filter'); + + await cardView.apiOption('filterPanel.texts.clearFilter', 'Custom Clear Filter2'); + await cardView.apiOption('filterPanel.texts.filterEnabledHint', 'Custom Filter Enabled Hint2'); + + await t + .expect(filterPanel.getFilterEnabledCheckbox().element.getAttribute('title')) + .eql('Custom Filter Enabled Hint2') + .expect(filterPanel.getClearFilterButton().element.innerText) + .eql('Custom Clear Filter2'); + + await t + .click(filterPanel.getClearFilterButton().element) + .expect(filterPanel.getFilterText().element.innerText) + .eql('Custom Create Filter'); + + await cardView.apiOption('filterPanel.texts.createFilter', 'Custom Create Filter2'); + + await t + .expect(filterPanel.getFilterText().element.innerText) + .eql('Custom Create Filter2'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + texts: { + clearFilter: 'Custom Clear Filter', + createFilter: 'Custom Create Filter', + filterEnabledHint: 'Custom Filter Enabled Hint', + }, + }, + }, + }); +}); + +test('filterPanel.visible API', async (t) => { + const cardView = new CardView('#container'); + + await t + .expect(cardView.getFilterPanel().element.exists) + .notOk(); + + await cardView.apiOption('filterPanel.visible', true); + + await t + .expect(cardView.getFilterPanel().element.exists) + .ok(); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + visible: false, + }, + }, + }); +}); + +test('filterValue API', async (t) => { + const cardView = new CardView('#container'); + const filterText = cardView.getFilterPanel().getFilterText(); + + await t + .expect(filterText.element.innerText) + .eql('[Title] Equals \'Mr.\''); + + await cardView.apiOption('filterValue', ['title', '=', 'Mrs.']); + + await t + .expect(filterText.element.innerText) + .eql('[Title] Equals \'Mrs.\''); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + }, + }); +}); + +test('clearFilter API', async (t) => { + const cardView = new CardView('#container'); + const filterText = cardView.getFilterPanel().getFilterText(); + + await t + .expect(filterText.element.innerText) + .eql('[Title] Equals \'Mr.\'') + .expect(cardView.getCards().count) + .eql(3); + + await cardView.apiClearFilter(); + + await t + .expect(filterText.element.innerText) + .eql('Create Filter') + .expect(cardView.getCards().count) + .eql(4); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.functional.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.functional.ts new file mode 100644 index 000000000000..edf47ccad66d --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.functional.ts @@ -0,0 +1,235 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import Button from 'devextreme-testcafe-models/button'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - FilterPanel Behavior` + .page(url(__dirname, '../../container.html')); + +test('filterEnabled checkbox switches the filter by click', async (t) => { + const cardView = new CardView('#container'); + const filterEnabledCheckbox = cardView.getFilterPanel().getFilterEnabledCheckbox(); + await t + .expect(filterEnabledCheckbox.isChecked) + .notOk() + .expect(cardView.getCards().count) + .eql(4); + + await t + .click(filterEnabledCheckbox.element) + .expect(filterEnabledCheckbox.isChecked) + .ok() + .expect(cardView.getCards().count) + .eql(3); + + await t + .click(filterEnabledCheckbox.element) + .expect(filterEnabledCheckbox.isChecked) + .notOk() + .expect(cardView.getCards().count) + .eql(4); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + filterEnabled: false, + }, + }, + }); +}); + +test('filterEnabled checkbox switches the filter by keyboard', async (t) => { + const cardView = new CardView('#container'); + const startButton = new Button('#otherContainer'); + const filterEnabledCheckbox = cardView.getFilterPanel().getFilterEnabledCheckbox(); + + await t + .expect(filterEnabledCheckbox.isChecked) + .notOk() + .expect(cardView.getCards().count) + .eql(4); + + await t + .click(startButton.element) + .pressKey('shift+tab shift+tab shift+tab shift+tab') + .pressKey('space') + .expect(filterEnabledCheckbox.isChecked) + .ok() + .expect(cardView.getCards().count) + .eql(3); + + await t + .click(startButton.element) // TODO: remove this when checkbox focus loosing is fixed + .pressKey('shift+tab shift+tab shift+tab shift+tab') + .pressKey('space') + .expect(filterEnabledCheckbox.isChecked) + .notOk() + .expect(cardView.getCards().count) + .eql(4); +}).before(async () => { + await createWidget('dxButton', { + text: 'Click Here First', + }, '#otherContainer'); + + await createWidget('dxCardView', { + ...baseConfig, + ...{ + filterValue: ['title', '=', 'Mr.'], + filterPanel: { + ...baseConfig.filterPanel, + filterEnabled: false, + }, + }, + }); +}); + +test('FilterIcon opens popup by click', async (t) => { + const cardView = new CardView('#container'); + const popup = cardView.getFilterPanel().getFilterBuilderPopup(); + + await t + .expect(popup.element.exists) + .notOk() + .click(cardView.getFilterPanel().getIconFilter().element) + .expect(popup.element.exists) + .ok(); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('FilterIcon opens popup by keyboard', async (t) => { + const cardView = new CardView('#container'); + const startButton = new Button('#otherContainer'); + const popup = cardView.getFilterPanel().getFilterBuilderPopup(); + + await t + .expect(popup.element.exists) + .notOk() + .click(startButton.element) + .pressKey('shift+tab shift+tab') + .pressKey('enter') + .expect(popup.element.exists) + .ok(); +}).before(async () => { + await createWidget('dxButton', { + text: 'Click Here First', + }, '#otherContainer'); + + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('FilterText opens popup by click', async (t) => { + const cardView = new CardView('#container'); + const popup = cardView.getFilterPanel().getFilterBuilderPopup(); + + await t + .expect(popup.element.exists) + .notOk() + .click(cardView.getFilterPanel().getFilterText().element) + .expect(popup.element.exists) + .ok(); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('FilterText opens popup by click by keyboard', async (t) => { + const cardView = new CardView('#container'); + const startButton = new Button('#otherContainer'); + const popup = cardView.getFilterPanel().getFilterBuilderPopup(); + + await t + .expect(popup.element.exists) + .notOk() + .click(startButton.element) + .pressKey('shift+tab') + .pressKey('enter') + .expect(popup.element.exists) + .ok(); +}).before(async () => { + await createWidget('dxButton', { + text: 'Click Here First', + }, '#otherContainer'); + + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('ClearFilter button clears filter by click', async (t) => { + const cardView = new CardView('#container'); + + await t + .expect(cardView.option('filterValue')) + .eql(['title', '=', 'Mr.']); + + await t + .click(cardView.getFilterPanel().getClearFilterButton().element) + .expect(cardView.option('filterValue')) + .eql(null); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + filterValue: ['title', '=', 'Mr.'], + }); +}); + +test('ClearFilter button clears filter by keyboard', async (t) => { + const cardView = new CardView('#container'); + const startButton = new Button('#otherContainer'); + + await t + .expect(cardView.option('filterValue')) + .eql(['title', '=', 'Mr.']); + + await t + .click(startButton.element) + .pressKey('shift+tab') + .pressKey('enter') + .expect(cardView.option('filterValue')) + .eql(null); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + filterValue: ['title', '=', 'Mr.'], + }); + + await createWidget('dxButton', { + text: 'Click Here First', + }, '#otherContainer'); +}); + +test('Focus returns to FilterIcon after FilterPopup is closed', async (t) => { + const cardView = new CardView('#container'); + const startButton = new Button('#otherContainer'); + const filterIcon = cardView.getFilterPanel().getIconFilter(); + + await t + .click(startButton.element) + .pressKey('shift+tab shift+tab') + .expect(filterIcon.element.focused) + .ok() + .pressKey('enter') + .expect(filterIcon.element.focused) + .notOk() + .pressKey('esc') + .expect(filterIcon.element.focused) + .ok(); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + }); + + await createWidget('dxButton', { + text: 'Click Here First', + }, '#otherContainer'); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.themes.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.themes.ts new file mode 100644 index 000000000000..e92c62e92a14 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/behavior.themes.ts @@ -0,0 +1,30 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - FilterPanel Appearance` + .page(url(__dirname, '../../container.html')); + +test('FilterPanel and FilterBuilderPopup screenshots', async (t) => { + const cardView = new CardView('#container'); + const popup = cardView.getFilterPanel().getFilterBuilderPopup(); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await testScreenshot(t, takeScreenshot, 'cardView_FilterPanel.png', { element: cardView.getFilterPanel().element }); + + await t.click(cardView.getFilterPanel().getIconFilter().element); + + await testScreenshot(t, takeScreenshot, 'cardView_FilterBuilderPopup.png', { element: popup.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + filterValue: ['title', '=', 'Mr.'], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (fluent-blue-light).png new file mode 100644 index 000000000000..e98a5545387c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (generic-light).png new file mode 100644 index 000000000000..6d421e6173e0 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (material-blue-light).png new file mode 100644 index 000000000000..97302df1d94b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterBuilderPopup (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (fluent-blue-light).png new file mode 100644 index 000000000000..6f5e86366ec8 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (generic-light).png new file mode 100644 index 000000000000..b0343b4560a9 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (material-blue-light).png new file mode 100644 index 000000000000..e0b9976b3b6b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/filterPanel/etalons/cardView_FilterPanel (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/filterPanel/helpers/baseConfig.ts b/e2e/testcafe-devextreme/tests/cardView/filterPanel/helpers/baseConfig.ts new file mode 100644 index 000000000000..8e9cb7ecf2a0 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/filterPanel/helpers/baseConfig.ts @@ -0,0 +1,22 @@ +import { data } from '../../helpers/simpleArrayData'; + +export const baseConfig = { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + filterPanel: { + visible: true, + }, +}; diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/a11y.functional.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/a11y.functional.ts new file mode 100644 index 000000000000..b1a60beae0f2 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/a11y.functional.ts @@ -0,0 +1,66 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`HeaderFilter.A11y.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('should open popup by enter if filter icon in the focused state', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const firstHeaderItem = cardView + .getHeaderPanel() + .getHeaderItem(); + await t.click(firstHeaderItem.element) + .pressKey('alt+down'); + + // NOTE: We check list here, because this list rendered inside popup + const list = cardView.getHeaderFilterList(); + + await t.expect(list.element.exists).ok(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0' }, + { A: 'A_1' }, + { A: 'A_2' }, + ], + columns: [{ dataField: 'A', caption: 'LONG_COLUMN_A_CAPTION' }], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should return focus on the same icon after the popup closing', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const firstHeaderItem = cardView + .getHeaderPanel() + .getHeaderItem(); + await t.click(firstHeaderItem.element) + .pressKey('alt+down'); + + // NOTE: We check list here, because this list rendered inside popup + const list = cardView.getHeaderFilterList(); + await t.expect(list.element.exists).ok(); + + await t + .pressKey('tab') + .pressKey('tab') + .pressKey('enter'); + + await t.expect(firstHeaderItem.element.focused).ok(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0' }, + { A: 'A_1' }, + { A: 'A_2' }, + ], + columns: [{ dataField: 'A', caption: 'LONG_COLUMN_A_CAPTION' }], + headerFilter: { + visible: true, + }, + height: 600, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/api.functional.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/api.functional.ts new file mode 100644 index 000000000000..ed1955923952 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/api.functional.ts @@ -0,0 +1,128 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`HeaderFilter.API.Functional` + .page(url(__dirname, '../../container.html')); + +test('clearFilter API', async (t) => { + const cardView = new CardView('#container'); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + const doneBtn = popup.getButton(0); + const firstItem = list.getItem(0); + + await t + .expect(cardView.getCards().count) + .eql(4) + + .click(firstItem.element) + .click(doneBtn.element) + + .expect(cardView.getCards().count) + .eql(1); + + await cardView.apiClearFilter(); + + await t + .expect(cardView.getCards().count) + .eql(4); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('getCombinedFilter API', async (t) => { + const cardView = new CardView('#container'); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + const doneBtn = popup.getButton(0); + const firstItem = list.getItem(0); + + await t + .expect(cardView.getCards().count) + .eql(4) + + .click(firstItem.element) + .click(doneBtn.element) + + .expect(cardView.getCards().count) + .eql(1); + + await t + .expect(cardView.apiGetCombinedFilter()) + .eql(['id', '=', 1]); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('groupInterval API', async (t) => { + const cardView = new CardView('#container'); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + const doneBtn = popup.getButton(0); + const firstItem = list.getItem(1); + + await t + .expect(cardView.getCards().count) + .eql(4) + + .click(firstItem.element) + .click(doneBtn.element) + + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getFieldValueCell('Id').textContent) + .eql('2') + .expect(cardView.getCard(1).getFieldValueCell('Id').textContent) + .eql('3'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + columns: [ + { + dataField: 'id', + dataType: 'number', + headerFilter: { + groupInterval: 2, + }, + }, + { + dataField: 'title', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/common.functional.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/common.functional.ts new file mode 100644 index 000000000000..57ba21b6afeb --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/common.functional.ts @@ -0,0 +1,260 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { createWidget } from '../../../helpers/createWidget'; +import url from '../../../helpers/getPageUrl'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`HeaderFilter.Common.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('should support custom translations', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + const doneBtn = popup.getButton(0); + const closeBtn = popup.getButton(1); + const firstItem = list.getItem(0); + + await t.expect(doneBtn.text) + .eql('TEST_OK') + .expect(closeBtn.text) + .eql('TEST_CANCEL') + .expect(firstItem.text) + .eql('TEST_EMPTY'); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + calculateCellValue: () => undefined, + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + texts: { + ok: 'TEST_OK', + cancel: 'TEST_CANCEL', + emptyValue: 'TEST_EMPTY', + }, + }, + height: 600, +})); + +test('Filtering different data types', async (t) => { + const cardView = new CardView('#container'); + const headerPanel = cardView.getHeaderPanel(); + + const list = cardView.getHeaderFilterList(); + const firstItem = list.getItem(0); + + const treeView = cardView.getHeaderFilterTreeView(); + + const popup = cardView.getHeaderFilterPopup(); + const doneBtn = popup.getButton(0); + + // Number type + await t + .click(headerPanel.getHeaderItem(0).getFilterIcon()) + .click(firstItem.element) + .click(doneBtn.element) + .expect(cardView.getCards().count) + .eql(1) + .expect(cardView.getCard(0).getFieldValueCell('Id').textContent) + .eql('1'); + await cardView.apiClearFilter(); + + // String type + await t + .click(headerPanel.getHeaderItem(1).getFilterIcon()) + .click(firstItem.element) + .click(doneBtn.element) + .expect(cardView.getCards().count) + .eql(3) + .expect(cardView.getCard(0).getFieldValueCell('Id').textContent) + .eql('1') + .expect(cardView.getCard(1).getFieldValueCell('Id').textContent) + .eql('3') + .expect(cardView.getCard(2).getFieldValueCell('Id').textContent) + .eql('4'); + await cardView.apiClearFilter(); + + // Date type + await t + .click(headerPanel.getHeaderItem(4).getFilterIcon()) + .click(treeView.getNode(0).getExpandButton()) + .click(treeView.getNode(1).getExpandButton()) + .click(treeView.getNode(2).getCheckBox().element) + .click(doneBtn.element) + .expect(cardView.getCards().count) + .eql(1) + .expect(cardView.getCard(0).getFieldValueCell('Id').textContent) + .eql('2'); + await cardView.apiClearFilter(); + + // Boolean type + await t + .click(headerPanel.getHeaderItem(5).getFilterIcon()) + .click(firstItem.element) + .click(doneBtn.element) + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getFieldValueCell('Id').textContent) + .eql('2') + .expect(cardView.getCard(1).getFieldValueCell('Id').textContent) + .eql('4'); + await cardView.apiClearFilter(); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + columns: [ + { + dataField: 'id', + dataType: 'number', + }, + { + dataField: 'title', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + { + dataField: 'birthDate', + dataType: 'date', + groupInterval: 'day', + }, + { + dataField: 'hasOrders', + dataType: 'boolean', + }, + ], + }, + }); +}); + +test('Should apply filter to values in another column', async (t) => { + const cardView = new CardView('#container'); + + const popup = cardView.getHeaderFilterPopup(); + const doneBtn = popup.getButton(0); + const cancelBtn = popup.getButton(1); + + await t + .click(cardView.getHeaderPanel().getHeaderItem(0).getFilterIcon()) + .expect(cardView.getHeaderFilterList().getItems().count) + .eql(4) + .click(cancelBtn.element); + + await t + .click(cardView.getHeaderPanel().getHeaderItem(1).getFilterIcon()) + .click(cardView.getHeaderFilterList().getItem(0).element) + .click(doneBtn.element) + + .expect(cardView.getCards().count) + .eql(3); + + await t + .click(cardView.getHeaderPanel().getHeaderItem(0).getFilterIcon()) + .expect(cardView.getHeaderFilterList().getItems().count) + .eql(3); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('Filter values should not filter themselves', async (t) => { + const cardView = new CardView('#container'); + + const popup = cardView.getHeaderFilterPopup(); + const doneBtn = popup.getButton(0); + const cancelBtn = popup.getButton(1); + + await t + .click(cardView.getHeaderPanel().getHeaderItem(0).getFilterIcon()) + .expect(cardView.getHeaderFilterList().getItems().count) + .eql(4) + .click(cancelBtn.element); + + await t + .click(cardView.getHeaderPanel().getHeaderItem(1).getFilterIcon()) + .click(cardView.getHeaderFilterList().getItem(0).element) + .click(doneBtn.element) + .expect(cardView.getCards().count) + .eql(3); + + await t + .click(cardView.getHeaderPanel().getHeaderItem(0).getFilterIcon()) + .expect(cardView.getHeaderFilterList().getItems().count) + .eql(3) + .click(cardView.getHeaderFilterList().getItem(0).element) + .click(doneBtn.element) + .expect(cardView.getCards().count) + .eql(1); + + await t + .click(cardView.getHeaderPanel().getHeaderItem(0).getFilterIcon()) + .expect(cardView.getHeaderFilterList().getItems().count) + .eql(3); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + }); +}); + +test('Filter values should be filtered by SearchPanel', async (t) => { + const cardView = new CardView('#container'); + + const popup = cardView.getHeaderFilterPopup(); + const cancelBtn = popup.getButton(1); + + await t + .click(cardView.getHeaderPanel().getHeaderItem(0).getFilterIcon()) + .expect(cardView.getHeaderFilterList().getItems().count) + .eql(4) + .click(cancelBtn.element); + + await t + .typeText(cardView.getSearchBox().getInput(), 'rt') + .expect(cardView.getCards().count) + .eql(2); + + await t + .click(cardView.getHeaderPanel().getHeaderItem(0).getFilterIcon()) + .expect(cardView.getHeaderFilterList().getItems().count) + .eql(2) + .expect(cardView.getHeaderFilterList().getItem(0).text) + .eql('1') + .expect(cardView.getHeaderFilterList().getItem(1).text) + .eql('3'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + ...{ + searchPanel: { + visible: true, + }, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/helpers/baseConfig.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/helpers/baseConfig.ts new file mode 100644 index 000000000000..2c0ee2e8cd47 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/helpers/baseConfig.ts @@ -0,0 +1,22 @@ +import { data } from '../../helpers/simpleArrayData'; + +export const baseConfig = { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + headerFilter: { + visible: true, + }, +}; diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/local.functional.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/local.functional.ts new file mode 100644 index 000000000000..88786a4ba959 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/local.functional.ts @@ -0,0 +1,543 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +// TODO: Write integration test with filtering after filtering will be implemented +fixture.disablePageReloads`HeaderFilter.LocalDataSource.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('list should contain all column values', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(5); + + for (let idx = 0; idx < 5; idx += 1) { + await t.expect(list.getItem(idx).text).eql(`A_${idx}`); + } + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('list should contain all column values from all pages', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(5); + + for (let idx = 0; idx < 5; idx += 1) { + await t.expect(list.getItem(idx).text).eql(`A_${idx}`); + } + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + paging: { + pageSize: 1, + pageIndex: 0, + }, + height: 600, +})); + +test('list should contain all values from computed column', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(5); + + for (let idx = 0; idx < 3; idx += 1) { + await t.expect(list.getItem(idx).text).eql(`A_${idx}_B_${idx}`); + } + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + caption: 'Computed', + calculateCellValue: (data) => `${data.A}_${data.B}`, + }, + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should support custom dataSource', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(3); + + for (let idx = 0; idx < 3; idx += 1) { + await t.expect(list.getItem(idx).text).eql(`CUSTOM_${idx}`); + } + + await t.click(cardView.element); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + headerFilter: { + dataSource: [ + { text: 'CUSTOM_0', value: 0 }, + { text: 'CUSTOM_1', value: 1 }, + { text: 'CUSTOM_2', value: 2 }, + ], + }, + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, + }); +}); + +test('should update column options with filterType and values (regular selection)', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + + await t.click(firstItem.element) + .click(secondItem.element) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.filterType).eql(undefined) + .expect(columnOptions.headerFilter.values).eql(['A_0', 'A_1']); + + await t.click(cardView.element); +}).before(async () => { + await createWidget('dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + height: 600, + }); +}); + +test('should update column options with filterType and values (selectAll case #0)', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const selectAllCheckbox = list.selectAll.element; + + await t.click(selectAllCheckbox) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.filterType).eql('exclude') + .expect(columnOptions.headerFilter.values).eql(null); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should update column options with filterType and values (selectAll case #1)', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const selectAllCheckbox = list.selectAll.element; + const firstItem = list.getItem(2); + const secondItem = list.getItem(3); + + await t.click(selectAllCheckbox) + .click(firstItem.element) + .click(secondItem.element) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.filterType).eql('exclude') + .expect(columnOptions.headerFilter.values).eql(['A_2', 'A_3']); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + columns: ['A', 'B', 'C'], + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should apply filter from options (type: "include" by default)', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_0', 'A_1'], + }, + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should apply filter from options (type: "include")', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_0', 'A_1'], + }, + filterType: 'include', + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should apply filter from options (type: "exclude")', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await t.expect(cardView.getHeaderFilterPopup().element.visible).notOk(); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_2', 'A_3', 'A_4'], + }, + filterType: 'exclude', + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should process groupInterval option', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const expectedTexts = [ + '0 - 5', + '5 - 10', + ]; + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(expectedTexts.length); + + for (let idx = 0; idx < expectedTexts.length; idx += 1) { + await t.expect(list.getItem(idx).text).eql(expectedTexts[idx]); + } + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { id: 0, A: 'A_0' }, + { id: 1, A: 'A_1' }, + { id: 2, A: 'A_2' }, + { id: 3, A: 'A_3' }, + { id: 4, A: 'A_4' }, + { id: 5, A: 'A_4' }, + { id: 6, A: 'A_4' }, + { id: 7, A: 'A_4' }, + { id: 8, A: 'A_4' }, + { id: 9, A: 'A_4' }, + ], + columns: [ + { + dataField: 'id', + dataType: 'number', + headerFilter: { + groupInterval: 5, + }, + }, + 'A', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('should not update column options if popup cancel btn clicked', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const cancelBtn = popup.getButton(1); + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + + await t + .click(firstItem.element) + .click(secondItem.element) + .click(cancelBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.filterType).eql(undefined) + .expect(columnOptions.headerFilter.values).eql(['A_4']); + + await t.click(cardView.element); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_4'], + }, + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/remote.functional.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/remote.functional.ts new file mode 100644 index 000000000000..6736c35aa8b2 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/remote.functional.ts @@ -0,0 +1,542 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { ClientFunction } from 'testcafe'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { remoteApiIdGroupMock, remoteApiMock, remoteData } from '../helpers/remoteApiMock'; + +fixture`HeaderFilter.RemoteDataSource.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +const setRemoteOperations = (remoteOperations) => ClientFunction(() => { + (window as any).testRemoteOperations = remoteOperations; +}, { dependencies: { remoteOperations } })(); + +const clearRemoteOperations = () => ClientFunction(() => { + delete (window as any).testRemoteOperations; +})(); + +[ + { remoteOperations: 'auto' }, + { remoteOperations: true }, + { remoteOperations: false }, +].forEach(({ remoteOperations }) => { + test(`remote operations: ${remoteOperations} -> list should contain loaded items`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(remoteData.length); + + for (let idx = 0; idx < remoteData.length; idx += 1) { + await t.expect(list.getItem(idx).text).eql(remoteData[idx].A); + } + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should support custom dataSource`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(3); + + for (let idx = 0; idx < 3; idx += 1) { + await t.expect(list.getItem(idx).text).eql(`CUSTOM_${idx}`); + } + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'A', + headerFilter: { + dataSource: [ + { text: 'CUSTOM_0', value: 0 }, + { text: 'CUSTOM_1', value: 1 }, + { text: 'CUSTOM_2', value: 2 }, + ], + }, + }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should update column options with filterType and values (regular selection)`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + + await t.click(firstItem.element) + .click(secondItem.element) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.filterType).eql(undefined) + .expect(columnOptions.headerFilter.values).eql(['A_0', 'A_1']); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should update column options with filterType and values (selectAll case #0)`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const selectAllCheckbox = list.selectAll.element; + + await t.click(selectAllCheckbox) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.filterType).eql('exclude') + .expect(columnOptions.headerFilter.values).eql(null); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should update column options with filterType and values (selectAll case #1)`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const okBtn = popup.getButton(0); + const selectAllCheckbox = list.selectAll.element; + const firstItem = list.getItem(2); + const secondItem = list.getItem(3); + + await t.click(selectAllCheckbox) + .click(firstItem.element) + .click(secondItem.element) + .click(okBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.filterType).eql('exclude') + .expect(columnOptions.headerFilter.values).eql(['A_2', 'A_3']); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: ['A', 'B', 'C'], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should apply filter from options (type: "include" by default)`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_0', 'A_1'], + }, + }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should apply filter from options (type: "include")`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_0', 'A_1'], + }, + filterType: 'include', + }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should apply filter from options (type: "exclude")`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await t.expect(cardView.getHeaderFilterPopup().element.visible).notOk(); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + const thirdItem = list.getItem(2); + + await t + .expect(firstItem.checkBox.isChecked).ok() + .expect(secondItem.checkBox.isChecked).ok() + .expect(thirdItem.checkBox.isChecked) + .notOk(); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_2', 'A_3', 'A_4'], + }, + filterType: 'exclude', + }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should process groupInterval option`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const expectedTexts = [ + '0 - 5', + '5 - 10', + ]; + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const list = cardView.getHeaderFilterList(); + const itemCount = await list.getItems().count; + + await t.expect(itemCount).eql(expectedTexts.length); + + for (let idx = 0; idx < expectedTexts.length; idx += 1) { + await t.expect(list.getItem(idx).text).eql(expectedTexts[idx]); + } + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiIdGroupMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'id', + dataType: 'number', + headerFilter: { + groupInterval: 5, + }, + }, + 'A', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiIdGroupMock); + await clearRemoteOperations(); + }); + + test(`remote operations: ${remoteOperations} -> should not update column options if popup cancel btn clicked`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + const popup = cardView.getHeaderFilterPopup(); + const list = cardView.getHeaderFilterList(); + + const cancelBtn = popup.getButton(1); + const firstItem = list.getItem(0); + const secondItem = list.getItem(1); + + await t + .click(firstItem.element) + .click(secondItem.element) + .click(cancelBtn.element); + + const columnOptions = await cardView.getColumnOption('A'); + + await t + .expect(columnOptions.filterType).eql(undefined) + .expect(columnOptions.headerFilter.values).eql(['A_4']); + + await t.click(cardView.element); + }).before(async (t) => { + await t.addRequestHooks(remoteApiMock); + await setRemoteOperations(remoteOperations); + await createWidget('dxCardView', () => ({ + dataSource: { + store: (window as any).DevExpress.data.AspNet.createStore({ + key: 'id', + loadUrl: 'https://api/data', + }), + }, + columns: [ + { + dataField: 'A', + headerFilter: { + values: ['A_4'], + }, + }, + 'B', + 'C', + ], + remoteOperations: (window as any).testRemoteOperations, + headerFilter: { + visible: true, + }, + height: 600, + })); + }).after(async (t) => { + await t.removeRequestHooks(remoteApiMock); + await clearRemoteOperations(); + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/headerFilter/visual.ts b/e2e/testcafe-devextreme/tests/cardView/headerFilter/visual.ts new file mode 100644 index 000000000000..9cba523769ad --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerFilter/visual.ts @@ -0,0 +1,112 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +// TODO: Unskip this fixture after markup will be stabilized +fixture.skip`HeaderFilter.Visual` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('popup with list', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + await testScreenshot(t, takeScreenshot, 'card-view_header-filter_popup-with-list.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + }, + height: 600, +})); + +test('popup with search', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + await testScreenshot(t, takeScreenshot, 'card-view_header-filter_popup-with-search.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: 'A_0', B: 'B_0', C: 'C_0' }, + { A: 'A_1', B: 'B_1', C: 'C_1' }, + { A: 'A_2', B: 'B_2', C: 'C_2' }, + { A: 'A_3', B: 'B_3', C: 'C_3' }, + { A: 'A_4', B: 'B_4', C: 'C_4' }, + ], + headerFilter: { + visible: true, + search: { + enabled: true, + }, + }, + height: 600, +})); + +test('popup with tree', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + const filterIcon = cardView + .getHeaderPanel() + .getHeaderItem() + .getFilterIcon(); + await t.click(filterIcon); + + await testScreenshot(t, takeScreenshot, 'card-view_header-filter_popup-with-tree.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { A: '2024-01-01', B: 'B_0', C: 'C_0' }, + { A: '2024-01-01', B: 'B_1', C: 'C_1' }, + { A: '2024-01-01', B: 'B_2', C: 'C_2' }, + { A: '2025-01-01', B: 'B_3', C: 'C_3' }, + { A: '2025-01-01', B: 'B_4', C: 'C_4' }, + { A: '2026-01-01', B: 'B_5', C: 'C_5' }, + ], + columns: [ + { + dataField: 'A', + dataType: 'date', + // TODO calculateCellValue issue: Remove after task will be complete + calculateCellValue: ({ A }) => new Date(A), + }, + 'B', + 'C', + ], + headerFilter: { + visible: true, + }, + height: 600, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/headerPanel.ts b/e2e/testcafe-devextreme/tests/cardView/headerPanel.ts new file mode 100644 index 000000000000..993b0cca707d --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/headerPanel.ts @@ -0,0 +1,23 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../helpers/getPageUrl'; +import { createWidget } from '../../helpers/createWidget'; +import { testScreenshot } from '../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - HeaderPanel` + .page(url(__dirname, '../container.html')); + +test('default render', async (t) => { + const cardView = new CardView('#container'); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await testScreenshot(t, takeScreenshot, 'header-panel.png', { element: cardView.getHeaderPanel().element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + width: 400, + height: 600, + columns: ['Customer', 'Order Date'], +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/helpers/remoteApiMock.ts b/e2e/testcafe-devextreme/tests/cardView/helpers/remoteApiMock.ts new file mode 100644 index 000000000000..52d25ae55513 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/helpers/remoteApiMock.ts @@ -0,0 +1,53 @@ +import { RequestMock } from 'testcafe'; + +export const remoteData = new Array(10) + .fill(null) + .map((_, idx) => ({ + id: idx, A: `A_${idx}`, B: `B_${idx}`, C: `C_${idx}`, + })); + +export const remoteDataGroupedByA = new Array(10) + .fill(null) + .map((_, idx) => ({ + key: `A_${idx}`, + items: null, + })); + +export const remoteApiMock = RequestMock() + .onRequestTo(/\/api\/data\?.*group=/) + .respond( + { + data: remoteDataGroupedByA, + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data/) + .respond( + { + data: remoteData, + }, + 200, + { 'access-control-allow-origin': '*' }, + ); + +export const remoteApiIdGroupMock = RequestMock() + .onRequestTo(/\/api\/data\?.*group=/) + .respond( + { + data: [ + { key: 0, items: null }, + { key: 5, items: null }, + ], + }, + 200, + { 'access-control-allow-origin': '*' }, + ) + .onRequestTo(/\/api\/data/) + .respond( + { + data: remoteData, + }, + 200, + { 'access-control-allow-origin': '*' }, + ); diff --git a/e2e/testcafe-devextreme/tests/cardView/helpers/simpleArrayData.ts b/e2e/testcafe-devextreme/tests/cardView/helpers/simpleArrayData.ts new file mode 100644 index 000000000000..8b556329dc22 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/helpers/simpleArrayData.ts @@ -0,0 +1,34 @@ +export const data = [ + { + id: 1, + title: 'Mr.', + name: 'John', + lastName: 'Heart', + birthDate: new Date('06/10/1980'), + hasOrders: true, + }, + { + id: 2, + title: 'Mrs.', + name: 'Olivia', + lastName: 'Peyton', + birthDate: new Date('06/02/1980'), + hasOrders: false, + }, + { + id: 3, + title: 'Mr.', + name: 'Robert', + lastName: 'Reagan', + birthDate: new Date('06/03/1980'), + hasOrders: true, + }, + { + id: 4, + title: 'Mr.', + name: 'Greta', + lastName: 'Sims', + birthDate: new Date('06/04/1980'), + hasOrders: false, + }, +]; diff --git a/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/contentView.functional.ts b/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/contentView.functional.ts new file mode 100644 index 000000000000..fad4eca90576 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/contentView.functional.ts @@ -0,0 +1,184 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +// TODO: Add card inner KBN tests after editing will be ready +fixture.disablePageReloads`KeyboardNavigation.ContentView` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +const getResultMatrix = (rowIdx: number, colIdx: number) => new Array(9) + .fill(false) + .map((_, idx) => idx === (rowIdx * 3 + colIdx)); + +[ + { caseName: 'arrows -> same first item', keys: 'right left', result: getResultMatrix(1, 1) }, + { caseName: 'arrows -> left item', keys: 'left', result: getResultMatrix(1, 0) }, + { caseName: 'arrows -> right item', keys: 'right', result: getResultMatrix(1, 2) }, + { caseName: 'arrows -> top item', keys: 'up', result: getResultMatrix(0, 1) }, + { caseName: 'arrows -> bottom item', keys: 'down', result: getResultMatrix(2, 1) }, + { caseName: 'arrows -> no left overflow', keys: 'left left left left left', result: getResultMatrix(1, 0) }, + { caseName: 'arrows -> no right overflow', keys: 'right right right right right', result: getResultMatrix(1, 2) }, + { caseName: 'arrows -> no top overflow', keys: 'up up up up up', result: getResultMatrix(0, 1) }, + { caseName: 'arrows -> no bottom overflow', keys: 'down down down down down', result: getResultMatrix(2, 1) }, + { caseName: 'first in same row', keys: 'home', result: getResultMatrix(1, 0) }, + { caseName: 'last in same row', keys: 'end', result: getResultMatrix(1, 2) }, + { caseName: 'first in first row', keys: 'ctrl+home', result: getResultMatrix(0, 0) }, + { caseName: 'last in last row', keys: 'ctrl+end', result: getResultMatrix(2, 2) }, +].forEach(({ caseName, keys, result }) => { + test(`Should move between cards: ${caseName}`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(4); + + await t + .click(firstCard.element) + .pressKey(keys); + + const focusState = [ + await cardView.getCard(0).element.focused, + await cardView.getCard(1).element.focused, + await cardView.getCard(2).element.focused, + await cardView.getCard(3).element.focused, + await cardView.getCard(4).element.focused, + await cardView.getCard(5).element.focused, + await cardView.getCard(6).element.focused, + await cardView.getCard(7).element.focused, + await cardView.getCard(8).element.focused, + ]; + + await t.expect(focusState).eql(result); + }).before(async () => createWidget('dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { + pageSize: 9, + }, + height: 700, + })); +}); + +test('Should change page to the previous one and focus first card', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(1); + const firstCardValue = await firstCard.getFieldValueCell('Id').textContent; + + await t.expect(firstCardValue).eql('4'); + + await t + .click(firstCard.element) + .pressKey('pageup'); + + const targetCardText = await cardView.getCard(0).getFieldValueCell('Id').textContent; + const focusState = [ + await cardView.getCard(0).element.focused, + await cardView.getCard(1).element.focused, + await cardView.getCard(2).element.focused, + ]; + + await t.expect(focusState).eql([true, false, false]); + await t.expect(targetCardText).eql('0'); +}).before(async () => createWidget('dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { + pageSize: 3, + pageIndex: 1, + }, + height: 700, +})); + +test('Should change page to the next one and focus first card', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(1); + const firstCardValue = await firstCard.getFieldValueCell('Id').textContent; + + await t.expect(firstCardValue).eql('4'); + + await t + .click(firstCard.element) + .pressKey('pagedown'); + + const targetCardText = await cardView.getCard(0).getFieldValueCell('Id').textContent; + const focusState = [ + await cardView.getCard(0).element.focused, + await cardView.getCard(1).element.focused, + await cardView.getCard(2).element.focused, + ]; + + await t.expect(focusState).eql([true, false, false]); + await t.expect(targetCardText).eql('6'); +}).before(async () => createWidget('dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { + pageSize: 3, + pageIndex: 1, + }, + height: 700, +})); + +test('Should do nothing if pageup pressed on first page', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(2); + const firstCardValue = await firstCard.getFieldValueCell('Id').textContent; + + await t.expect(firstCardValue).eql('2'); + + await t + .click(firstCard.element) + .pressKey('pageup'); + + const targetCardText = await cardView.getCard(0).getFieldValueCell('Id').textContent; + const focusState = [ + await cardView.getCard(0).element.focused, + await cardView.getCard(1).element.focused, + await cardView.getCard(2).element.focused, + ]; + + await t.expect(focusState).eql([false, false, true]); + await t.expect(targetCardText).eql('0'); +}).before(async () => createWidget('dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { + pageSize: 3, + pageIndex: 0, + }, + height: 700, +})); + +test('Should do nothing if pagedown pressed on last page', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(2); + const firstCardValue = await firstCard.getFieldValueCell('Id').textContent; + + await t.expect(firstCardValue).eql('8'); + + await t + .click(firstCard.element) + .pressKey('pagedown'); + + const targetCardText = await cardView.getCard(0).getFieldValueCell('Id').textContent; + const focusState = [ + await cardView.getCard(0).element.focused, + await cardView.getCard(1).element.focused, + await cardView.getCard(2).element.focused, + ]; + + await t.expect(focusState).eql([false, false, true]); + await t.expect(targetCardText).eql('6'); +}).before(async () => createWidget('dxCardView', { + dataSource: new Array(9).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + paging: { + pageSize: 3, + pageIndex: 2, + }, + height: 700, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/header.functional.ts b/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/header.functional.ts new file mode 100644 index 000000000000..5a77877b90d7 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/header.functional.ts @@ -0,0 +1,236 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +// NOTE: TestCafe cannot trigger "pressKey" on some specific element +// It triggers this event on document level and process keyboard navigation action immediately. +// Therefore, it's impossible to test switching focus between areas in TestCafe +// Because this feature works with event bubbling + inert attribute +fixture.disablePageReloads`KeyboardNavigation.Header` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('Should navigate between items by arrows', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const headerPanel = cardView.getHeaderPanel(); + + await t + .click(headerPanel.getHeaderItem(0).element) + .pressKey('right right'); + + const focusState = [ + await headerPanel.getHeaderItem(0).element.focused, + await headerPanel.getHeaderItem(1).element.focused, + await headerPanel.getHeaderItem(2).element.focused, + ]; + + await t.expect(focusState).eql([false, false, true]); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, A: 'A_0', B: 'B_0', C: 'C_0', + }, + ], + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, +})); + +test('Should focus item by click', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const headerPanel = cardView.getHeaderPanel(); + + await t.click(headerPanel.getHeaderItem(1).element); + + const focusState = [ + await headerPanel.getHeaderItem(0).element.focused, + await headerPanel.getHeaderItem(1).element.focused, + await headerPanel.getHeaderItem(2).element.focused, + ]; + + await t.expect(focusState).eql([false, true, false]); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, A: 'A_0', B: 'B_0', C: 'C_0', + }, + ], + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, +})); + +test('Should continue arrow navigation from last focused item', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const headerPanel = cardView.getHeaderPanel(); + + await t + .click(headerPanel.getHeaderItem(1).element) + .pressKey('right'); + + const focusState = [ + await headerPanel.getHeaderItem(0).element.focused, + await headerPanel.getHeaderItem(1).element.focused, + await headerPanel.getHeaderItem(2).element.focused, + ]; + + await t.expect(focusState).eql([false, false, true]); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, A: 'A_0', B: 'B_0', C: 'C_0', + }, + ], + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, +})); + +test('Should enable sorting by Enter', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await t + .pressKey('tab tab tab enter'); + + const texts = [ + await cardView.getCard(0).getFieldValueCell('Id').textContent, + await cardView.getCard(1).getFieldValueCell('Id').textContent, + await cardView.getCard(2).getFieldValueCell('Id').textContent, + await cardView.getCard(3).getFieldValueCell('Id').textContent, + ]; + + await t.expect(texts).eql(['0', '1', '2', '3']); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { id: 1 }, + { id: 0 }, + { id: 3 }, + { id: 2 }, + ], + columns: ['id'], + keyExpr: 'id', + height: 700, +})); + +test('Should switch sorting by Enter', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await t + .pressKey('tab tab tab enter enter'); + + const texts = [ + await cardView.getCard(0).getFieldValueCell('Id').textContent, + await cardView.getCard(1).getFieldValueCell('Id').textContent, + await cardView.getCard(2).getFieldValueCell('Id').textContent, + await cardView.getCard(3).getFieldValueCell('Id').textContent, + ]; + + await t.expect(texts).eql(['3', '2', '1', '0']); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { id: 1 }, + { id: 0 }, + { id: 3 }, + { id: 2 }, + ], + columns: ['id'], + keyExpr: 'id', + height: 700, +})); + +test('Should clear sorting by ctrl+Enter', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await t + .pressKey('tab tab tab enter'); + + const sortedTexts = [ + await cardView.getCard(0).getFieldValueCell('Id').textContent, + await cardView.getCard(1).getFieldValueCell('Id').textContent, + await cardView.getCard(2).getFieldValueCell('Id').textContent, + await cardView.getCard(3).getFieldValueCell('Id').textContent, + ]; + + await t.expect(sortedTexts).eql(['0', '1', '2', '3']); + + await t + .pressKey('ctrl+Enter'); + + const unsortedTexts = [ + await cardView.getCard(0).getFieldValueCell('Id').textContent, + await cardView.getCard(1).getFieldValueCell('Id').textContent, + await cardView.getCard(2).getFieldValueCell('Id').textContent, + await cardView.getCard(3).getFieldValueCell('Id').textContent, + ]; + + await t.expect(unsortedTexts).eql(['1', '0', '3', '2']); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { id: 1 }, + { id: 0 }, + { id: 3 }, + { id: 2 }, + ], + columns: ['id'], + keyExpr: 'id', + height: 700, +})); + +test('Should enable multi field sorting by shift+Enter', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await t + .pressKey('tab tab tab right shift+enter left shift+enter'); + + const aTexts = [ + await cardView.getCard(0).getFieldValueCell('A').textContent, + await cardView.getCard(1).getFieldValueCell('A').textContent, + await cardView.getCard(2).getFieldValueCell('A').textContent, + await cardView.getCard(3).getFieldValueCell('A').textContent, + ]; + + const idTexts = [ + await cardView.getCard(0).getFieldValueCell('Id').textContent, + await cardView.getCard(1).getFieldValueCell('Id').textContent, + await cardView.getCard(2).getFieldValueCell('Id').textContent, + await cardView.getCard(3).getFieldValueCell('Id').textContent, + ]; + + await t.expect(aTexts).eql(['0', '0', '1', '1']); + await t.expect(idTexts).eql(['2', '3', '0', '1']); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { id: 1, A: 1 }, + { id: 0, A: 1 }, + { id: 3, A: 0 }, + { id: 2, A: 0 }, + ], + columns: ['id', 'A'], + keyExpr: 'id', + sorting: { + mode: 'multiple', + }, + height: 700, +})); + +test('Should open header filter by alt+ArrowDown', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await t + .pressKey('tab tab tab alt+down'); + + const popup = cardView.getHeaderFilterPopup(); + + await t.expect(popup.element.exists).ok(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { id: 1 }, + { id: 0 }, + { id: 3 }, + { id: 2 }, + ], + columns: ['id'], + keyExpr: 'id', + height: 700, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/search.functional.ts b/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/search.functional.ts new file mode 100644 index 000000000000..49ae70087348 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/search.functional.ts @@ -0,0 +1,51 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`KeyboardNavigation.Selection` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('Should focus search text box after ctrl+f if card is focused', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(1); + + await t + .click(firstCard.element) + .dispatchEvent(firstCard.element, 'keydown', { key: 'f', ctrlKey: true }); + + const toolbar = cardView.getToolbar(); + const isFocused = await toolbar.getSearchTextBox()?.isFocused; + + await t.expect(isFocused).ok(); +}).before(async () => createWidget('dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + searchPanel: { + visible: true, + }, + height: 700, +})); + +test('Should do nothing after ctrl+f if card is not focused', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const toolbar = cardView.getToolbar(); + + await t + .click(toolbar.element) + .dispatchEvent(toolbar.element, 'keydown', { key: 'f', ctrlKey: true }); + + const isFocused = await toolbar.getSearchTextBox()?.isFocused; + + await t.expect(isFocused).notOk(); +}).before(async () => createWidget('dxCardView', { + dataSource: new Array(6).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + searchPanel: { + visible: true, + }, + height: 700, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/selection.functional.ts b/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/selection.functional.ts new file mode 100644 index 000000000000..c9480576e7dc --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/keyboardNavigation/selection.functional.ts @@ -0,0 +1,119 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`KeyboardNavigation.Selection` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +[ + { caseName: 'card selection', keys: 'space', result: [false, true, false] }, + { caseName: 'card cannot be deselected', keys: 'space space', result: [false, true, false] }, + { caseName: 'the next card selection', keys: 'space right space', result: [false, false, true] }, +].forEach(({ caseName, keys, result }) => { + test(`Should handle selection in single mode: ${caseName}`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(1); + + await t + .click(firstCard.element) + .pressKey(keys); + + const selectionState = [ + await cardView.getCard(0).isSelected, + await cardView.getCard(1).isSelected, + await cardView.getCard(2).isSelected, + ]; + + await t.expect(selectionState).eql(result); + }).before(async () => createWidget('dxCardView', { + dataSource: new Array(3).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + selection: { + mode: 'single', + }, + height: 700, + })); +}); + +[ + { caseName: 'card selection', keys: 'space', result: [false, true, false] }, + { caseName: 'card deselection', keys: 'space space', result: [false, false, false] }, + { caseName: 'the next card selection', keys: 'space right space', result: [false, true, true] }, + { caseName: 'range selection', keys: 'left space right right shift+space', result: [true, true, true] }, +].forEach(({ caseName, keys, result }) => { + test(`Should handle selection in multiple mode: ${caseName}`, async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(1); + + await t + .click(firstCard.element) + .pressKey(keys); + + const selectionState = [ + await cardView.getCard(0).isSelected, + await cardView.getCard(1).isSelected, + await cardView.getCard(2).isSelected, + ]; + + await t.expect(selectionState).eql(result); + }).before(async () => createWidget('dxCardView', { + dataSource: new Array(3).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + selection: { + mode: 'multiple', + }, + height: 700, + })); +}); + +test('Should do nothing after ctrl+a with selection single mode', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(1); + + await t + .dispatchEvent(firstCard.element, 'keydown', { key: 'a', ctrlKey: true }); + + const selectionState = [ + await cardView.getCard(0).isSelected, + await cardView.getCard(1).isSelected, + await cardView.getCard(2).isSelected, + ]; + + await t.expect(selectionState).eql([false, false, false]); +}).before(async () => createWidget('dxCardView', { + dataSource: new Array(3).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + selection: { + mode: 'single', + }, + height: 700, +})); + +test('Should select all cards after ctrl+a with selection multiple mode', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(1); + + await t + .dispatchEvent(firstCard.element, 'keydown', { key: 'a', ctrlKey: true }); + + const selectionState = [ + await cardView.getCard(0).isSelected, + await cardView.getCard(1).isSelected, + await cardView.getCard(2).isSelected, + ]; + + await t.expect(selectionState).eql([true, true, true]); +}).before(async () => createWidget('dxCardView', { + dataSource: new Array(3).fill(undefined).map((_, idx) => ({ id: idx })), + columns: ['id'], + keyExpr: 'id', + selection: { + mode: 'multiple', + }, + height: 700, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/noData.visual.ts b/e2e/testcafe-devextreme/tests/cardView/noData.visual.ts new file mode 100644 index 000000000000..53de9dd68831 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/noData.visual.ts @@ -0,0 +1,24 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../helpers/getPageUrl'; +import { createWidget } from '../../helpers/createWidget'; +import { testScreenshot } from '../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - HeaderPanel` + .page(url(__dirname, '../container.html')); + +test('default render', async (t) => { + const cardView = new CardView('#container'); + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + await testScreenshot(t, takeScreenshot, 'content-no-data.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + width: 1000, + height: 600, + columns: ['Customer', 'Order Date'], + dataSource: [], +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/pager.ts b/e2e/testcafe-devextreme/tests/cardView/pager.ts new file mode 100644 index 000000000000..60d66f0527f0 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/pager.ts @@ -0,0 +1,57 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../helpers/getPageUrl'; +import { createWidget } from '../../helpers/createWidget'; + +async function createCardViewWithPager(): Promise { + const dataSource = Array.from({ length: 20 }, (_, i) => ({ text: i.toString(), value: i })); + return createWidget('dxCardView', { + dataSource, + + // TODO: resolve the situation when colums are not set in config + // There is the 'Maximum call stack size exceeded' error in this case + columns: [ + 'text', + 'value', + ], + paging: { + pageSize: 2, + pageIndex: 5, + }, + pager: { + showPageSizeSelector: true, + allowedPageSizes: [2, 3, 4], + showInfo: true, + showNavigationButtons: true, + }, + }); +} +fixture.disablePageReloads`Pager` + .page(url(__dirname, '../container.html')); + +test('Page index interaction', async (t) => { + const cardView = new CardView('#container'); + const pager = cardView.getPager(); + await t + .expect(pager.getPageSize(0).selected) + .ok('page size 2 selected') + .expect(pager.getNavPage('6').selected) + .ok('page 6 selected') + .expect(pager.getInfoText().textContent) + .eql('Page 6 of 10 (20 items)') + .expect(cardView.getCard(1).getFieldValueCell('Text').innerText) + .eql('11'); + + // set page index 7 + await t + .click(pager.getNavPage('7').element) + .expect(cardView.getCard(1).getFieldValueCell('Text').innerText) + .eql('13') + .expect(pager.getInfoText().textContent) + .eql('Page 7 of 10 (20 items)'); + + // navigate to prev page (6) + await t + .click(pager.getPrevNavButton().element) + .expect(pager.getInfoText().textContent) + .eql('Page 6 of 10 (20 items)'); +}).before(async () => createCardViewWithPager()); diff --git a/e2e/testcafe-devextreme/tests/cardView/search/a11y.functional.ts b/e2e/testcafe-devextreme/tests/cardView/search/a11y.functional.ts new file mode 100644 index 000000000000..bbf433e67bcd --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/search/a11y.functional.ts @@ -0,0 +1,18 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - Search.A11y.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('Search field should have aria-label attribute', async (t) => { + const cardView = new CardView(CARD_VIEW_SELECTOR); + const searchBox = cardView.getSearchBox(); + + await t + .expect(searchBox.getInput().getAttribute('aria-label')) + .eql('Search in the card view'); +}).before(async () => createWidget('dxCardView', baseConfig)); diff --git a/e2e/testcafe-devextreme/tests/cardView/search/api.functional.ts b/e2e/testcafe-devextreme/tests/cardView/search/api.functional.ts new file mode 100644 index 000000000000..702b766621b5 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/search/api.functional.ts @@ -0,0 +1,208 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - SearchPanel API` + .page(url(__dirname, '../../container.html')); + +test('searchPanel.visible API', async (t) => { + const cardView = new CardView('#container'); + const searchBox = cardView.getSearchBox(); + + await t + .expect(searchBox.element.exists) + .ok(); + + await cardView.apiOption('searchPanel.visible', false); + + await t + .expect(searchBox.element.exists) + .notOk(); + + await cardView.apiOption('searchPanel.visible', true); + + await t + .expect(searchBox.element.exists) + .ok(); +}).before(async () => { + await createWidget('dxCardView', baseConfig); +}); + +test('searchPanel.width API', async (t) => { + const cardView = new CardView('#container'); + const searchBox = cardView.getSearchBox(); + + await t + .expect(searchBox.element.getBoundingClientRectProperty('width')) + .eql(300); + + await cardView.apiOption('searchPanel.width', 200); + + await t + .expect(searchBox.element.getBoundingClientRectProperty('width')) + .eql(200); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + searchPanel: { + ...baseConfig.searchPanel, + width: 300, + }, + }); +}); + +test('searchPanel.placeholder API', async (t) => { + const cardView = new CardView('#container'); + const searchBox = cardView.getSearchBox(); + + await t + .expect(searchBox.getInput().getAttribute('placeholder')) + .eql('Test placeholder'); + + await cardView.apiOption('searchPanel.placeholder', 'Test placeholder 2'); + + await t + .expect(searchBox.getInput().getAttribute('placeholder')) + .eql('Test placeholder 2'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + searchPanel: { + ...baseConfig.searchPanel, + placeholder: 'Test placeholder', + }, + }); +}); + +// TODO: unskip when update text property state issue is resolved +test.skip('searchPanel.text API', async (t) => { + const cardView = new CardView('#container'); + const searchBox = cardView.getSearchBox(); + + await t + .expect(searchBox.getInput().getAttribute('value')) + .eql('rt'); + + await cardView.apiOption('searchPanel.text', ''); + + await t + .expect(searchBox.getInput().getAttribute('value')) + .eql(''); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + searchPanel: { + ...baseConfig.searchPanel, + text: 'rt', + }, + }); +}); + +test('searchPanel.searchVisibleColumnsOnly API', async (t) => { + const cardView = new CardView('#container'); + const searchInput = cardView.getSearchBox().getInput(); + + await t + .expect(cardView.getCards().count) + .eql(4) + .typeText(searchInput, '2') + .expect(cardView.getCards().count) + .eql(1); + + await cardView.apiOption('searchPanel.searchVisibleColumnsOnly', true); + + await t + .expect(cardView.getCards().count) + .eql(0); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + columns: [ + { + dataField: 'id', + visible: false, + }, + { + dataField: 'title', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); +}); + +test('searchPanel.highlightSearchText API', async (t) => { + const cardView = new CardView('#container'); + const searchInput = cardView.getSearchBox().getInput(); + + await t + .expect(cardView.getCards().count) + .eql(4) + .typeText(searchInput, 'rt') + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getHighlightedTexts().count) + .eql(1) + .expect(cardView.getCard(0).getHighlightedTexts().nth(0).innerText) + .eql('rt') + .expect(cardView.getCard(0).getFieldValueCell('Last Name').innerText) + .eql('Heart'); + + await cardView.apiOption('searchPanel.highlightSearchText', false); + + await t + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getHighlightedTexts().count) + .eql(0); +}).before(async () => { + await createWidget('dxCardView', baseConfig); +}); + +test('searchPanel.highlightCaseSensitive API', async (t) => { + const cardView = new CardView('#container'); + const searchInput = cardView.getSearchBox().getInput(); + + await t + .expect(cardView.getCards().count) + .eql(4) + .typeText(searchInput, 'rt') + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getHighlightedTexts().count) + .eql(1) + .expect(cardView.getCard(0).getHighlightedTexts().nth(0).innerText) + .eql('rt') + .expect(cardView.getCard(0).getFieldValueCell('Last Name').innerText) + .eql('Heart'); + + await t + .typeText(searchInput, 'RT', { replace: true }) + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getHighlightedTexts().count) + .eql(0); + + await cardView.apiOption('searchPanel.highlightCaseSensitive', false); + + await t + .expect(cardView.getCard(0).getHighlightedTexts().count) + .eql(1) + .expect(cardView.getCard(0).getHighlightedTexts().nth(0).innerText) + .eql('rt') + .expect(cardView.getCard(0).getFieldValueCell('Last Name').innerText) + .eql('Heart'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + searchPanel: { + ...baseConfig.searchPanel, + highlightCaseSensitive: true, + }, + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/search/behavior.functional.ts b/e2e/testcafe-devextreme/tests/cardView/search/behavior.functional.ts new file mode 100644 index 000000000000..97588dd65b5d --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/search/behavior.functional.ts @@ -0,0 +1,61 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { baseConfig } from './helpers/baseConfig'; + +fixture.disablePageReloads`CardView - SearchPanel API` + .page(url(__dirname, '../../container.html')); + +test('Search panel should filter cards', async (t) => { + const cardView = new CardView('#container'); + const searchInput = cardView.getSearchBox().getInput(); + + await t + .expect(cardView.getCards().count) + .eql(4) + .typeText(searchInput, 'rt') + .expect(cardView.getCards().count) + .eql(2) + .pressKey('ctrl+a backspace') + .expect(cardView.getCards().count) + .eql(4); +}).before(async () => { + await createWidget('dxCardView', baseConfig); +}); + +test('Search panel should should take into account calculateFilterExpression', async (t) => { + const cardView = new CardView('#container'); + const searchInput = cardView.getSearchBox().getInput(); + + await t + .expect(cardView.getCards().count) + .eql(4) + .typeText(searchInput, '1') + .expect(cardView.getCards().count) + .eql(2) + .expect(cardView.getCard(0).getFieldValueCell('Last Name').innerText) + .eql('Reagan') + .expect(cardView.getCard(1).getFieldValueCell('Last Name').innerText) + .eql('Sims'); +}).before(async () => { + await createWidget('dxCardView', { + ...baseConfig, + columns: [ + { + dataField: 'id', + calculateFilterExpression() { + return [this.dataField, '>', '2']; + }, + }, + { + dataField: 'title', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (fluent-blue-light).png new file mode 100644 index 000000000000..d3ef00d19d50 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (generic-light).png new file mode 100644 index 000000000000..7fa912938ad0 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (material-blue-light).png new file mode 100644 index 000000000000..648558d7d930 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/search/etalons/card-view_search_text-highlighting (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/search/helpers/baseConfig.ts b/e2e/testcafe-devextreme/tests/cardView/search/helpers/baseConfig.ts new file mode 100644 index 000000000000..b811ad1f6244 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/search/helpers/baseConfig.ts @@ -0,0 +1,22 @@ +import { data } from '../../helpers/simpleArrayData'; + +export const baseConfig = { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + searchPanel: { + visible: true, + }, +}; diff --git a/e2e/testcafe-devextreme/tests/cardView/search/visual.ts b/e2e/testcafe-devextreme/tests/cardView/search/visual.ts new file mode 100644 index 000000000000..1e62e873f6c8 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/search/visual.ts @@ -0,0 +1,64 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +fixture`Search.Visual` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +const COLUMNS = ['id', 'firstName', 'lastName', 'email', 'gender']; + +const DATA = [{ + id: 1, + firstName: 'Darin', + lastName: 'Heritege', + email: 'dheritege0@jugem.jp', + gender: 'Male', +}, { + id: 2, + firstName: 'Aeriel', + lastName: 'Giggs', + email: 'agiggs1@hubpages.com', + gender: 'Female', +}, { + id: 3, + firstName: 'Theo', + lastName: 'Aleksidze', + email: 'taleksidze2@patch.com', + gender: 'Female', +}, { + id: 4, + firstName: 'Dalli', + lastName: 'Ashwood', + email: 'dashwood3@buzzfeed.com', + gender: 'Male', +}, { + id: 5, + firstName: 'Paule', + lastName: 'Pidgeley', + email: 'ppidgeley4@upenn.edu', + gender: 'Female', +}]; + +test('highlighted search text', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await testScreenshot(t, takeScreenshot, 'card-view_search_text-highlighting.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: DATA, + columns: COLUMNS, + searchPanel: { + visible: true, + text: 'rt', + }, + height: 600, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png new file mode 100644 index 000000000000..a5a0fe5d7c70 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png new file mode 100644 index 000000000000..ab2008b86bc6 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png new file mode 100644 index 000000000000..68c653fa7ff9 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png new file mode 100644 index 000000000000..450cad589f72 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png new file mode 100644 index 000000000000..2d985a469fbe Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png new file mode 100644 index 000000000000..dedae416f5d2 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (fluent-blue-light).png new file mode 100644 index 000000000000..2b3b141853aa Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (generic-light).png new file mode 100644 index 000000000000..2d624cf69e07 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (material-blue-light).png new file mode 100644 index 000000000000..1ee801c525c0 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_always (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (fluent-blue-light).png new file mode 100644 index 000000000000..de752ae591f4 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (generic-light).png new file mode 100644 index 000000000000..6dfcf6c37d36 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (material-blue-light).png new file mode 100644 index 000000000000..62e66badae8f Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_none (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (fluent-blue-light).png new file mode 100644 index 000000000000..d549727dd3fa Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (generic-light).png new file mode 100644 index 000000000000..4e5944ef395f Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (material-blue-light).png new file mode 100644 index 000000000000..6aceb34ec8db Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (fluent-blue-light).png new file mode 100644 index 000000000000..8815097f9a04 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (generic-light).png new file mode 100644 index 000000000000..b5c559469ca0 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (material-blue-light).png new file mode 100644 index 000000000000..6f6e2a478d12 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (fluent-blue-light).png new file mode 100644 index 000000000000..d549727dd3fa Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (generic-light).png new file mode 100644 index 000000000000..4e5944ef395f Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (material-blue-light).png new file mode 100644 index 000000000000..6aceb34ec8db Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (fluent-blue-light).png new file mode 100644 index 000000000000..de752ae591f4 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (generic-light).png new file mode 100644 index 000000000000..6dfcf6c37d36 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (material-blue-light).png new file mode 100644 index 000000000000..62e66badae8f Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (fluent-blue-light).png new file mode 100644 index 000000000000..2b3b141853aa Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (generic-light).png new file mode 100644 index 000000000000..2d624cf69e07 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (material-blue-light).png new file mode 100644 index 000000000000..1ee801c525c0 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (fluent-blue-light).png new file mode 100644 index 000000000000..de752ae591f4 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (generic-light).png new file mode 100644 index 000000000000..6dfcf6c37d36 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (material-blue-light).png new file mode 100644 index 000000000000..62e66badae8f Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3 (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (fluent-blue-light).png new file mode 100644 index 000000000000..3fa48879e468 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (generic-light).png new file mode 100644 index 000000000000..e1b4ace54810 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (material-blue-light).png new file mode 100644 index 000000000000..e82b5054eb36 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_miltiple_selection_without_select-all (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (fluent-blue-light).png new file mode 100644 index 000000000000..f2820890e12b Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (generic-light).png new file mode 100644 index 000000000000..7d5226ba46c5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (material-blue-light).png new file mode 100644 index 000000000000..4714e041399a Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/selection/etalons/card-view_single_selection (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/functional.ts b/e2e/testcafe-devextreme/tests/cardView/selection/functional.ts new file mode 100644 index 000000000000..49a82523f63e --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/selection/functional.ts @@ -0,0 +1,970 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { ClientFunction } from 'testcafe'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; + +fixture.disablePageReloads`Selection.Functional` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('Single mode: select a first card -> select a second card -> deselect a second card', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(0); + const secondCard = cardView.getCard(1); + + // act + await t.click(firstCard.element); + + // assert + await t.expect(firstCard.isSelected).ok(); + + // act + await t.click(secondCard.element); + + // assert + await t + .expect(firstCard.isSelected) + .notOk() + .expect(secondCard.isSelected) + .ok(); + + // act + await t.click(secondCard.element, { modifiers: { ctrl: true } }); + + // assert + await t + .expect(firstCard.isSelected) + .notOk() + .expect(secondCard.isSelected) + .notOk(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'single', + }, +})); + +test('Multiple mode with showCheckBoxesMode=\'always\': select a first card -> select a second card -> deselect a first card -> deselect a second card', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(0); + const firstSelectCheckbox = firstCard.getSelectCheckbox(); + const secondCard = cardView.getCard(1); + const secondSelectCheckbox = secondCard.getSelectCheckbox(); + + // act + await t.click(firstSelectCheckbox); + + // assert + await t.expect(firstCard.isSelected).ok(); + + // act + await t.click(secondSelectCheckbox); + + // assert + await t + .expect(firstCard.isSelected) + .ok() + .expect(secondCard.isSelected) + .ok(); + + // act + await t.click(firstSelectCheckbox); + + // assert + await t + .expect(firstCard.isSelected) + .notOk() + .expect(secondCard.isSelected) + .ok(); + + // act + await t.click(secondSelectCheckbox); + + // assert + await t + .expect(firstCard.isSelected) + .notOk() + .expect(secondCard.isSelected) + .notOk(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + allowSelectAll: true, + }, +})); + +test('Multiple mode with showCheckBoxesMode=\'always\': select a several cards with shift -> unselect a several cards with shift', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(0); + const secondCard = cardView.getCard(1); + const thirdCard = cardView.getCard(2); + const firstSelectCheckbox = firstCard.getSelectCheckbox(); + const thirdSelectCheckbox = thirdCard.getSelectCheckbox(); + + // act + await t.click(firstSelectCheckbox); + + // assert + await t.expect(firstCard.isSelected).ok(); + + // act + await t.click(thirdSelectCheckbox, { modifiers: { shift: true } }); + + // assert + await t + .expect(firstCard.isSelected) + .ok() + .expect(secondCard.isSelected) + .ok() + .expect(thirdCard.isSelected) + .ok(); + + // act + await t.click(firstSelectCheckbox, { modifiers: { shift: true } }); + + // assert + await t + .expect(firstCard.isSelected) + .ok() + .expect(secondCard.isSelected) + .notOk() + .expect(thirdCard.isSelected) + .notOk(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + allowSelectAll: true, + }, +})); + +test('Multiple mode with showCheckBoxesMode=\'onClick\': select a first card by clicking a checkbox -> deselect a first card by clicking a checkbox', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(0); + const firstSelectCheckbox = firstCard.getSelectCheckbox(); + const firstSelectCheckboxItemContent = firstCard.getToolbarItemContent(0); + + // act + await t.hover(firstSelectCheckboxItemContent); + + // assert + await t.expect(firstSelectCheckbox.visible).ok(); + + // act + await t.click(firstSelectCheckbox); + + // assert + await t + .expect(firstCard.isSelected) + .ok() + .expect(cardView.isCheckBoxesHidden()) + .notOk(); + + // act + await t.click(firstSelectCheckbox); + + // assert + await t + .expect(firstCard.isSelected) + .notOk() + .expect(cardView.isCheckBoxesHidden()) + .ok(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + allowSelectAll: true, + }, +})); + +test('Multiple mode with showCheckBoxesMode=\'onClick\': select a first card by clicking a card -> deselect a first card by clicking a card', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(0); + + // act + await t.click(firstCard.element); + + // assert + await t + .expect(firstCard.isSelected) + .ok() + .expect(cardView.isCheckBoxesHidden()) + .ok(); + + // act + await t.click(firstCard.element, { modifiers: { ctrl: true } }); + + // assert + await t + .expect(firstCard.isSelected) + .notOk() + .expect(cardView.isCheckBoxesHidden()) + .ok(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + allowSelectAll: true, + }, +})); + +test('Multiple mode with showCheckBoxesMode=\'onClick\': select a first card -> select a second card (first card selection state is reset) -> select a first card with ctrl', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(0); + const secondCard = cardView.getCard(1); + + // act + await t.click(firstCard.element); + + // assert + await t + .expect(firstCard.isSelected) + .ok() + .expect(cardView.isCheckBoxesHidden()) + .ok(); + + // act + await t.click(secondCard.element); + + // assert + await t + .expect(firstCard.isSelected) + .notOk() + .expect(secondCard.isSelected) + .ok() + .expect(cardView.isCheckBoxesHidden()) + .ok(); + + // act + await t.click(firstCard.element, { modifiers: { ctrl: true } }); + + // assert + await t + .expect(firstCard.isSelected) + .ok() + .expect(secondCard.isSelected) + .ok() + .expect(cardView.isCheckBoxesHidden()) + .notOk(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + allowSelectAll: true, + }, +})); + +test('Multiple mode with showCheckBoxesMode=\'onClick\': select a first card by card hold -> deselect a first card by card hold', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(0); + + // act + await ClientFunction((card) => { + $(card()).trigger('dxhold'); + })(firstCard.element); + + // assert + await t + .expect(firstCard.isSelected) + .ok() + .expect(cardView.isCheckBoxesHidden()) + .notOk(); + + // act + await ClientFunction((card) => { + $(card()).trigger('dxhold'); + })(firstCard.element); + + // assert + await t + .expect(firstCard.isSelected) + .notOk() + .expect(cardView.isCheckBoxesHidden()) + .ok(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + allowSelectAll: true, + }, +})); + +test('Multiple mode with showCheckBoxesMode=\'onLongTap\': select a several cards', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(0); + const secondCard = cardView.getCard(1); + + // act + await ClientFunction((card) => { + $(card()).trigger('dxhold'); + })(firstCard.element); + + // assert + await t + .expect(firstCard.isSelected) + .notOk() + .expect(cardView.isCheckBoxesHidden()) + .notOk(); + + // act + await t.click(firstCard.element); + + // assert + await t + .expect(firstCard.isSelected) + .ok(); + + await t.click(secondCard.element); + + // assert + await t + .expect(secondCard.isSelected) + .ok(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onLongTap', + allowSelectAll: true, + }, +})); + +test('Select all when selectAllMode = \'allPages\'', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const toolbar = cardView.getToolbar(); + const selectAllButton = toolbar.getSelectAllButton(); + + // act + await t.click(selectAllButton); + + // assert + await t + .expect(toolbar.isSelectAllButtonDisabled()) + .ok() + .expect(toolbar.isClearSelectionButtonDisabled()) + .notOk() + .expect(cardView.getSelectedCardKeys()) + .eql([0, 1, 2, 3, 4]); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + allowSelectAll: true, + selectAllMode: 'allPages', + }, +})); + +test('Deselect all when selectAllMode = \'allPages\'', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const toolbar = cardView.getToolbar(); + const clearSelectionButton = toolbar.getClearSelectionButton(); + + // assert + await t + .expect(toolbar.isSelectAllButtonDisabled()) + .ok() + .expect(toolbar.isClearSelectionButtonDisabled()) + .notOk(); + + // act + await t.click(clearSelectionButton); + + // assert + await t + .expect(toolbar.isSelectAllButtonDisabled()) + .notOk() + .expect(toolbar.isClearSelectionButtonDisabled()) + .ok() + .expect(cardView.getSelectedCardKeys()) + .eql([]); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selectedCardKeys: [0, 1, 2, 3, 4], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + allowSelectAll: true, + selectAllMode: 'allPages', + }, +})); + +test('Select all when selectAllMode = \'page\'', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const toolbar = cardView.getToolbar(); + const selectAllButton = toolbar.getSelectAllButton(); + + // act + await t.click(selectAllButton); + + // assert + await t + .expect(toolbar.isSelectAllButtonDisabled()) + .ok() + .expect(toolbar.isClearSelectionButtonDisabled()) + .notOk() + .expect(cardView.getSelectedCardKeys()) + .eql([0, 1, 2]); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + paging: { + pageSize: 3, + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + allowSelectAll: true, + selectAllMode: 'page', + }, +})); + +test('Deselect all when selectAllMode = \'page\'', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const toolbar = cardView.getToolbar(); + const clearSelectionButton = toolbar.getClearSelectionButton(); + + // assert + await t + .expect(toolbar.isSelectAllButtonDisabled()) + .ok() + .expect(toolbar.isClearSelectionButtonDisabled()) + .notOk(); + + // act + await t.click(clearSelectionButton); + + // assert + await t + .expect(toolbar.isSelectAllButtonDisabled()) + .notOk() + .expect(toolbar.isClearSelectionButtonDisabled()) + .ok() + .expect(cardView.getSelectedCardKeys()) + .eql([]); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + paging: { + pageSize: 3, + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selectedCardKeys: [0, 1, 2], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + allowSelectAll: true, + selectAllMode: 'page', + }, +})); + +test('The states of the Select All and Clear selection buttons should update correctly after changing the page when selectAllMode = \'allPages\'', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const toolbar = cardView.getToolbar(); + + // assert + await t + .expect(toolbar.isSelectAllButtonDisabled()) + .ok() + .expect(toolbar.isClearSelectionButtonDisabled()) + .notOk(); + + // act + await cardView.apiPageIndex(1); + + // assert + await t + .expect(toolbar.isSelectAllButtonDisabled()) + .ok() + .expect(toolbar.isClearSelectionButtonDisabled()) + .notOk(); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + paging: { + pageSize: 3, + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selectedCardKeys: [0, 1, 2, 3, 4], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + allowSelectAll: true, + selectAllMode: 'allPages', + }, +})); + +test('The states of the Select All and Clear selection buttons should update correctly after changing the page when selectAllMode = \'page\'', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const toolbar = cardView.getToolbar(); + + // assert + await t + .expect(toolbar.isSelectAllButtonDisabled()) + .ok() + .expect(toolbar.isClearSelectionButtonDisabled()) + .notOk(); + + // act + await cardView.apiPageIndex(1); + + // assert + await t + .expect(toolbar.isSelectAllButtonDisabled()) + .notOk() + .expect(toolbar.isClearSelectionButtonDisabled()) + .ok() + .expect(cardView.getSelectedCardKeys()) + .eql([0, 1, 2]); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + paging: { + pageSize: 3, + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selectedCardKeys: [0, 1, 2], + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + allowSelectAll: true, + selectAllMode: 'page', + }, +})); + +test('Switching the showCheckBoxesMode option from onClick to always at runtime should work correctly', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(0); + + // assert + await t + .expect(cardView.isCheckBoxesHidden()) + .ok(); + + // act + await cardView.apiOption('selection.showCheckBoxesMode', 'always'); + + // assert + await t + .expect(cardView.isCheckBoxesHidden()) + .notOk(); + + // act + await t.click(firstCard.element); + + // assert + await t + .expect(firstCard.isSelected) + .notOk() + .expect(cardView.getSelectedCardKeys()) + .eql([]); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + }, +})); + +test('Switching the showCheckBoxesMode option from always to onClick at runtime should work correctly', async (t) => { + // arrange + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(0); + + // assert + await t + .expect(cardView.isCheckBoxesHidden()) + .notOk(); + + // act + await cardView.apiOption('selection.showCheckBoxesMode', 'onClick'); + + // assert + await t + .expect(cardView.isCheckBoxesHidden()) + .ok(); + + // act + await t.click(firstCard.element); + + // assert + await t + .expect(firstCard.isSelected) + .ok() + .expect(cardView.getSelectedCardKeys()) + .eql([0]); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + }, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/selection/visual.ts b/e2e/testcafe-devextreme/tests/cardView/selection/visual.ts new file mode 100644 index 000000000000..4fc8708c2b99 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/selection/visual.ts @@ -0,0 +1,355 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import { ClientFunction } from 'testcafe'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +fixture`Selection.Visual` + .page(url(__dirname, '../../container.html')); + +const CARD_VIEW_SELECTOR = '#container'; + +test('Single mode', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await testScreenshot(t, takeScreenshot, 'card-view_single_selection.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selectedCardKeys: [0], + selection: { + mode: 'single', + }, +})); + +test('Multiple mode with Select All/Deselect All and showCheckBoxesMode = \'none\'', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_none.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'none', + allowSelectAll: true, + }, +})); + +test('Multiple mode with Select All/Deselect All and showCheckBoxesMode = \'always\'', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_always.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'always', + allowSelectAll: true, + }, +})); + +test('Multiple mode with Select All/Deselect All and showCheckBoxesMode = \'onClick\'', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstSelectCheckboxItemContent = cardView + .getCard(0) + .getToolbarItemContent(0); + + await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_1.png', { element: cardView.element }); + + await t.hover(firstSelectCheckboxItemContent); + + await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_2.png', { element: cardView.element }); + + await t.hover(cardView.element); + + await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onClick_3.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + allowSelectAll: true, + }, +})); + +test('Multiple mode with a selected card and showCheckBoxesMode = \'onClick\'', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await testScreenshot(t, takeScreenshot, 'card-view_checkbox_visibility_with_showCheckBoxesMode_=_onClick.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + selectedCardKeys: [0], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + allowSelectAll: true, + }, +})); + +test('Multiple mode with selected cards and showCheckBoxesMode = \'onClick\'', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await testScreenshot(t, takeScreenshot, 'card-view_checkboxes_visibility_with_showCheckBoxesMode_=_onClick.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + selectedCardKeys: [0, 1], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onClick', + allowSelectAll: true, + }, +})); + +test('Multiple mode with Select All/Deselect All and showCheckBoxesMode = \'onLongTap\'', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + const firstCard = cardView.getCard(0); + + await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_1.png', { element: cardView.element }); + + await ClientFunction((card) => { + $(card()).trigger('dxhold'); + })(firstCard.element); + + await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_2.png', { element: cardView.element }); + + await ClientFunction((card) => { + $(card()).trigger('dxhold'); + })(firstCard.element); + + await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_with_showCheckBoxesMode_=_onLongTap_3.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + showCheckBoxesMode: 'onLongTap', + allowSelectAll: true, + }, +})); + +test('Multiple mode without Select All/Deselect All', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView(CARD_VIEW_SELECTOR); + + await testScreenshot(t, takeScreenshot, 'card-view_miltiple_selection_without_select-all.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => createWidget('dxCardView', { + dataSource: [ + { + id: 0, title: 'header1', A: 'A_0', B: 'B_0', C: 'C_0', + }, + { + id: 1, title: 'header2', A: 'A_1', B: 'B_1', C: 'C_1', + }, + { + id: 2, title: 'header3', A: 'A_2', B: 'B_2', C: 'C_2', + }, + { + id: 3, title: 'header4', A: 'A_3', B: 'B_3', C: 'C_3', + }, + { + id: 4, title: 'header5', A: 'A_4', B: 'B_4', C: 'C_4', + }, + ], + cardHeader: { + captionExpr: () => 'title', + }, + columns: ['A', 'B', 'C'], + keyExpr: 'id', + height: 700, + selection: { + mode: 'multiple', + allowSelectAll: false, + }, +})); diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/api.themes.ts b/e2e/testcafe-devextreme/tests/cardView/sorting/api.themes.ts new file mode 100644 index 000000000000..b701101632b7 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/sorting/api.themes.ts @@ -0,0 +1,192 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { data } from '../helpers/simpleArrayData'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - Sorting Behavior` + .page(url(__dirname, '../../container.html')); + +test('Sort index API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_sort_index_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + sortIndex: 1, + }, + { + dataField: 'name', + sortOrder: 'asc', + sortIndex: 0, + }, + { + dataField: 'lastName', + }, + ], + }); +}); + +test('ShowSortIndexes API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_show_sort_indexes_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + sortIndex: 1, + }, + { + dataField: 'name', + sortOrder: 'asc', + sortIndex: 0, + }, + { + dataField: 'lastName', + }, + ], + }); +}); + +test('AllowSorting API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await t + .click(cardView.getHeaders().getHeaderItemByText('Title').element); + + await testScreenshot(t, takeScreenshot, 'cardview_allow_sorting_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + sortIndex: 1, + allowSorting: false, + }, + { + dataField: 'name', + sortOrder: 'asc', + sortIndex: 0, + }, + { + dataField: 'lastName', + }, + ], + }); +}); + +[ + function (rowData) { + return rowData.id % 3; + }, + 'name', +].forEach((calculateSortValue) => { + test('CalculateSortValue API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, `cardview_calculate_sort_value_is_${calculateSortValue === 'name' ? 'filed' : 'function'}_api.png`, { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'asc', + calculateSortValue, + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); + }); +}); + +test('SortingMethod API', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_sorting_method_api.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + showSortIndexes: false, + }, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'asc', + sortingMethod(value1, value2) { + if (value1 === 'Mr.' && value2 !== 'Mr.') return 1; + if (value1 !== 'Mr.' && value2 === 'Mr.') return -1; + return value1.localeCompare(value2); + }, + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/bahavior.themes.ts b/e2e/testcafe-devextreme/tests/cardView/sorting/bahavior.themes.ts new file mode 100644 index 000000000000..1f2b1f28cfbc --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/sorting/bahavior.themes.ts @@ -0,0 +1,67 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { data } from '../helpers/simpleArrayData'; +import { testScreenshot } from '../../../helpers/themeUtils'; + +fixture.disablePageReloads`CardView - Sorting Behavior - Themes` + .page(url(__dirname, '../../container.html')); + +test('Default render', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + + await testScreenshot(t, takeScreenshot, 'cardview_headers_default_render.png', { element: cardView.element }); + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + }, + { + dataField: 'name', + }, + { + dataField: 'lastName', + }, + ], + }); +}); +test('Default multiple sorting render', async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const cardView = new CardView('#container'); + await testScreenshot(t, takeScreenshot, 'cardview_headers_with_multiple_sorting_render.png', { element: cardView.element }); + + await t + .expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); +}).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + columns: [ + { + dataField: 'id', + }, + { + dataField: 'title', + sortOrder: 'desc', + }, + { + dataField: 'name', + sortOrder: 'asc', + }, + { + dataField: 'lastName', + }, + ], + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/behavior.functional.ts b/e2e/testcafe-devextreme/tests/cardView/sorting/behavior.functional.ts new file mode 100644 index 000000000000..a3c09a607fb2 --- /dev/null +++ b/e2e/testcafe-devextreme/tests/cardView/sorting/behavior.functional.ts @@ -0,0 +1,253 @@ +import CardView from 'devextreme-testcafe-models/cardView'; +import url from '../../../helpers/getPageUrl'; +import { createWidget } from '../../../helpers/createWidget'; +import { data } from '../helpers/simpleArrayData'; + +fixture.disablePageReloads`CardView - Sorting Behavior - Functional` + .page(url(__dirname, '../../container.html')); + +([ + ['none', false, false, false, [undefined, undefined]], + ['none', true, false, false, [undefined, undefined]], + ['none', false, true, false, [undefined, undefined]], + ['none', false, false, true, [undefined, undefined]], + + ['single', false, false, false, ['desc', undefined]], + ['single', true, false, false, ['desc', undefined]], + ['single', false, true, false, [undefined, undefined]], + ['single', false, false, true, [undefined, undefined]], + + ['multiple', false, false, false, ['desc', 0]], + ['multiple', true, false, false, ['desc', 0]], + ['multiple', false, true, false, [undefined, undefined]], + ['multiple', false, false, true, [undefined, undefined]], +] as [ + string, + boolean, + boolean, + boolean, + [ + string | undefined, + number | undefined, + ], +][] +).forEach(([ + mode, + shift, + ctrl, + meta, + [ + titleSortOrder, + titleSortIndex, + ]]) => { + test(`Change sorting of sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async (t) => { + const cardView = new CardView('#container'); + const titleHeaderItem = cardView.getHeaders().getHeaderItemByText('Title'); + + await t + .click(titleHeaderItem.element); + + await t + .click(titleHeaderItem.element, { + modifiers: { + shift, + ctrl, + meta, + }, + }) + .expect(cardView.apiColumnOption('title', 'sortOrder')) + .eql(titleSortOrder) + .expect(cardView.apiColumnOption('title', 'sortIndex')) + .eql(titleSortIndex); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + mode, + }, + columns: [ + { + dataField: 'title', + }, + { + dataField: 'name', + }, + ], + }); + }); +}); + +([ + ['none', false, false, false, [undefined, undefined], [undefined, undefined]], + ['none', true, false, false, [undefined, undefined], [undefined, undefined]], + ['none', false, true, false, [undefined, undefined], [undefined, undefined]], + ['none', false, false, true, [undefined, undefined], [undefined, undefined]], + + ['single', false, false, false, [undefined, undefined], ['asc', undefined]], + ['single', true, false, false, [undefined, undefined], ['asc', undefined]], + ['single', false, true, false, ['asc', undefined], [undefined, undefined]], + ['single', false, false, true, ['asc', undefined], [undefined, undefined]], + + ['multiple', false, false, false, [undefined, undefined], ['asc', 0]], + ['multiple', true, false, false, ['asc', 0], ['asc', 1]], + ['multiple', false, true, false, ['asc', 0], [undefined, undefined]], + ['multiple', false, false, true, ['asc', 0], [undefined, undefined]], +] as [ + string, + boolean, + boolean, + boolean, + [ + string | undefined, + number | undefined, + ], + [ + string | undefined, + number | undefined, + ], +][] +).forEach(([ + mode, + shift, + ctrl, + meta, + [ + titleSortOrder, + titleSortIndex, + ], + [ + nameSortOrder, + nameSortIndex, + ], +]) => { + test(`Change sorting of neighbour non sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async (t) => { + const cardView = new CardView('#container'); + const titleHeaderItem = cardView.getHeaders().getHeaderItemByText('Title'); + const nameHeaderItem = cardView.getHeaders().getHeaderItemByText('Name'); + + await t + .click(titleHeaderItem.element); + + await t + .click(nameHeaderItem.element, { + modifiers: { + shift, + ctrl, + meta, + }, + }) + .expect(cardView.apiColumnOption('title', 'sortOrder')) + .eql(titleSortOrder) + .expect(cardView.apiColumnOption('title', 'sortIndex')) + .eql(titleSortIndex) + .expect(cardView.apiColumnOption('name', 'sortOrder')) + .eql(nameSortOrder) + .expect(cardView.apiColumnOption('name', 'sortIndex')) + .eql(nameSortIndex); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + mode, + }, + columns: [ + { + dataField: 'title', + }, + { + dataField: 'name', + }, + ], + }); + }); +}); + +([ + ['none', false, false, false, [undefined, undefined], [undefined, undefined]], + ['none', true, false, false, [undefined, undefined], [undefined, undefined]], + ['none', false, true, false, [undefined, undefined], [undefined, undefined]], + ['none', false, false, true, [undefined, undefined], [undefined, undefined]], + + ['single', false, false, false, [undefined, undefined], ['desc', undefined]], + ['single', true, false, false, [undefined, undefined], ['desc', undefined]], + ['single', false, true, false, [undefined, undefined], [undefined, undefined]], + ['single', false, false, true, [undefined, undefined], [undefined, undefined]], + + ['multiple', false, false, false, [undefined, undefined], ['desc', 0]], + ['multiple', true, false, false, ['asc', 0], ['desc', 1]], + ['multiple', false, true, false, ['asc', 0], [undefined, undefined]], + ['multiple', false, false, true, ['asc', 0], [undefined, undefined]], +] as [ + string, + boolean, + boolean, + boolean, + [ + string | undefined, + number | undefined, + ], + [ + string | undefined, + number | undefined, + ], +][] +).forEach(([ + mode, + shift, + ctrl, + meta, + [ + titleSortOrder, + titleSortIndex, + ], + [ + nameSortOrder, + nameSortIndex, + ], +]) => { + test(`Change sorting of neighbour sorted item in ${mode} mode with shift=${shift}, ctrl=${ctrl}, meta=${meta}`, async (t) => { + const cardView = new CardView('#container'); + const titleHeaderItem = cardView.getHeaders().getHeaderItemByText('Title'); + const nameHeaderItem = cardView.getHeaders().getHeaderItemByText('Name'); + + await t + .click(titleHeaderItem.element) + .click(nameHeaderItem.element, { + modifiers: { + shift: true, + }, + }); + + await t + .click(nameHeaderItem.element, { + modifiers: { + shift, + ctrl, + meta, + }, + }) + .expect(cardView.apiColumnOption('title', 'sortOrder')) + .eql(titleSortOrder) + .expect(cardView.apiColumnOption('title', 'sortIndex')) + .eql(titleSortIndex) + .expect(cardView.apiColumnOption('name', 'sortOrder')) + .eql(nameSortOrder) + .expect(cardView.apiColumnOption('name', 'sortIndex')) + .eql(nameSortIndex); + }).before(async () => { + await createWidget('dxCardView', { + dataSource: data, + sorting: { + mode, + }, + columns: [ + { + dataField: 'title', + }, + { + dataField: 'name', + }, + ], + }); + }); +}); diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (fluent-blue-light).png new file mode 100644 index 000000000000..d1580e917d5e Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (generic-light).png new file mode 100644 index 000000000000..b6a1127f73e2 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (material-blue-light).png new file mode 100644 index 000000000000..b4db787997ce Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_allow_sorting_api (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (fluent-blue-light).png new file mode 100644 index 000000000000..da1fa3cdbc04 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (generic-light).png new file mode 100644 index 000000000000..3760d50672ee Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (material-blue-light).png new file mode 100644 index 000000000000..bd4b18d48e82 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_filed_api (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (fluent-blue-light).png new file mode 100644 index 000000000000..631b6fc1b8fa Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (generic-light).png new file mode 100644 index 000000000000..d85e18cbc81d Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (material-blue-light).png new file mode 100644 index 000000000000..cdd303a91277 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_calculate_sort_value_is_function_api (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (fluent-blue-light).png new file mode 100644 index 000000000000..1051af476409 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (generic-light).png new file mode 100644 index 000000000000..eb4bda28f72c Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (material-blue-light).png new file mode 100644 index 000000000000..263bd94d104a Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_default_render (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (fluent-blue-light).png new file mode 100644 index 000000000000..5f32ae2e6cfd Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (generic-light).png new file mode 100644 index 000000000000..6911b99d3b58 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (material-blue-light).png new file mode 100644 index 000000000000..cb42b007d798 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_headers_with_multiple_sorting_render (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (fluent-blue-light).png new file mode 100644 index 000000000000..394362b88157 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (generic-light).png new file mode 100644 index 000000000000..cd489c3fc822 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (material-blue-light).png new file mode 100644 index 000000000000..6841273861a2 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_show_sort_indexes_api (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (fluent-blue-light).png new file mode 100644 index 000000000000..cdff6e7df26d Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (generic-light).png new file mode 100644 index 000000000000..7758202a2f10 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (material-blue-light).png new file mode 100644 index 000000000000..6c1833eb4736 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sort_index_api (material-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (fluent-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (fluent-blue-light).png new file mode 100644 index 000000000000..5aa7f5c5faf5 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (fluent-blue-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (generic-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (generic-light).png new file mode 100644 index 000000000000..29a84bc719cd Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (generic-light).png differ diff --git a/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (material-blue-light).png b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (material-blue-light).png new file mode 100644 index 000000000000..181c8c1f6a89 Binary files /dev/null and b/e2e/testcafe-devextreme/tests/cardView/sorting/etalons/cardview_sorting_method_api (material-blue-light).png differ diff --git a/packages/devextreme-angular/src/server/render.ts b/packages/devextreme-angular/src/server/render.ts index 04773ba08834..3027dd54b1af 100644 --- a/packages/devextreme-angular/src/server/render.ts +++ b/packages/devextreme-angular/src/server/render.ts @@ -37,6 +37,12 @@ export class DxServerModule { container.innerHTML = childString; }, + renderIntoContainer: ( + jsx, + container, + ) => { + container.innerHTML = renderToString(jsx); + }, }); } } diff --git a/packages/devextreme-angular/tests/src/server/component-names.ts b/packages/devextreme-angular/tests/src/server/component-names.ts index d5914606f2ba..baed4ebe863c 100644 --- a/packages/devextreme-angular/tests/src/server/component-names.ts +++ b/packages/devextreme-angular/tests/src/server/component-names.ts @@ -7,6 +7,7 @@ export const componentNames = [ 'bullet', 'button', 'calendar', + 'card-view', 'chart', 'chat', 'check-box', diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss new file mode 100644 index 000000000000..19a36bc1f607 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss @@ -0,0 +1,23 @@ +@use './content_view'; +@use './header_panel'; +@use './filter_panel'; +@use './variables' as *; + +// adduse + +.dx-cardview { + padding: $cardview-padding; + background-color: $cardview-background-color; + border-radius: $cardview-border-radius; +} + +.dx-cardview-root-container { + display: flex; + flex-direction: column; + gap: $cardview-gap; + height: 100%; +} + +.dx-cardview-exclude-flexbox { + position: absolute; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/_variables.scss new file mode 100644 index 000000000000..893760ad1f6e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/_variables.scss @@ -0,0 +1,4 @@ +$cardview-gap: null !default; +$cardview-padding: null !default; +$cardview-background-color: null !default; +$cardview-border-radius: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/_index.scss new file mode 100644 index 000000000000..f8f2a72d84ba --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/_index.scss @@ -0,0 +1,8 @@ +@use '../variables' as *; +@use './content'; +@use './no_data'; + +.dx-gridcore-contentview { + overflow: hidden; + flex-grow: 1; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_index.scss new file mode 100644 index 000000000000..b220687c03b8 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_index.scss @@ -0,0 +1,10 @@ +@use './variables' as *; +@use './card'; + +.dx-cardview-content { + display: grid; + justify-items: center; + column-gap: $cardview-content-column-gap; + row-gap: $cardview-content-row-gap; + grid-template-columns: repeat(var(--dx-cardview-cardsperrow), 1fr); +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_variables.scss new file mode 100644 index 000000000000..19754837130c --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_variables.scss @@ -0,0 +1,2 @@ +$cardview-content-column-gap: null !default; +$cardview-content-row-gap: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_index.scss new file mode 100644 index 000000000000..0f3486021387 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_index.scss @@ -0,0 +1,23 @@ +@use './header'; +@use './content'; +@use './cover'; +@use './variables' as *; + +.dx-cardview-card { + width: 100%; + min-width: var(--dx-cardview-card-min-width, $cardview-card-min-width); + max-width: var(--dx-cardview-card-max-width); + border: $cardview-card-border-size solid $cardview-card-border-color; + border-radius: $cardview-card-border-radius; + background-color: $cardview-card-background-color; + overflow: hidden; + + & > div > :nth-child(2) { + border-top: none; + } +} + + +.dx-cardview-card-selection { + background-color: $cardview-card-selection-background-color; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_variables.scss new file mode 100644 index 000000000000..b4528656fde3 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_variables.scss @@ -0,0 +1,7 @@ +$cardview-card-border-color: null !default; + +$cardview-card-border-size: null !default; +$cardview-card-min-width: null !default; +$cardview-card-border-radius: null !default; +$cardview-card-background-color: null !default; +$cardview-card-selection-background-color: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_index.scss new file mode 100644 index 000000000000..3c43bca9d9c6 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_index.scss @@ -0,0 +1,53 @@ +@use './variables' as *; +@use '../variables' as *; + +.dx-cardview-card-content { + padding: ($cardview-card-content-padding-vertical - $cardview-card-content-field-gap) $cardview-card-content-padding-horizontal; + display: grid; + grid-template-columns: auto 1fr; + border-spacing: $cardview-card-content-field-gap; + width: 100%; + border-top: $cardview-card-border-size solid $cardview-card-border-color; +} + +.dx-cardview-field-template { + grid-column: span 2; +} + +.dx-cardview-field-caption, +.dx-cardview-field-value, +.dx-cardview-field-template { + padding: $cardview-card-content-cell-padding-vertical $cardview-card-content-cell-padding-horizontal; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + .dx-cardview-word-wrap-enabled & { + white-space: normal; + } +} + +.dx-cardview-field-caption { + font-weight: 600; +} + +.dx-cardview-field-value { + &--text-align-right { + text-align: right; + } + + &--text-align-left { + text-align: left; + } + + &--text-align-center { + text-align: center; + } + + &__text-part { + &--highlighted { + color: $cardview-card-content-field-value-highlight-color; + background: $cardview-card-content-field-value-highlight-background; + } + } +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_variables.scss new file mode 100644 index 000000000000..5d44ab4b2455 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_variables.scss @@ -0,0 +1,8 @@ +$cardview-card-content-padding-vertical: null !default; +$cardview-card-content-padding-horizontal: null !default; +$cardview-card-content-field-gap: null !default; +$cardview-card-content-cell-padding-vertical: null !default; +$cardview-card-content-cell-padding-horizontal: null !default; + +$cardview-card-content-field-value-highlight-color: null !default; +$cardview-card-content-field-value-highlight-background: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_index.scss new file mode 100644 index 000000000000..a341cfb8186d --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_index.scss @@ -0,0 +1,28 @@ +@use './variables' as *; +@use '../variables' as *; + +.dx-card-cover { + overflow: hidden; + display: flex; + justify-content: center; + border-top: $cardview-card-border-size solid $cardview-card-border-color; + max-height: var(--dx-cardview-card-cover-max-height); + aspect-ratio: var(--dx-cardview-card-cover-ratio); + width: 100%; + background-color: $cardview-card-cover-background-color; + + &-noimage { + background-color: $cardview-card-cover-noimage-background-color; + align-items: center; + + .dx-icon-imagethumbnail { + font-size: $cardview-card-cover-noimage-icon-size; + color: $cardview-card-cover-noimage-icon-color; + } + } +} + +.dx-card-cover-image { + object-fit: contain; +} + diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_variables.scss new file mode 100644 index 000000000000..9c178428557a --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_variables.scss @@ -0,0 +1,4 @@ +$cardview-card-cover-noimage-background-color: null !default; +$cardview-card-cover-noimage-icon-color: null !default; +$cardview-card-cover-noimage-icon-size: null !default; +$cardview-card-cover-background-color: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_index.scss new file mode 100644 index 000000000000..c1705bbb0df3 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_index.scss @@ -0,0 +1,43 @@ +@use './variables' as *; +@use '../variables' as *; + +.dx-cardview-card-header { + .dx-toolbar { + padding: 0 12px; + border-radius: $cardview-card-header-border-radius; + + .dx-toolbar-label { + font-size: $cardview-card-header-text-size; + } + } +} + +.dx-cardview-card-selection { + .dx-toolbar { + background-color: $cardview-card-selection-background-color; + } +} + +.dx-cardview-select-checkboxes-hidden .dx-cardview-card:not(.dx-cardview-card-selection) .dx-cardview-select-checkbox { + .dx-checkbox { + display: none; + } + + .dx-toolbar-item-content::before { + content: ''; + width: 20px; + height: 20px; + display: inline-block; + pointer-events: none; + } + + .dx-toolbar-item-content:hover { + &::before { + display: none; + } + + .dx-checkbox { + display: inline-block; + } + } +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_variables.scss new file mode 100644 index 000000000000..814471d72bd7 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_variables.scss @@ -0,0 +1,2 @@ +$cardview-card-header-text-size: null !default; +$cardview-card-header-border-radius: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/no_data/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/no_data/_index.scss new file mode 100644 index 000000000000..0f2481435c5e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/no_data/_index.scss @@ -0,0 +1,28 @@ +@use './variables' as *; + +.dx-gridcore-nodata-icon-container { + height: $cardview-nodata-icon-container-size; + width: $cardview-nodata-icon-container-size; + display: flex; + align-items: center; + justify-content: center; + background-color: $cardview-nodata-icon-container-background-color; + border-radius: 999px; + margin: $cardview-nodata-icon-container-margin; + + .dx-icon { + font-size: $cardview-nodata-icon-size; + } +} + +.dx-gridcore-nodata-element { + color: $cardview-nodata-text-color; + text-align: center; +} + +.dx-gridcore-nodata-container { + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/no_data/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/no_data/_variables.scss new file mode 100644 index 000000000000..cd82d27509b7 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/no_data/_variables.scss @@ -0,0 +1,7 @@ +$cardview-nodata-text-color: null !default; + +$cardview-nodata-icon-container-size: null !default; +$cardview-nodata-icon-container-background-color: null !default; +$cardview-nodata-icon-container-margin: null !default; + +$cardview-nodata-icon-size: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/filter_panel/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/filter_panel/_index.scss new file mode 100644 index 000000000000..1482e6b706d4 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/filter_panel/_index.scss @@ -0,0 +1,7 @@ +@use '../variables' as *; + +.dx-cardview { + .dx-datagrid-filter-panel { + background-color: $cardview-background-color; + } +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss new file mode 100644 index 000000000000..7842a1b5e2e2 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss @@ -0,0 +1,12 @@ + +@use './item'; +@use './variables' as *; + +.dx-cardview-headerpanel-content { + display: flex; + gap: $cardview-headerpanel-content-gap; +} + +.dx-cardview-header-item-sorting { + display: flex; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_variables.scss new file mode 100644 index 000000000000..6ab96b987436 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_variables.scss @@ -0,0 +1 @@ +$cardview-headerpanel-content-gap: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_index.scss new file mode 100644 index 000000000000..d4343cf70542 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_index.scss @@ -0,0 +1,39 @@ +@use '../../../icons' as *; +@use './variables' as *; + +.dx-cardview-header-item { + display: flex; + align-items: center; + padding: ($cardview-header-item-padding-vertical - $cardview-header-item-border-width) $cardview-header-item-padding-horizontal; + min-width: fit-content; + user-select: none; + gap: $cardview-header-item-content-gap; + border: solid $cardview-header-item-border-width $cardview-header-item-border-color; + border-radius: $cardview-header-item-border-radius; + background-color: $cardview-header-item-background-color; + line-height: $cardview-header-item-line-height; + cursor: pointer; + + &:hover { + background-color: $cardview-header-item-hovered-background-color; + border: solid $cardview-header-item-border-width $cardview-header-item-hovered-border-color; + } + + .dx-icon-cursorprohibition { + color: $cardview-header-item-prohibition-icon-color; + } + + .dx-icon { + font-size: $cardview-header-item-icon-size; + } +} + +.dx-cardview { + .dx-header-filter-icon { + color: $cardview-header-filter-icon-empty-color; + + &--selected { + color: $cardview-header-filter-icon-selected-color; + } + } +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_variables.scss new file mode 100644 index 000000000000..19d7416779af --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_variables.scss @@ -0,0 +1,18 @@ +$cardview-header-item-background-color: null !default; +$cardview-header-item-border-color: null !default; + +$cardview-header-item-hovered-background-color: null !default; +$cardview-header-item-hovered-border-color: null !default; + +$cardview-header-item-border-width: null !default; +$cardview-header-item-border-radius: null !default; +$cardview-header-item-padding-horizontal: null !default; +$cardview-header-item-padding-vertical: null !default; + +$cardview-header-item-content-gap: 4px !default; +$cardview-header-item-line-height: null !default; +$cardview-header-item-icon-size: null !default; +$cardview-header-filter-icon-empty-color: null !default; +$cardview-header-filter-icon-selected-color: null !default; + +$cardview-header-item-prohibition-icon-color: null; diff --git a/packages/devextreme-scss/scss/widgets/fluent/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/_index.scss index e9d6526025ba..ff95df79b8ec 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/_index.scss @@ -61,3 +61,4 @@ @use "./sortable"; @use "./deferRendering"; @use "./map"; +@use "./cardView"; diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss new file mode 100644 index 000000000000..99d0e33bbb4d --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss @@ -0,0 +1,128 @@ +@use '../colors' as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/cover/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; +@use '../../base/cardView/content_view/no_data/variables' as *; + +// adduse + +$cardview-fluent-background-color: null !default; + +$cardview-fluent-header-filter-icon-empty-color: null !default; +$cardview-fluent-header-filter-icon-selected-color: null !default; + +$cardview-fluent-header-item-border-color: null !default; +$cardview-fluent-header-item-background-color: null !default; +$cardview-fluent-header-item-hovered-background-color: null !default; +$cardview-fluent-header-item-hovered-border-color: null !default; + +$cardview-fluent-header-item-prohibition-icon-color: null !default; + +$cardview-fluent-nodata-text-color: null !default; +$cardview-fluent-nodata-icon-container-background-color: null !default; + +$cardview-fluent-card-content-field-value-highlight-color: null !default; +$cardview-fluent-card-content-field-value-highlight-background: null !default; + +$cardview-fluent-card-cover-background-color: null !default; + +$cardview-fluent-card-cover-noimage-background-color: null !default; +$cardview-fluent-card-cover-noimage-icon-color: null !default; + +$cardview-fluent-card-selection-background-color: null !default; + +$cardview-fluent-card-border-color: null !default; +$cardview-fluent-card-background-color: null !default; + +@if $mode == 'light' { + $cardview-fluent-background-color: $base-typography-bg !default; + + $cardview-fluent-header-filter-icon-empty-color: $base-text-color !default; + $cardview-fluent-header-filter-icon-selected-color: $base-accent !default; + + $cardview-fluent-header-item-border-color: $base-border-color !default; + $cardview-fluent-header-item-background-color: darken($base-bg, 5.88) !default; + $cardview-fluent-header-item-hovered-background-color: darken($cardview-fluent-header-item-background-color, 1.96) !default; + $cardview-fluent-header-item-hovered-border-color: darken($cardview-fluent-header-item-border-color, 13.73) !default; + + $cardview-fluent-header-item-prohibition-icon-color: darken(saturate(adjust-hue($base-danger, -4), 22.80), 9.61) !default; + + $cardview-fluent-nodata-text-color: lighten($base-text-color, 23.92) !default; + $cardview-fluent-nodata-icon-container-background-color: darken($base-bg, 9.80) !default; + + $cardview-fluent-card-content-field-value-highlight-color: $base-inverted-text-color !default; + $cardview-fluent-card-content-field-value-highlight-background: $base-accent !default; + + $cardview-fluent-card-cover-background-color: #FFF !default; + + $cardview-fluent-card-cover-noimage-background-color: darken($base-bg, 5.88) !default; + $cardview-fluent-card-cover-noimage-icon-color: lighten($base-text-color, 23.92) !default; + + $cardview-fluent-card-selection-background-color: lighten(desaturate(adjust-hue($base-accent, 4), 11.38), 55.49) !default; + + $cardview-fluent-card-border-color: $base-border-color !default; + $cardview-fluent-card-background-color: $base-bg !default; +} + +@if $mode == "dark" { + $cardview-fluent-background-color: $base-typography-bg !default; + + $cardview-fluent-header-filter-icon-empty-color: $base-text-color !default; + $cardview-fluent-header-filter-icon-selected-color: $base-accent !default; + + $cardview-fluent-header-item-border-color: $base-border-color !default; + $cardview-fluent-header-item-background-color: darken($base-bg, 12.16) !default; + $cardview-fluent-header-item-hovered-background-color: lighten($cardview-fluent-header-item-background-color, 18.04) !default; + $cardview-fluent-header-item-hovered-border-color: lighten($cardview-fluent-header-item-border-color, 11.76) !default; + + $cardview-fluent-header-item-prohibition-icon-color: darken(desaturate($base-danger, 0.27), 7.45) !default; + + $cardview-fluent-nodata-text-color: darken($base-text-color, 32.16) !default; + $cardview-fluent-nodata-icon-container-background-color: lighten($base-bg, 3.92) !default; + + $cardview-fluent-card-content-field-value-highlight-color: $base-inverted-text-color !default; + $cardview-fluent-card-content-field-value-highlight-background: $base-accent !default; + + $cardview-fluent-card-cover-background-color: #FFF !default; + + $cardview-fluent-card-cover-noimage-background-color: darken($base-bg, 12.16) !default; + $cardview-fluent-card-cover-noimage-icon-color: darken($base-text-color, 32.16) !default; + + $cardview-fluent-card-selection-background-color: darken(desaturate(adjust-hue($base-accent, -4), 14.69), 49.41) !default; + + $cardview-fluent-card-border-color: $base-border-color !default; + $cardview-fluent-card-background-color: $base-bg !default; +} + +$cardview-background-color: $cardview-fluent-background-color !default; + +$cardview-header-filter-icon-empty-color: $cardview-fluent-header-filter-icon-empty-color !default; +$cardview-header-filter-icon-selected-color: $cardview-fluent-header-filter-icon-selected-color !default; + +$cardview-header-item-border-color: $cardview-fluent-header-item-border-color !default; +$cardview-header-item-background-color: $cardview-fluent-header-item-background-color !default; +$cardview-header-item-hovered-background-color: $cardview-fluent-header-item-hovered-background-color !default; +$cardview-header-item-hovered-border-color: $cardview-fluent-header-item-hovered-border-color !default; + +$cardview-header-item-prohibition-icon-color: $cardview-fluent-header-item-prohibition-icon-color !default; + +$cardview-nodata-text-color: $cardview-fluent-nodata-text-color !default; +$cardview-nodata-icon-container-background-color: $cardview-fluent-nodata-icon-container-background-color !default; + +$cardview-card-content-field-value-highlight-color: $cardview-fluent-card-content-field-value-highlight-color !default; +$cardview-card-content-field-value-highlight-background: $cardview-fluent-card-content-field-value-highlight-background !default; + +$cardview-card-cover-background-color: $cardview-fluent-card-cover-background-color !default; + +$cardview-card-cover-noimage-background-color: $cardview-fluent-card-cover-noimage-background-color !default; +$cardview-card-cover-noimage-icon-color: $cardview-fluent-card-cover-noimage-icon-color !default; + +$cardview-card-selection-background-color: $cardview-fluent-card-selection-background-color !default; + +$cardview-card-border-color: $cardview-fluent-card-border-color !default; +$cardview-card-background-color: $cardview-fluent-card-background-color !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_index.scss new file mode 100644 index 000000000000..29f2262bbe3e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_index.scss @@ -0,0 +1,5 @@ +@use 'colors'; +@use 'sizes'; +@use '../../base/cardView/index' as *; + +// adduse diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss new file mode 100644 index 000000000000..186012adba5e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss @@ -0,0 +1,85 @@ +@use "../sizes" as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/cover/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; +@use '../../base/cardView/content_view/no_data/variables' as *; + +// adduse + +$cardview-fluent-sizes-64: null; +$cardview-fluent-sizes-48: null; +$cardview-fluent-sizes-40: null; +$cardview-fluent-paddings-24: null; +$cardview-fluent-paddings-12: null; +$cardview-fluent-paddings-8: null; +$cardview-fluent-paddings-6: null; +$cardview-fluent-text-size-16: null; +$cardview-fluent-border-radius-8: null; +$cardview-fluent-gaps-16: null; +$cardview-fluent-gaps-24: null; + +@if $size == "default" { + $cardview-fluent-sizes-64: 64px; + $cardview-fluent-sizes-48: 48px; + $cardview-fluent-sizes-40: 40px; + $cardview-fluent-paddings-24: 24px; + $cardview-fluent-paddings-12: 12px; + $cardview-fluent-paddings-8: 8px; + $cardview-fluent-paddings-6: 6px; + $cardview-fluent-text-size-16: 16px; + $cardview-fluent-border-radius-8: 8px; + $cardview-fluent-gaps-16: 16px; + $cardview-fluent-gaps-24: 24px; +} +@else if $size == "compact" { + $cardview-fluent-sizes-64: 48px; + $cardview-fluent-sizes-48: 32px; + $cardview-fluent-sizes-40: 32px; + $cardview-fluent-paddings-24: 16px; + $cardview-fluent-paddings-12: 8px; + $cardview-fluent-paddings-8: 6px; + $cardview-fluent-paddings-6: 4px; + $cardview-fluent-text-size-16: 14px; + $cardview-fluent-border-radius-8: 6px; + $cardview-fluent-gaps-16: 12px; + $cardview-fluent-gaps-24: 16px; +} + +$cardview-border-radius: 16px !default; +$cardview-gap: $cardview-fluent-gaps-24 !default; +$cardview-padding: $cardview-fluent-paddings-24 !default; + +$cardview-headerpanel-content-gap: 8px !default; + +$cardview-header-item-border-width: 1px !default; +$cardview-header-item-border-radius: 6px !default; +$cardview-header-item-padding-horizontal: $cardview-fluent-paddings-8 !default; +$cardview-header-item-padding-vertical: $cardview-fluent-paddings-6 !default; +$cardview-header-item-icon-size: $fluent-base-icon-size !default; + +$cardview-nodata-icon-container-size: $cardview-fluent-sizes-64 !default; +$cardview-nodata-icon-container-margin: $cardview-fluent-paddings-12 !default; +$cardview-nodata-icon-size: $cardview-fluent-sizes-40 !default; + +$cardview-content-column-gap: $cardview-fluent-gaps-16 !default; +$cardview-content-row-gap: $cardview-fluent-gaps-16 !default; + +$cardview-card-border-size: 1px !default; +$cardview-card-min-width: 250px !default; +$cardview-card-border-radius: $cardview-fluent-border-radius-8 !default; + +$cardview-card-cover-noimage-icon-size: $cardview-fluent-sizes-48 !default; + +$cardview-card-content-padding-vertical: $cardview-fluent-paddings-12 !default; +$cardview-card-content-padding-horizontal: $cardview-fluent-paddings-12 !default; +$cardview-card-content-field-gap: 5px !default; +$cardview-card-content-cell-padding-vertical: $cardview-fluent-paddings-6 !default; +$cardview-card-content-cell-padding-horizontal: $cardview-fluent-paddings-12 !default; + +$cardview-card-header-text-size: $cardview-fluent-text-size-16 !default; +$cardview-card-header-border-radius: $cardview-fluent-border-radius-8 !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/_index.scss b/packages/devextreme-scss/scss/widgets/generic/_index.scss index aac2ff330556..ff95df79b8ec 100644 --- a/packages/devextreme-scss/scss/widgets/generic/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/_index.scss @@ -61,4 +61,4 @@ @use "./sortable"; @use "./deferRendering"; @use "./map"; - +@use "./cardView"; diff --git a/packages/devextreme-scss/scss/widgets/generic/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/cardView/_colors.scss new file mode 100644 index 000000000000..8a66e283b0f4 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/generic/cardView/_colors.scss @@ -0,0 +1,161 @@ +@use '../colors' as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/cover/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; +@use '../../base/cardView/content_view/no_data/variables' as *; +@use "sass:color"; + +// adduse + +$cardview-generic-background-color: null !default; + +$cardview-generic-header-filter-icon-empty-color: null !default; +$cardview-generic-header-filter-icon-selected-color: null !default; + +$cardview-generic-header-item-border-color: null !default; + +$cardview-generic-header-item-background-color: null !default; +$cardview-generic-header-item-hovered-background-color: null !default; +$cardview-generic-header-item-hovered-border-color: null !default; + +$cardview-generic-header-item-prohibition-icon-color: null !default; + +$cardview-generic-nodata-text-color: null !default; +$cardview-generic-nodata-icon-container-background-color: null !default; + +$cardview-generic-card-content-field-value-highlight-color: null !default; +$cardview-generic-card-content-field-value-highlight-background: null !default; + +$cardview-generic-card-cover-background-color: null !default; + +$cardview-generic-card-cover-noimage-background-color: null !default; +$cardview-generic-card-cover-noimage-icon-color: null !default; + +$cardview-generic-card-selection-background-color: null !default; + +$cardview-generic-card-border-color: null !default; +$cardview-generic-card-background-color: null !default; + +@if $color == 'light' or $color == 'carmine' or $color == 'softblue' or $color == 'greenmist' { + $cardview-generic-background-color: $base-bg !default; + + $cardview-generic-header-filter-icon-empty-color: $base-text-color !default; + $cardview-generic-header-filter-icon-selected-color: $base-accent !default; + + $cardview-generic-header-item-background-color: darken($base-bg, 3.92) !default; + $cardview-generic-header-item-border-color: $base-border-color !default; + $cardview-generic-header-item-hovered-background-color: darken($cardview-generic-header-item-background-color, 3.92) !default; + $cardview-generic-header-item-hovered-border-color: darken($cardview-generic-header-item-hovered-background-color, 10.59) !default; + + $cardview-generic-header-item-prohibition-icon-color: $base-danger !default; + + $cardview-generic-nodata-text-color: $base-label-color !default; + $cardview-generic-nodata-icon-container-background-color: darken($base-bg, 7.84) !default; + + $cardview-generic-card-content-field-value-highlight-color: $base-inverted-text-color !default; + $cardview-generic-card-content-field-value-highlight-background: $base-accent !default; + + $cardview-generic-card-cover-background-color: #FFF !default; + + $cardview-generic-card-cover-noimage-background-color: darken($base-bg, 7.84) !default; + $cardview-generic-card-cover-noimage-icon-color: lighten($base-text-color, 38.43) !default; + + $cardview-generic-card-selection-background-color: lighten(desaturate(adjust-hue($base-accent, 1), 7.32), 43.33) !default; + + $cardview-generic-card-border-color: $base-border-color !default; + $cardview-generic-card-background-color: $base-bg !default; +} + +@if $color == "dark" or $color == "darkviolet" or $color == "darkmoon" { + $cardview-generic-background-color: $base-bg !default; + + $cardview-generic-header-filter-icon-empty-color: $base-text-color !default; + $cardview-generic-header-filter-icon-selected-color: $base-accent !default; + + $cardview-generic-header-item-background-color: lighten($base-bg, 3.92) !default; + $cardview-generic-header-item-border-color: $base-border-color !default; + $cardview-generic-header-item-hovered-background-color: lighten($cardview-generic-header-item-background-color, 3.92) !default; + $cardview-generic-header-item-hovered-border-color: lighten($cardview-generic-header-item-border-color, 10.20) !default; + + $cardview-generic-header-item-prohibition-icon-color: $base-danger !default; + + $cardview-generic-nodata-text-color: $base-label-color !default; + $cardview-generic-nodata-icon-container-background-color: lighten(desaturate($base-bg, 0.30), 8.04) !default; + + $cardview-generic-card-content-field-value-highlight-color: $base-inverted-text-color !default; + $cardview-generic-card-content-field-value-highlight-background: $base-accent !default; + + $cardview-generic-card-cover-background-color: #FFF !default; + + $cardview-generic-card-cover-noimage-background-color: lighten(desaturate($base-bg, 0.30), 8.04) !default; + $cardview-generic-card-cover-noimage-icon-color: darken($base-text-color, 38.04) !default; + + $cardview-generic-card-selection-background-color: darken(desaturate($base-accent, 44.18), 25.88) !default; + + $cardview-generic-card-border-color: $base-border-color !default; + $cardview-generic-card-background-color: $base-bg !default; +} + +@if $color == "contrast" { + $cardview-generic-background-color: $base-bg !default; + + $cardview-generic-header-filter-icon-empty-color: $base-text-color !default; + $cardview-generic-header-filter-icon-selected-color: $base-accent !default; + + $cardview-generic-header-item-background-color: $base-bg !default; + $cardview-generic-header-item-border-color: $base-border-color !default; + $cardview-generic-header-item-hovered-background-color: $cardview-generic-header-item-background-color !default; + $cardview-generic-header-item-hovered-border-color: $cardview-generic-header-item-border-color !default; + + $cardview-generic-header-item-prohibition-icon-color: $base-danger !default; + + $cardview-generic-nodata-text-color: $base-label-color !default; + $cardview-generic-nodata-icon-container-background-color: $base-bg !default; + + $cardview-generic-card-content-field-value-highlight-color: $base-inverted-text-color !default; + $cardview-generic-card-content-field-value-highlight-background: $base-accent !default; + + $cardview-generic-card-cover-background-color: #FFF !default; + + $cardview-generic-card-cover-noimage-background-color: $base-bg !default; + $cardview-generic-card-cover-noimage-icon-color: $base-text-color !default; + + $cardview-generic-card-selection-background-color: $base-accent !default; + + $cardview-generic-card-border-color: $base-border-color !default; + $cardview-generic-card-background-color: $base-bg !default; +} + +$cardview-background-color: $cardview-generic-background-color !default; + +$cardview-header-filter-icon-empty-color: $cardview-generic-header-filter-icon-empty-color !default; +$cardview-header-filter-icon-selected-color: $cardview-generic-header-filter-icon-selected-color !default; + +$cardview-header-item-border-color: $cardview-generic-header-item-border-color !default; + +$cardview-header-item-background-color: $cardview-generic-header-item-background-color !default; +$cardview-header-item-hovered-background-color: $cardview-generic-header-item-hovered-background-color !default; +$cardview-header-item-hovered-border-color: $cardview-generic-header-item-hovered-border-color !default; + +$cardview-header-item-prohibition-icon-color: $cardview-generic-header-item-prohibition-icon-color !default; + +$cardview-nodata-text-color: $cardview-generic-nodata-text-color !default; +$cardview-nodata-icon-container-background-color: $cardview-generic-nodata-icon-container-background-color !default; + +$cardview-card-content-field-value-highlight-color: $cardview-generic-card-content-field-value-highlight-color !default; +$cardview-card-content-field-value-highlight-background: $cardview-generic-card-content-field-value-highlight-background !default; + +$cardview-card-cover-background-color: $cardview-generic-card-cover-background-color !default; + +$cardview-card-cover-noimage-background-color: $cardview-generic-card-cover-noimage-background-color !default; +$cardview-card-cover-noimage-icon-color: $cardview-generic-card-cover-noimage-icon-color !default; + +$cardview-card-selection-background-color: $cardview-generic-card-selection-background-color !default; + +$cardview-card-border-color: $cardview-generic-card-border-color !default; +$cardview-card-background-color: $cardview-generic-card-background-color !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/generic/cardView/_index.scss new file mode 100644 index 000000000000..29f2262bbe3e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/generic/cardView/_index.scss @@ -0,0 +1,5 @@ +@use 'colors'; +@use 'sizes'; +@use '../../base/cardView/index' as *; + +// adduse diff --git a/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss new file mode 100644 index 000000000000..18ca55b60c05 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss @@ -0,0 +1,95 @@ +@use "../sizes" as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/cover/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; +@use '../../base/cardView/content_view/no_data/variables' as *; + +// adduse + +$cardview-generic-sizes-60: null; +$cardview-generic-sizes-48: null; +$cardview-generic-sizes-36: null; +$cardview-generic-sizes-18: null; +$cardview-generic-paddings-24: null; +$cardview-generic-paddings-16: null; +$cardview-generic-paddings-12: null; +$cardview-generic-paddings-9: null; +$cardview-generic-paddings-6: null; +$cardview-generic-text-size-16: null; +$cardview-generic-border-radius-8: null; +$cardview-generic-gaps-8: null; +$cardview-generic-gaps-16: null; +$cardview-generic-gaps-20: null; + +@if $size == "default" { + $cardview-generic-sizes-60: 60px; + $cardview-generic-sizes-48: 48px; + $cardview-generic-sizes-36: 36px; + $cardview-generic-sizes-18: 18px; + $cardview-generic-paddings-24: 24px; + $cardview-generic-paddings-16: 16px; + $cardview-generic-paddings-12: 12px; + $cardview-generic-paddings-9: 9px; + $cardview-generic-paddings-6: 6px; + $cardview-generic-text-size-16: 16px; + $cardview-generic-border-radius-8: 8px; + $cardview-generic-gaps-8: 8px; + $cardview-generic-gaps-16: 16px; + $cardview-generic-gaps-20: 20px; +} +@else if $size == "compact" { + $cardview-generic-sizes-60: 40px; + $cardview-generic-sizes-48: 32px; + $cardview-generic-sizes-36: 24px; + $cardview-generic-sizes-18: 14px; + $cardview-generic-paddings-24: 16px; + $cardview-generic-paddings-16: 12px; + $cardview-generic-paddings-12: 8px; + $cardview-generic-paddings-9: 5px; + $cardview-generic-paddings-6: 4px; + $cardview-generic-text-size-16: 14px; + $cardview-generic-border-radius-8: 6px; + $cardview-generic-gaps-8: 4px; + $cardview-generic-gaps-16: 12px; + $cardview-generic-gaps-20: 16px; +} + +$cardview-border-radius: 16px !default; +$cardview-gap: $cardview-generic-gaps-20 !default; +$cardview-padding: $cardview-generic-paddings-24 !default; + +$cardview-headerpanel-content-gap: $cardview-generic-gaps-8 !default; + +$cardview-header-item-border-width: 1px !default; +$cardview-header-item-border-radius: 4px !default; +$cardview-header-item-padding-horizontal: $cardview-generic-paddings-12 !default; +$cardview-header-item-padding-vertical: $cardview-generic-paddings-9 !default; +$cardview-header-item-line-height: $cardview-generic-sizes-18 !default; +$cardview-header-item-icon-size: $cardview-generic-sizes-18 !default; + +$cardview-nodata-icon-container-size: $cardview-generic-sizes-60 !default; +$cardview-nodata-icon-container-margin: $cardview-generic-paddings-12 !default; +$cardview-nodata-icon-size: $cardview-generic-sizes-36 !default; + +$cardview-content-column-gap: $cardview-generic-gaps-16 !default; +$cardview-content-row-gap: $cardview-generic-gaps-16 !default; + +$cardview-card-border-size: 1px !default; +$cardview-card-min-width: 250px !default; +$cardview-card-border-radius: $cardview-generic-border-radius-8 !default; + +$cardview-card-cover-noimage-icon-size: $cardview-generic-sizes-48 !default; + +$cardview-card-content-padding-vertical: $cardview-generic-paddings-12 !default; +$cardview-card-content-padding-horizontal: $cardview-generic-paddings-12 !default; +$cardview-card-content-field-gap: 5px !default; +$cardview-card-content-cell-padding-vertical: $cardview-generic-paddings-6 !default; +$cardview-card-content-cell-padding-horizontal: $cardview-generic-paddings-12 !default; + +$cardview-card-header-text-size: $cardview-generic-text-size-16 !default; +$cardview-card-header-border-radius: $cardview-generic-border-radius-8 !default; diff --git a/packages/devextreme-scss/scss/widgets/material/_index.scss b/packages/devextreme-scss/scss/widgets/material/_index.scss index e9d6526025ba..ff95df79b8ec 100644 --- a/packages/devextreme-scss/scss/widgets/material/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/_index.scss @@ -61,3 +61,4 @@ @use "./sortable"; @use "./deferRendering"; @use "./map"; +@use "./cardView"; diff --git a/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss new file mode 100644 index 000000000000..6a0f623b8baf --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss @@ -0,0 +1,131 @@ +@use '../colors' as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/cover/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; +@use '../../base/cardView/content_view/no_data/variables' as *; +@use "sass:color"; + +// adduse + +$cardview-material-background-color: null !default; + +$cardview-material-header-filter-icon-empty-color: null !default; +$cardview-material-header-filter-icon-selected-color: null !default; + +$cardview-material-header-item-border-color: null !default; + +$cardview-material-header-item-background-color: null !default; +$cardview-material-header-item-hovered-background-color: null !default; +$cardview-material-header-item-hovered-border-color: null !default; + +$cardview-material-header-item-prohibition-icon-color: null !default; + +$cardview-material-nodata-text-color: null !default; +$cardview-material-nodata-icon-container-background-color: null !default; + +$cardview-material-card-content-field-value-highlight-color: null !default; +$cardview-material-card-content-field-value-highlight-background: null !default; + +$cardview-material-card-cover-background-color: null !default; + +$cardview-material-card-cover-noimage-background-color: null !default; +$cardview-material-card-cover-noimage-icon-color: null !default; + +$cardview-material-card-selection-background-color: null !default; + +$cardview-material-card-border-color: null !default; +$cardview-material-card-background-color: null !default; + +@if $mode == 'light' { + $cardview-material-background-color: $base-bg !default; + + $cardview-material-header-filter-icon-empty-color: $base-text-color !default; + $cardview-material-header-filter-icon-selected-color: $base-accent !default; + + $cardview-material-header-item-background-color: darken($base-bg, 7.84) !default; + $cardview-material-header-item-border-color: $cardview-material-header-item-background-color !default; + $cardview-material-header-item-hovered-background-color: darken($cardview-material-header-item-background-color, 4.31) !default; + $cardview-material-header-item-hovered-border-color: $cardview-material-header-item-hovered-background-color !default; + + $cardview-material-header-item-prohibition-icon-color: darken(saturate(adjust-hue($base-danger, -4), 22.80), 9.61) !default; + + $cardview-material-nodata-text-color: $base-label-color !default; + $cardview-material-nodata-icon-container-background-color: darken($base-bg, 7.84) !default; + + $cardview-material-card-content-field-value-highlight-color: $base-inverted-text-color !default; + $cardview-material-card-content-field-value-highlight-background: $base-accent !default; + + $cardview-material-card-cover-background-color: #FFF !default; + + $cardview-material-card-cover-noimage-background-color: darken($base-bg, 7.84) !default; + $cardview-material-card-cover-noimage-icon-color: color.change($base-text-color, $alpha: 0.38) !default; + + $cardview-material-card-selection-background-color: color.change($base-accent, $alpha: 0.08) !default; + + $cardview-material-card-border-color: $base-border-color !default; + $cardview-material-card-background-color: $base-bg !default; +} + +@if $mode == "dark" { + $cardview-material-background-color: $base-bg !default; + + $cardview-material-header-filter-icon-empty-color: $base-text-color !default; + $cardview-material-header-filter-icon-selected-color: $base-accent !default; + + $cardview-material-header-item-background-color: lighten(desaturate($base-bg, 0.30), 8.04) !default; + $cardview-material-header-item-border-color: $cardview-material-header-item-background-color!default; + $cardview-material-header-item-hovered-background-color: lighten(desaturate($cardview-material-header-item-background-color, 3.47), 2.16) !default; + $cardview-material-header-item-hovered-border-color: $cardview-material-header-item-hovered-background-color !default; + + $cardview-material-header-item-prohibition-icon-color: darken(desaturate($base-danger, 0.27), 7.45) !default; + + $cardview-material-nodata-text-color: $base-label-color !default; + $cardview-material-nodata-icon-container-background-color: lighten(desaturate($base-bg, 0.30), 8.04) !default; + + $cardview-material-card-content-field-value-highlight-color: $base-inverted-text-color !default; + $cardview-material-card-content-field-value-highlight-background: $base-accent !default; + + $cardview-material-card-cover-background-color: #FFF !default; + + $cardview-material-card-cover-noimage-background-color: lighten(desaturate($base-bg, 0.30), 8.04) !default; + $cardview-material-card-cover-noimage-icon-color: color.change($base-text-color, $alpha: 0.38) !default; + + $cardview-material-card-selection-background-color: color.change(lighten($base-accent, 19.22), $alpha: 0.08) !default; + + $cardview-material-card-border-color: $base-border-color !default; + $cardview-material-card-background-color: $base-bg !default; +} + +$cardview-background-color: $cardview-material-background-color !default; + +$cardview-header-filter-icon-empty-color: $cardview-material-header-filter-icon-empty-color !default; +$cardview-header-filter-icon-selected-color: $cardview-material-header-filter-icon-selected-color !default; + +$cardview-header-item-border-color: $cardview-material-header-item-border-color !default; + +$cardview-header-item-background-color: $cardview-material-header-item-background-color !default; +$cardview-header-item-hovered-background-color: $cardview-material-header-item-hovered-background-color !default; +$cardview-header-item-hovered-border-color: $cardview-material-header-item-hovered-border-color !default; + +$cardview-header-item-prohibition-icon-color: $cardview-material-header-item-prohibition-icon-color !default; + +$cardview-nodata-text-color: $cardview-material-nodata-text-color !default; +$cardview-nodata-icon-container-background-color: $cardview-material-nodata-icon-container-background-color !default; + +$cardview-card-content-field-value-highlight-color: $cardview-material-card-content-field-value-highlight-color !default; +$cardview-card-content-field-value-highlight-background: $cardview-material-card-content-field-value-highlight-background !default; + +$cardview-card-cover-background-color: $cardview-material-card-cover-background-color !default; + +$cardview-card-cover-noimage-background-color: $cardview-material-card-cover-noimage-background-color !default; +$cardview-card-cover-noimage-icon-color: $cardview-material-card-cover-noimage-icon-color !default; + +$cardview-card-selection-background-color: $cardview-material-card-selection-background-color !default; + +$cardview-card-border-color: $cardview-material-card-border-color !default; +$cardview-card-background-color: $cardview-material-card-background-color !default; diff --git a/packages/devextreme-scss/scss/widgets/material/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/material/cardView/_index.scss new file mode 100644 index 000000000000..29f2262bbe3e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/material/cardView/_index.scss @@ -0,0 +1,5 @@ +@use 'colors'; +@use 'sizes'; +@use '../../base/cardView/index' as *; + +// adduse diff --git a/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss new file mode 100644 index 000000000000..508b922c0cf9 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss @@ -0,0 +1,95 @@ +@use "../sizes" as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/cover/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; +@use '../../base/cardView/content_view/no_data/variables' as *; + +// adduse + +$cardview-material-sizes-64: null; +$cardview-material-sizes-48: null; +$cardview-material-sizes-40: null; +$cardview-material-sizes-20: null; +$cardview-material-paddings-24: null; +$cardview-material-paddings-16: null; +$cardview-material-paddings-12: null; +$cardview-material-paddings-8: null; +$cardview-material-paddings-6: null; +$cardview-material-text-size-16: null; +$cardview-material-border-radius-8: null; +$cardview-material-gaps-8: null; +$cardview-material-gaps-16: null; +$cardview-material-gaps-24: null; + +@if $size == "default" { + $cardview-material-sizes-64: 64px; + $cardview-material-sizes-48: 48px; + $cardview-material-sizes-40: 40px; + $cardview-material-sizes-20: 20px; + $cardview-material-paddings-24: 24px; + $cardview-material-paddings-16: 16px; + $cardview-material-paddings-12: 12px; + $cardview-material-paddings-8: 8px; + $cardview-material-paddings-6: 6px; + $cardview-material-text-size-16: 16px; + $cardview-material-border-radius-8: 8px; + $cardview-material-gaps-8: 8px; + $cardview-material-gaps-16: 16px; + $cardview-material-gaps-24: 24px; +} +@else if $size == "compact" { + $cardview-material-sizes-64: 48px; + $cardview-material-sizes-48: 32px; + $cardview-material-sizes-40: 32px; + $cardview-material-sizes-20: 16px; + $cardview-material-paddings-24: 16px; + $cardview-material-paddings-16: 12px; + $cardview-material-paddings-12: 6px; + $cardview-material-paddings-8: 8px; + $cardview-material-paddings-6: 4px; + $cardview-material-text-size-16: 14px; + $cardview-material-border-radius-8: 6px; + $cardview-material-gaps-8: 4px; + $cardview-material-gaps-16: 12px; + $cardview-material-gaps-24: 16px; +} + +$cardview-border-radius: 16px !default; +$cardview-gap: $cardview-material-gaps-24 !default; +$cardview-padding: $cardview-material-paddings-24 !default; + +$cardview-headerpanel-content-gap: $cardview-material-gaps-8 !default; + +$cardview-header-item-border-width: 1px !default; +$cardview-header-item-border-radius: 99px !default; +$cardview-header-item-padding-horizontal: $cardview-material-paddings-16 !default; +$cardview-header-item-padding-vertical: $cardview-material-paddings-8 !default; +$cardview-header-item-line-height: $cardview-material-sizes-20 !default; +$cardview-header-item-icon-size: $cardview-material-sizes-20 !default; + +$cardview-nodata-icon-container-size: $cardview-material-sizes-64 !default; +$cardview-nodata-icon-container-margin: $cardview-material-paddings-12 !default; +$cardview-nodata-icon-size: $cardview-material-sizes-40 !default; + +$cardview-content-column-gap: $cardview-material-gaps-16 !default; +$cardview-content-row-gap: $cardview-material-gaps-16 !default; + +$cardview-card-border-size: 1px !default; +$cardview-card-min-width: 250px !default; +$cardview-card-border-radius: $cardview-material-border-radius-8 !default; + +$cardview-card-cover-noimage-icon-size: $cardview-material-sizes-48 !default; + +$cardview-card-content-padding-vertical: $cardview-material-paddings-12 !default; +$cardview-card-content-padding-horizontal: $cardview-material-paddings-12 !default; +$cardview-card-content-field-gap: 5px !default; +$cardview-card-content-cell-padding-vertical: $cardview-material-paddings-6 !default; +$cardview-card-content-cell-padding-horizontal: $cardview-material-paddings-12 !default; + +$cardview-card-header-text-size: $cardview-material-text-size-16 !default; +$cardview-card-header-border-radius: $cardview-material-border-radius-8 !default; diff --git a/packages/devextreme-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts index 27ffe61a73de..b2d36dd7ea7b 100644 --- a/packages/devextreme-themebuilder/tests/data/dependencies.ts +++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts @@ -16,6 +16,7 @@ export const dependencies: FlatStylesDependencies = { buttongroup: ['validation', 'button'], dropdownbutton: ['validation', 'button', 'buttongroup', 'popup', 'loadindicator', 'loadpanel', 'scrollview', 'list'], calendar: ['validation', 'button'], + cardview: ['box', 'button', 'calendar', 'checkbox', 'contextmenu', 'datebox', 'filterbuilder', 'list', 'loadindicator', 'loadpanel', 'numberbox', 'popup', 'scrollview', 'selectbox', 'sortable', 'textbox', 'toast', 'toolbar', 'treeview', 'validation'], chat: ['button', 'loadindicator', 'loadpanel', 'scrollview', 'textbox', 'validation'], checkbox: ['validation'], numberbox: ['validation', 'button', 'loadindicator'], diff --git a/packages/devextreme/js/__internal/core/di/index.test.ts b/packages/devextreme/js/__internal/core/di/index.test.ts new file mode 100644 index 000000000000..4e5ece50a074 --- /dev/null +++ b/packages/devextreme/js/__internal/core/di/index.test.ts @@ -0,0 +1,186 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ +/* eslint-disable prefer-const */ +/* eslint-disable @typescript-eslint/init-declarations */ +/* eslint-disable max-classes-per-file */ +/* eslint-disable class-methods-use-this */ +import { describe, expect, it } from '@jest/globals'; + +import { DIContext } from './index'; + +describe('basic', () => { + describe('register', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + it('should return registered class', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + + expect(ctx.get(MyClass)).toBeInstanceOf(MyClass); + expect(ctx.get(MyClass).getNumber()).toBe(1); + }); + + it('should return registered class with tryGet', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + + expect(ctx.tryGet(MyClass)).toBeInstanceOf(MyClass); + expect(ctx.tryGet(MyClass)?.getNumber()).toBe(1); + }); + + it('should return same instance each time', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + + expect(ctx.get(MyClass)).toBe(ctx.get(MyClass)); + }); + }); + + describe('registerInstance', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + const ctx = new DIContext(); + const instance = new MyClass(); + ctx.registerInstance(MyClass, instance); + + it('should work', () => { + expect(ctx.get(MyClass)).toBe(instance); + }); + }); + + describe('non registered items', () => { + const ctx = new DIContext(); + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + it('should throw', () => { + expect(() => ctx.get(MyClass)).toThrow(); + }); + it('should not throw if tryGet', () => { + expect(ctx.tryGet(MyClass)).toBe(null); + }); + }); +}); + +describe('dependencies', () => { + class MyUtilityClass { + static dependencies = [] as const; + + getNumber(): number { + return 2; + } + } + + class MyClass { + static dependencies = [MyUtilityClass] as const; + + constructor(private readonly utility: MyUtilityClass) {} + + getSuperNumber(): number { + return this.utility.getNumber() * 2; + } + } + + const ctx = new DIContext(); + ctx.register(MyUtilityClass); + ctx.register(MyClass); + + it('should return registered class', () => { + expect(ctx.get(MyClass)).toBeInstanceOf(MyClass); + expect(ctx.get(MyUtilityClass)).toBeInstanceOf(MyUtilityClass); + }); + + it('dependecies should work', () => { + expect(ctx.get(MyClass).getSuperNumber()).toBe(4); + }); +}); + +describe('mocks', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + class MyClassMock implements MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 2; + } + } + + const ctx = new DIContext(); + ctx.register(MyClass, MyClassMock); + + it('should return mock class when they are registered', () => { + expect(ctx.get(MyClass)).toBeInstanceOf(MyClassMock); + expect(ctx.get(MyClass).getNumber()).toBe(2); + }); +}); + +it('should work regardless of registration order', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + class MyDependentClass { + static dependencies = [MyClass] as const; + + constructor(private readonly myClass: MyClass) {} + + getSuperNumber(): number { + return this.myClass.getNumber() * 2; + } + } + + const ctx = new DIContext(); + ctx.register(MyDependentClass); + ctx.register(MyClass); + expect(ctx.get(MyDependentClass).getSuperNumber()).toBe(2); +}); + +describe('dependency cycle', () => { + class MyClass1 { + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-use-before-define + static dependencies = [MyClass2] as const; + + constructor(private readonly myClass2: MyClass2) {} + } + class MyClass2 { + static dependencies = [MyClass1] as const; + + constructor(private readonly myClass1: MyClass1) {} + } + + const ctx = new DIContext(); + ctx.register(MyClass1); + ctx.register(MyClass2); + + it('should throw', () => { + expect(() => ctx.get(MyClass1)).toThrow(); + expect(() => ctx.get(MyClass2)).toThrow(); + }); +}); diff --git a/packages/devextreme/js/__internal/core/di/index.ts b/packages/devextreme/js/__internal/core/di/index.ts new file mode 100644 index 000000000000..20f8700496f1 --- /dev/null +++ b/packages/devextreme/js/__internal/core/di/index.ts @@ -0,0 +1,88 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +interface AbstractType extends Function { + prototype: T; +} + +type Constructor = new(...deps: TDeps) => T; + +interface DIItem extends Constructor { + dependencies: readonly [...{ [P in keyof TDeps]: AbstractType }]; +} + +export class DIContext { + private readonly instances: Map = new Map(); + + private readonly fabrics: Map = new Map(); + + private readonly antiRecursionSet = new Set(); + + public register( + id: AbstractType, + fabric: DIItem, + ): void; + public register( + idAndFabric: DIItem, + ): void; + public register( + id: DIItem, + fabric?: DIItem, + ): void { + // eslint-disable-next-line no-param-reassign + fabric ??= id; + this.fabrics.set(id, fabric); + } + + public registerInstance( + id: AbstractType, + instance: T, + ): void { + this.instances.set(id, instance); + } + + public get( + id: AbstractType, + ): T { + const instance = this.tryGet(id); + + if (instance) { + return instance; + } + + throw new Error(`DI item is not registered: ${id}`); + } + + public tryGet( + id: AbstractType, + ): T | null { + if (this.instances.get(id)) { + return this.instances.get(id) as T; + } + + const fabric = this.fabrics.get(id); + if (fabric) { + const res: T = this.create(fabric as any); + this.instances.set(id, res); + this.instances.set(fabric, res); + return res; + } + + return null; + } + + private create(fabric: DIItem): T { + if (this.antiRecursionSet.has(fabric)) { + throw new Error('dependency cycle in DI'); + } + + this.antiRecursionSet.add(fabric); + + const args = fabric.dependencies.map((dependency) => this.get(dependency)); + + this.antiRecursionSet.delete(fabric); + + // eslint-disable-next-line new-cap + return new fabric(...args as any); + } +} diff --git a/packages/devextreme/js/__internal/core/m_inferno_renderer.ts b/packages/devextreme/js/__internal/core/m_inferno_renderer.ts index 2a1e550457a0..06ee5e06927b 100644 --- a/packages/devextreme/js/__internal/core/m_inferno_renderer.ts +++ b/packages/devextreme/js/__internal/core/m_inferno_renderer.ts @@ -1,9 +1,9 @@ +/* eslint-disable spellcheck/spell-checker */ import domAdapter from '@js/core/dom_adapter'; import { cleanDataRecursive } from '@js/core/element_data'; import injector from '@js/core/utils/dependency_injector'; import { hydrate, InfernoEffectHost } from '@ts/core/r1/runtime/inferno/index'; import { render } from 'inferno'; -// eslint-disable-next-line import/no-extraneous-dependencies import { createElement } from 'inferno-create-element'; const remove = (element) => { @@ -59,6 +59,14 @@ const infernoRenderer = injector({ render(createElement(component, props), container); } }, + + renderIntoContainer: (jsx, container, replace) => { + if (!replace) { + hydrate(jsx, container); + } else { + render(jsx, container); + } + }, }); export { infernoRenderer }; diff --git a/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts b/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts index 94cda0c2ce25..ddf7e068703d 100644 --- a/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts +++ b/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts @@ -1,13 +1,13 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable func-names */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable no-plusplus */ -/* eslint-disable @typescript-eslint/no-explicit-any */ + import { Component } from 'inferno'; let contextId = 0; -export const createContext = function(defaultValue: T): { id: number; - Provider: any; - defaultValue: unknown; } { +export const createContext = function(defaultValue: T) { const id = contextId++; return { diff --git a/packages/devextreme/js/__internal/core/reactive/core.ts b/packages/devextreme/js/__internal/core/reactive/core.ts new file mode 100644 index 000000000000..e0874385e623 --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/core.ts @@ -0,0 +1,88 @@ +/* eslint-disable spellcheck/spell-checker */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable max-classes-per-file */ + +import { type Subscription, SubscriptionBag } from './subscription'; +import type { + Callback, Gettable, Subscribable, Updatable, +} from './types'; + +export class Observable implements Subscribable, Updatable, Gettable { + private readonly callbacks: Set> = new Set(); + + constructor(private value: T) {} + + update(value: T): void { + if (this.value === value) { + return; + } + this.value = value; + + this.callbacks.forEach((c) => { + c(value); + }); + } + + updateFunc(func: (oldValue: T) => T): void { + this.update(func(this.value)); + } + + subscribe(callback: Callback): Subscription { + this.callbacks.add(callback); + callback(this.value); + + return { + unsubscribe: () => this.callbacks.delete(callback), + }; + } + + unreactive_get(): T { + return this.value; + } + + dispose(): void { + this.callbacks.clear(); + } +} + +export class InterruptableComputed< + TArgs extends readonly any[], TValue, +> extends Observable { + private readonly depValues: [...TArgs]; + + private readonly depInitialized: boolean[]; + + private isInitialized = false; + + private readonly subscriptions = new SubscriptionBag(); + + constructor( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, + ) { + super(undefined as any); + + this.depValues = deps.map(() => undefined) as any; + this.depInitialized = deps.map(() => false); + + deps.forEach((dep, i) => { + this.subscriptions.add(dep.subscribe((v) => { + this.depValues[i] = v; + + if (!this.isInitialized) { + this.depInitialized[i] = true; + this.isInitialized = this.depInitialized.every((e) => e); + } + + if (this.isInitialized) { + this.update(compute(...this.depValues)); + } + })); + }); + } + + dispose(): void { + super.dispose(); + this.subscriptions.unsubscribe(); + } +} diff --git a/packages/devextreme/js/__internal/core/reactive/index.ts b/packages/devextreme/js/__internal/core/reactive/index.ts new file mode 100644 index 000000000000..e2e8474530df --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/index.ts @@ -0,0 +1,3 @@ +export * from './subscription'; +export * from './types'; +export * from './utilities'; diff --git a/packages/devextreme/js/__internal/core/reactive/subscription.ts b/packages/devextreme/js/__internal/core/reactive/subscription.ts new file mode 100644 index 000000000000..d3bc303311df --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/subscription.ts @@ -0,0 +1,17 @@ +export interface Subscription { + unsubscribe: () => void; +} + +export class SubscriptionBag implements Subscription { + private readonly subscriptions: Subscription[] = []; + + add(subscription: Subscription): void { + this.subscriptions.push(subscription); + } + + unsubscribe(): void { + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + } +} diff --git a/packages/devextreme/js/__internal/core/reactive/types.ts b/packages/devextreme/js/__internal/core/reactive/types.ts new file mode 100644 index 000000000000..176361809ac4 --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/types.ts @@ -0,0 +1,28 @@ +/* eslint-disable spellcheck/spell-checker */ +import type { Subscription } from './subscription'; + +export interface Subscribable { + subscribe: (callback: Callback) => Subscription; +} + +export type MaybeSubscribable = T | Subscribable; + +export type MapMaybeSubscribable = { [K in keyof T]: MaybeSubscribable }; + +export function isSubscribable(value: unknown): value is Subscribable { + return typeof value === 'object' && !!value && 'subscribe' in value; +} + +export type Callback = (value: T) => void; + +export interface Updatable { + update: (value: T) => void; + updateFunc: (func: (oldValue: T) => T) => void; +} + +export interface Gettable { + unreactive_get: () => T; +} + +export type SubsGets = Subscribable & Gettable; +export type SubsGetsUpd = Subscribable & Gettable & Updatable; diff --git a/packages/devextreme/js/__internal/core/reactive/utilities.test.ts b/packages/devextreme/js/__internal/core/reactive/utilities.test.ts new file mode 100644 index 000000000000..c2faab3d23c4 --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/utilities.test.ts @@ -0,0 +1,217 @@ +/* eslint-disable spellcheck/spell-checker */ +import { + beforeEach, describe, expect, it, jest, +} from '@jest/globals'; + +import { + computed, interruptableComputed, state, toSubscribable, +} from './utilities'; + +describe('state', () => { + let myState = state('some value'); + + beforeEach(() => { + myState = state('some value'); + }); + + describe('unreactive_get', () => { + it('should return value', () => { + expect(myState.unreactive_get()).toBe('some value'); + }); + + it('should return current value if it was updated', () => { + myState.update('new value'); + expect(myState.unreactive_get()).toBe('new value'); + }); + }); + + describe('subscribe', () => { + it('should call callback on initial set', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value'); + }); + + it('should call callback on update', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + myState.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value'); + }); + + it('should not trigger update if value is not changed', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + + myState.update('some value'); + + expect(callback).toBeCalledTimes(1); + }); + }); + + describe('dispose', () => { + it('should prevent all updates', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value'); + + // @ts-expect-error + myState.dispose(); + myState.update('new value'); + + expect(callback).toBeCalledTimes(1); + }); + }); +}); + +describe('computed', () => { + let myState1 = state('some value'); + let myState2 = state('other value'); + let myComputed = computed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + + beforeEach(() => { + myState1 = state('some value'); + myState2 = state('other value'); + myComputed = computed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + }); + + describe('unreactive_get', () => { + it('should calculate initial value', () => { + expect(myComputed.unreactive_get()).toBe('some value other value'); + }); + + it('should return current value if it dependency is updated', () => { + myState1.update('new value'); + expect(myComputed.unreactive_get()).toBe('new value other value'); + }); + }); + + describe('subscribe', () => { + it('should call callback on initial set', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value other value'); + }); + + it('should call callback on update of dependency', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + myState1.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value other value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value other value'); + }); + }); +}); + +describe('interruptableComputed', () => { + let myState1 = state('some value'); + let myState2 = state('other value'); + let myComputed = interruptableComputed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + + beforeEach(() => { + myState1 = state('some value'); + myState2 = state('other value'); + myComputed = interruptableComputed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + }); + + describe('unreactive_get', () => { + it('should calculate initial value', () => { + expect(myComputed.unreactive_get()).toBe('some value other value'); + }); + + it('should return current value if it was updated', () => { + myComputed.update('new value'); + expect(myComputed.unreactive_get()).toBe('new value'); + }); + + it('should return current value if it dependency is updated', () => { + myState1.update('new value'); + expect(myComputed.unreactive_get()).toBe('new value other value'); + }); + }); + + describe('subscribe', () => { + it('should call callback on initial set', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value other value'); + }); + + it('should call callback on update', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + myComputed.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value other value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value'); + }); + + it('should call callback on update of dependency', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + myState1.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value other value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value other value'); + }); + + it('should not trigger update if value is not changed', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + + myComputed.update('some value other value'); + + expect(callback).toBeCalledTimes(1); + }); + }); +}); + +describe('toSubscribable', () => { + it('should wrap value if it is not subscribable', () => { + const callback = jest.fn(); + toSubscribable('some value').subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value'); + }); + + it('should return value as is if subscribable', () => { + const myState = state(1); + expect(toSubscribable(myState)).toBe(myState); + }); +}); diff --git a/packages/devextreme/js/__internal/core/reactive/utilities.ts b/packages/devextreme/js/__internal/core/reactive/utilities.ts new file mode 100644 index 000000000000..79e6cee8ec0a --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/utilities.ts @@ -0,0 +1,248 @@ +/* eslint-disable @typescript-eslint/no-invalid-void-type */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable spellcheck/spell-checker */ +import { InterruptableComputed, Observable } from './core'; +import { type Subscription, SubscriptionBag } from './subscription'; +import type { + Gettable, MapMaybeSubscribable, MaybeSubscribable, Subscribable, SubsGets, SubsGetsUpd, Updatable, +} from './types'; +import { isSubscribable } from './types'; + +/** + * Creates new reactive state atom. + * @example + * ``` + * const myState = state(0); + * myState.update(1); + * ``` + * @param value initial value of state + */ +export function state(value: T): Subscribable & Updatable & Gettable { + return new Observable(value); +} + +/** + * Creates computed atom based on other atoms. + * @example + * ``` + * const myState = state(0); + * const myComputed = computed( + * (value) => value + 1, + * [myState] + * ); + * ``` + * @param compute computation func + * @param deps dependency atoms + */ +export function computed( + compute: (t1: T1) => TValue, + deps: [Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2) => TValue, + deps: [Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3,) => TValue, + deps: [Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4) => TValue, + deps: [Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => TValue, + deps: [Subscribable, Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => TValue, + // eslint-disable-next-line @stylistic/max-len + deps: [Subscribable, Subscribable, Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, +): SubsGets; +export function computed( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, +): SubsGets { + return new InterruptableComputed(compute, deps); +} + +/** + * Computed, with ability to override value using `.update(...)` method. + * @see {@link computed} + */ +export function interruptableComputed( + compute: (t1: T1) => TValue, + deps: [Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (t1: T1, t2: T2) => TValue, + deps: [Subscribable, Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (t1: T1, t2: T2, t3: T3,) => TValue, + deps: [Subscribable, Subscribable, Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4) => TValue, + deps: [Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, +): SubsGetsUpd { + return new InterruptableComputed(compute, deps); +} + +/** + * Allows to subscribe function with some side effects to changes of dependency atoms. + * @param callback function which is executed each time any dependency is updated + * @param deps dependencies + */ +export function effect( + callback: (t1: T1) => ((() => void) | void), + deps: [Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2) => ((() => void) | void), + deps: [Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3,) => ((() => void) | void), + deps: [Subscribable, Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4) => ((() => void) | void), + deps: [Subscribable, Subscribable, Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => ((() => void) | void), + deps: [Subscribable, Subscribable, Subscribable, Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => ((() => void) | void), + deps: [ + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + ] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7) => ((() => void) | void), + deps: [ + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + ] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8) + => ((() => void) | void), + deps: [ + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + ] +): Subscription; +export function effect( + callback: (...args: TArgs) => ((() => void) | void), + deps: { [I in keyof TArgs]: Subscribable }, +): Subscription { + const depValues: [...TArgs] = deps.map(() => undefined) as any; + const depInitialized = deps.map(() => false); + let isInitialized = false; + + const subscription = new SubscriptionBag(); + + deps.forEach((dep, i) => { + subscription.add(dep.subscribe((v) => { + depValues[i] = v; + + if (!isInitialized) { + depInitialized[i] = true; + isInitialized = depInitialized.every((e) => e); + } + + if (isInitialized) { + callback(...depValues); + } + })); + }); + + return subscription; +} + +export function toSubscribable(v: MaybeSubscribable): Subscribable { + if (isSubscribable(v)) { + return v; + } + + return new Observable(v); +} + +/** + * Condition atom, basing whether `cond` is true or false, + * returns value of `ifTrue` or `ifFalse` param. + * @param cond + * @param ifTrue + * @param ifFalse + */ +export function iif( + cond: MaybeSubscribable, + ifTrue: MaybeSubscribable, + ifFalse: MaybeSubscribable, +): Subscribable { + const obs = state(undefined as any); + // eslint-disable-next-line @typescript-eslint/init-declarations + let subscription: Subscription | undefined; + + // eslint-disable-next-line @typescript-eslint/no-shadow + toSubscribable(cond).subscribe((cond) => { + subscription?.unsubscribe(); + const newSource = cond ? ifTrue : ifFalse; + subscription = toSubscribable(newSource).subscribe(obs.update.bind(obs)); + }); + + return obs; +} + +/** + * Combines object of Subscribables to Subscribable of object. + * @example + * ``` + * const myValueA = state(0); + * const myValueB = state(1); + * const obj = combine({ + * myValueA, myValueB + * }); + * + * obj.unreactive_get(); // {myValueA: 0, myValueB: 1} + * @returns + */ +export function combined( + obj: MapMaybeSubscribable, +): SubsGets { + const entries = Object.entries(obj) as any as [string, Subscribable][]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return computed( + (...args) => Object.fromEntries( + args.map((v, i) => [entries[i][0], v]), + ), + entries.map(([, v]) => toSubscribable(v)), + ) as any; +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/const.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/const.ts new file mode 100644 index 000000000000..9d180a29dcf8 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/const.ts @@ -0,0 +1,26 @@ +import messageLocalization from '@js/common/core/localization/message'; +import type { ColumnChooser } from '@js/common/grids'; + +export const defaultOptions = { + columnChooser: { + enabled: false, + search: { + enabled: false, + timeout: 500, + editorOptions: {}, + }, + selection: { + allowSelectAll: false, + selectByClick: false, + recursive: false, + }, + position: undefined, + sortOrder: undefined, + mode: 'dragAndDrop', + width: 250, + height: 260, + title: messageLocalization.format('dxDataGrid-columnChooserTitle'), + emptyPanelText: messageLocalization.format('dxDataGrid-columnChooserEmptyText'), + container: undefined, + } as ColumnChooser, +}; diff --git a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts index 6db8f21ffb23..4cb0ef5d6f09 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/column_chooser/m_column_chooser.ts @@ -19,6 +19,7 @@ import type { HeaderPanel } from '../header_panel/m_header_panel'; import modules from '../m_modules'; import type { ModuleType } from '../m_types'; import { ColumnsView } from '../views/m_columns_view'; +import { defaultOptions } from './const'; const COLUMN_CHOOSER_CLASS = 'column-chooser'; const COLUMN_CHOOSER_BUTTON_CLASS = 'column-chooser-button'; @@ -600,29 +601,7 @@ const columnHeadersView = (Base: ModuleType) => class ColumnC export const columnChooserModule = { defaultOptions() { - return { - columnChooser: { - enabled: false, - search: { - enabled: false, - timeout: 500, - editorOptions: {}, - }, - selection: { - allowSelectAll: false, - selectByClick: false, - recursive: false, - }, - position: undefined, - mode: 'dragAndDrop', - width: 250, - height: 260, - title: messageLocalization.format('dxDataGrid-columnChooserTitle'), - emptyPanelText: messageLocalization.format('dxDataGrid-columnChooserEmptyText'), - // TODO private option - container: undefined, - }, - }; + return defaultOptions; }, controllers: { columnChooser: ColumnChooserController, diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts index 9701c24a428c..bebdc62db134 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts @@ -60,7 +60,7 @@ function ungroupUTCDates(items, dateParts?, dates?) { return dates; } -function convertDataFromUTCToLocal(data, column) { +export function convertDataFromUTCToLocal(data, column) { const dates = ungroupUTCDates(data); // @ts-expect-error const query = dataQuery(dates); @@ -72,11 +72,11 @@ function convertDataFromUTCToLocal(data, column) { return storeHelper.queryByOptions(query, { group }).toArray(); } -function isUTCFormat(format) { +export function isUTCFormat(format) { return format?.slice(-1) === 'Z' || format?.slice(-3) === '\'Z\''; } -const getFormatOptions = function (value, column, currentLevel) { +export const getFormatOptions = function (value, column, currentLevel) { const groupInterval = filterUtils.getGroupInterval(column); const result: any = gridCoreUtils.getFormatOptionsByColumn(column, 'headerFilter'); diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts index 4eaa86385eb8..17a9e96d867c 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter_core.ts @@ -213,6 +213,7 @@ export class HeaderFilterView extends Modules.View { const $element = that.element(); const headerFilterOptions = this._normalizeHeaderFilterOptions(options); + const { hidePopupCallback } = options; const { height, width } = headerFilterOptions; const dxPopupOptions = { @@ -247,6 +248,7 @@ export class HeaderFilterView extends Modules.View { text: headerFilterOptions.texts.cancel, onClick() { that.hideHeaderFilterMenu(); + hidePopupCallback?.(); }, }, }, diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts index c4682b9c663d..57b5dd0307ec 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts @@ -2,12 +2,12 @@ import messageLocalization from '@js/common/core/localization/message'; import $ from '@js/core/renderer'; import { getPathParts } from '@js/core/utils/data'; -import { extend } from '@js/core/utils/extend'; -import { isDefined, isString } from '@js/core/utils/type'; +import { isDefined } from '@js/core/utils/type'; import type { Properties as ToolbarProperties } from '@js/ui/toolbar'; import Toolbar from '@js/ui/toolbar'; import type { EditingController } from '@ts/grids/grid_core/editing/m_editing'; import type { HeaderFilterController } from '@ts/grids/grid_core/header_filter/m_header_filter'; +import { normalizeToolbarItems } from '@ts/grids/new/grid_core/toolbar/utils'; import type { ModuleType } from '../m_types'; import { ColumnsView } from '../views/m_columns_view'; @@ -72,7 +72,11 @@ export class HeaderPanel extends ColumnsView { }; const userItems = userToolbarOptions?.items; - options.toolbarOptions.items = this._normalizeToolbarItems(options.toolbarOptions.items, userItems); + options.toolbarOptions.items = normalizeToolbarItems( + options.toolbarOptions.items, + userItems, + DEFAULT_TOOLBAR_ITEM_NAMES, + ); this.executeAction('onToolbarPreparing', options); @@ -84,51 +88,6 @@ export class HeaderPanel extends ColumnsView { return options.toolbarOptions; } - private _normalizeToolbarItems(defaultItems, userItems) { - defaultItems.forEach((button) => { - if (!DEFAULT_TOOLBAR_ITEM_NAMES.includes(button.name)) { - throw new Error(`Default toolbar item '${button.name}' is not added to DEFAULT_TOOLBAR_ITEM_NAMES`); - } - }); - - const defaultProps = { - location: 'after', - }; - - const isArray = Array.isArray(userItems); - - if (!isDefined(userItems)) { - return defaultItems; - } - - if (!isArray) { - userItems = [userItems]; - } - - const defaultButtonsByNames = {}; - defaultItems.forEach((button) => { - defaultButtonsByNames[button.name] = button; - }); - - const normalizedItems = userItems.map((button) => { - if (isString(button)) { - button = { name: button }; - } - - if (isDefined(button.name)) { - if (isDefined(defaultButtonsByNames[button.name])) { - button = extend(true, {}, defaultButtonsByNames[button.name], button); - } else if (DEFAULT_TOOLBAR_ITEM_NAMES.includes(button.name)) { - button = { ...button, visible: false }; - } - } - - return extend(true, {}, defaultProps, button); - }); - - return isArray ? normalizedItems : normalizedItems[0]; - } - protected _renderCore() { if (!this._toolbar) { const $headerPanel = this.element(); @@ -217,7 +176,11 @@ export class HeaderPanel extends ColumnsView { this._invalidate(); } else if (parts.length === 3) { // `toolbar.items[i]` case - const normalizedItem = this._normalizeToolbarItems(this._getToolbarItems(), args.value); + const normalizedItem = normalizeToolbarItems( + this._getToolbarItems(), + [args.value], + DEFAULT_TOOLBAR_ITEM_NAMES, + )[0]; this._toolbar?.option(optionName, normalizedItem); } else if (parts.length >= 4) { // `toolbar.items[i].prop` case diff --git a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap new file mode 100644 index 000000000000..74c4e14b2e76 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap @@ -0,0 +1,185 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`common initial render should be successfull 1`] = ` +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No data +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+ +
+ +
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/caption.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/caption.test.tsx.snap new file mode 100644 index 000000000000..27b5ca7b6467 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/caption.test.tsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Content View Caption should render template with title 1`] = ` +
+
+
+ TEST_TITLE +
+
+
+`; + +exports[`Content View Caption should render title 1`] = ` +
+
+ TEST_TITLE + : +
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/card.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/card.test.tsx.snap new file mode 100644 index 000000000000..e8c610bd6aa5 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/card.test.tsx.snap @@ -0,0 +1,155 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rendering should be rendered correctly 1`] = ` +
+
+
+
+
+ +
+ Card Cover +
+
+
+ Field + : +
+
+ devextreme +
+
+
+
+
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/cover.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/cover.test.tsx.snap new file mode 100644 index 000000000000..2ba7b1dac4cf --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/cover.test.tsx.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Cover when there is no image should render image thumbnail 1`] = ` +
+
+ +
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/value_text.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/value_text.test.tsx.snap new file mode 100644 index 000000000000..b2901f53ca1c --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/value_text.test.tsx.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Content View ValueText should add title attribute 1`] = ` +
+
+ TEST +
+
+`; + +exports[`Content View ValueText should render highlighted text if passed 1`] = ` +
+
+ + USUAL_PART + + + MATCH_PART + + + USUAL_PART + +
+
+`; + +exports[`Content View ValueText should render plain text if highlighted text is null 1`] = ` +
+
+ TEST_TEXT +
+
+`; + +exports[`Content View ValueText should set root classes 1`] = ` +
+
+ TEST +
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.test.tsx new file mode 100644 index 000000000000..68719e4bdc3d --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.test.tsx @@ -0,0 +1,34 @@ +import { + describe, expect, it, +} from '@jest/globals'; +import type { Cell } from '@ts/grids/new/grid_core/columns_controller/types'; +import { render } from 'inferno'; + +import { Caption } from './caption'; + +describe('Content View', () => { + describe('Caption', () => { + it('should render title', () => { + const container = document.createElement('div'); + + const cell = { column: { caption: 'TEST_TITLE' } } as unknown as Cell; + + render(, container); + + expect(container).toMatchSnapshot(); + }); + + it('should render template with title', () => { + const container = document.createElement('div'); + + const cell = { column: { caption: 'TEST_TITLE' } } as unknown as Cell; + + render( (
{cell.column.caption}
)} + />, container); + + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.tsx new file mode 100644 index 000000000000..1488e38ca494 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/caption.tsx @@ -0,0 +1,21 @@ +import type { Cell } from '@ts/grids/new/grid_core/columns_controller/types'; +import type { ComponentType } from 'inferno'; + +export interface CaptionProps { + cell: Cell; + template?: ComponentType<{ cell: Cell }>; +} + +export const Caption = (props: CaptionProps): JSX.Element => { + const Template = props.template; + + return ( +
+ {Template ? ( +