diff --git a/examples/webpack-demo-vanilla-bundle/package.json b/examples/webpack-demo-vanilla-bundle/package.json index 3122ca549..e057bb679 100644 --- a/examples/webpack-demo-vanilla-bundle/package.json +++ b/examples/webpack-demo-vanilla-bundle/package.json @@ -44,6 +44,9 @@ "whatwg-fetch": "^3.6.2" }, "devDependencies": { + "@fnando/sparkline": "^0.3.10", + "@types/faker": "^5.5.9", + "@types/fnando__sparkline": "^0.3.4", "@types/jquery": "^3.5.11", "@types/moment": "^2.13.0", "@types/node": "^17.0.5", @@ -51,6 +54,7 @@ "clean-webpack-plugin": "4.0.0", "copy-webpack-plugin": "^10.2.0", "css-loader": "^6.5.1", + "faker": "^5.5.3", "file-loader": "^6.2.0", "fork-ts-checker-webpack-plugin": "^6.5.0", "html-loader": "^3.0.1", diff --git a/examples/webpack-demo-vanilla-bundle/src/app-routing.ts b/examples/webpack-demo-vanilla-bundle/src/app-routing.ts index acbcbd8cf..632b1faa5 100644 --- a/examples/webpack-demo-vanilla-bundle/src/app-routing.ts +++ b/examples/webpack-demo-vanilla-bundle/src/app-routing.ts @@ -21,6 +21,7 @@ export class AppRouting { { route: 'example15', name: 'example15', title: 'Example15', moduleId: './examples/example15' }, { route: 'example16', name: 'example16', title: 'Example16', moduleId: './examples/example16' }, { route: 'example17', name: 'example17', title: 'Example17', moduleId: './examples/example17' }, + { route: 'example18', name: 'example18', title: 'Example18', moduleId: './examples/example18' }, { route: 'icons', name: 'icons', title: 'icons', moduleId: './examples/icons' }, { route: '', redirect: 'example01' }, { route: '**', redirect: 'example01' } diff --git a/examples/webpack-demo-vanilla-bundle/src/app.html b/examples/webpack-demo-vanilla-bundle/src/app.html index 15aa14378..994850756 100644 --- a/examples/webpack-demo-vanilla-bundle/src/app.html +++ b/examples/webpack-demo-vanilla-bundle/src/app.html @@ -33,7 +33,7 @@

Slickgrid-Universal

+ diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example18.html b/examples/webpack-demo-vanilla-bundle/src/examples/example18.html new file mode 100644 index 000000000..e889b38b1 --- /dev/null +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example18.html @@ -0,0 +1,59 @@ +

+ Example 18 - Real-Time Trading Platform + (with Material Theme) +
+ + code + +
+

+
+ Simulate a stock trading platform with lot of price changes, to show SlickGrid HUGE PERF., do the following: (1) lower + Changes Rate (2) increase both Changes per Cycle and (3) lower Highlight Duration +
+ +
+
+
+
+ + + + + +
+ + + + + + + + + + to + + + + + + + + + +
+
+ +
+
+
\ No newline at end of file diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example18.scss b/examples/webpack-demo-vanilla-bundle/src/examples/example18.scss new file mode 100644 index 000000000..791d364ec --- /dev/null +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example18.scss @@ -0,0 +1,51 @@ +// @import '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-salesforce.lite.scss'; + +$sparkline-color: #00b78d; +// $sparkline-color: #573585; + +.trading-platform.full-screen { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding: 10px 12px 0 10px; + background-color: white; + z-index: 8000; + position: fixed; +} +.changed-gain { + background-color: #eafae8 !important; +} +.changed-loss { + background-color: #ffeae8 !important; +} +.simulation-form { + margin-bottom: 15px; + + input[type=number] { + height: 30px; + width: 50px; + border: 1px solid #c0c0c0; + border-radius: 3px; + } + div.range { + display: contents; + width: 200px; + label.form-label { + margin: 0; + } + input.form-range { + width: 120px; + } + } + .refresh-rate input { + height: 30px; + width: 46px; + } +} +.sparkline { + stroke: $sparkline-color; + // fill: none; + fill: rgba($sparkline-color, 0.03); +} diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example18.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example18.ts new file mode 100644 index 000000000..cee136c88 --- /dev/null +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example18.ts @@ -0,0 +1,314 @@ +import * as Faker from 'faker'; +import sparkline from '@fnando/sparkline'; +import { + Aggregators, + Column, + deepCopy, + FieldType, + Filters, + Formatter, + Formatters, + GridOption, + GroupTotalFormatters, +} from '@slickgrid-universal/common'; +import './example18.scss'; +import { Slicker, SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle'; +import { ExampleGridOptions } from './example-grid-options'; + +const NB_ITEMS = 200; + +const currencyFormatter: Formatter = (cell: number, row: number, value: string) => + ` ${value}`; + +const priceFormatter: Formatter = (cell: number, row: number, value: number, col: Column, dataContext: any) => { + const direction = dataContext.priceChange >= 0 ? 'up' : 'down'; + return ` ${value}`; +}; + +const transactionTypeFormatter: Formatter = (row: number, cell: number, value: string) => + ` ${value}`; + +const historicSparklineFormatter: Formatter = (row: number, cell: number, value: string, col: Column, dataContext: any) => { + const svgElem = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svgElem.setAttributeNS(null, 'width', '135'); + svgElem.setAttributeNS(null, 'height', '30'); + svgElem.setAttributeNS(null, 'stroke-width', '2'); + svgElem.classList.add('sparkline'); + sparkline(svgElem, dataContext.historic, { interactive: true }); + return svgElem.outerHTML; +}; + +export class Example34 { + title = 'Example 34: Real-Time Stock Trading'; + subTitle = `Simulate a stock trading platform with lot of price changes + `; + + columnDefinitions: Column[] = []; + dataset: any[] = []; + gridOptions!: GridOption; + isFullScreen = false; + highlightDuration = 150; + itemCount = 200; + minChangePerCycle = 0; + maxChangePerCycle = 10; + refreshRate = 75; + timer: any; + toggleClassName = this.isFullScreen ? 'icon mdi mdi-arrow-collapse' : 'icon mdi mdi-arrow-expand'; + sgb: SlickVanillaGridBundle; + + attached() { + // define the grid options & columns and then create the grid itself + this.defineGrid(); + + // mock some data (different in each dataset) + this.dataset = this.getData(NB_ITEMS); + this.sgb = new Slicker.GridBundle(document.querySelector(`.grid18`), this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset); + + setTimeout(() => { + this.startSimulation(); + }, this.refreshRate); + } + + dispose() { + this.stopSimulation(); + this.sgb?.dispose(); + } + + /* Define grid Options and Columns */ + defineGrid() { + // the columns field property is type-safe, try to add a different string not representing one of DataItems properties + this.columnDefinitions = [ + { + id: 'currency', name: 'Currency', field: 'currency', filterable: true, sortable: true, minWidth: 65, width: 65, + formatter: currencyFormatter, + filter: { + model: Filters.singleSelect, + collection: [{ label: '', value: '' }, { label: 'CAD', value: 'CAD' }, { label: 'USD', value: 'USD' }] + }, + grouping: { + getter: 'currency', + formatter: (g) => `Currency: ${g.value} (${g.count} items)`, + aggregators: [ + new Aggregators.Sum('amount') + ], + aggregateCollapsed: true, + collapsed: false + } + }, + { id: 'symbol', name: 'Symbol', field: 'symbol', filterable: true, sortable: true, minWidth: 65, width: 65 }, + { + id: 'market', name: 'Market', field: 'market', filterable: true, sortable: true, minWidth: 75, width: 75, + grouping: { + getter: 'market', + formatter: (g) => `Market: ${g.value} (${g.count} items)`, + aggregators: [ + new Aggregators.Sum('amount') + ], + aggregateCollapsed: true, + collapsed: false + } + }, + { id: 'company', name: 'Company', field: 'company', filterable: true, sortable: true, minWidth: 80, width: 130 }, + { + id: 'trsnType', name: 'Type', field: 'trsnType', filterable: true, sortable: true, minWidth: 60, width: 60, + formatter: transactionTypeFormatter, + filter: { + model: Filters.singleSelect, + collection: [{ label: '', value: '' }, { label: 'Buy', value: 'Buy' }, { label: 'Sell', value: 'Sell' }] + }, + grouping: { + getter: 'trsnType', + formatter: (g) => `Type: ${g.value} (${g.count} items)`, + aggregators: [ + new Aggregators.Sum('amount') + ], + aggregateCollapsed: true, + collapsed: false + } + }, + { + id: 'priceChange', name: 'Change', field: 'priceChange', filterable: true, sortable: true, minWidth: 80, width: 80, + filter: { model: Filters.compoundInputNumber }, type: FieldType.number, + formatter: Formatters.multiple, + params: { + formatters: [Formatters.dollarColored, priceFormatter], + maxDecimal: 2, + } + + }, + { + id: 'price', name: 'Price', field: 'price', filterable: true, sortable: true, minWidth: 70, width: 70, + filter: { model: Filters.compoundInputNumber }, type: FieldType.number, + formatter: Formatters.dollar, params: { maxDecimal: 2 } + }, + { + id: 'quantity', name: 'Quantity', field: 'quantity', filterable: true, sortable: true, minWidth: 70, width: 70, + filter: { model: Filters.compoundInputNumber }, type: FieldType.number, + }, + { + id: 'amount', name: 'Amount', field: 'amount', filterable: true, sortable: true, minWidth: 70, width: 60, + filter: { model: Filters.compoundInputNumber }, type: FieldType.number, + formatter: Formatters.dollar, params: { maxDecimal: 2 }, + groupTotalsFormatter: GroupTotalFormatters.sumTotalsDollarBold, + }, + { id: 'historic', name: 'Price History', field: 'historic', minWidth: 100, width: 150, maxWidth: 150, formatter: historicSparklineFormatter }, + { + id: 'execution', name: 'Execution Timestamp', field: 'execution', filterable: true, sortable: true, minWidth: 125, + formatter: Formatters.dateTimeIsoAmPm, exportWithFormatter: true, + type: FieldType.dateTimeIsoAM_PM, filter: { model: Filters.compoundDate } + }, + ]; + + this.gridOptions = { + autoResize: { + container: '.trading-platform', + rightPadding: 0, + bottomPadding: 20, + }, + formatterOptions: { + displayNegativeNumberWithParentheses: true, + thousandSeparator: ',' + }, + draggableGrouping: { + dropPlaceHolderText: 'Drop a column header here to group by any of these available columns: Currency, Market or Type', + deleteIconCssClass: 'mdi mdi-close color-danger', + }, + enableDraggableGrouping: true, + createPreHeaderPanel: true, + showPreHeaderPanel: true, + preHeaderPanelHeight: 40, + enableCellNavigation: true, + enableFiltering: true, + cellHighlightCssClass: 'changed', + rowHeight: 40 + }; + } + + getData(itemCount: number) { + // mock a dataset + const datasetTmp = []; + for (let i = 0; i < itemCount; i++) { + const randomPercent = Math.round(Math.random() * 100); + const randomLowQty = this.randomNumber(1, 50); + const randomHighQty = this.randomNumber(125, 255); + const priceChange = this.randomNumber(-25, 35, false); + const price = this.randomNumber(priceChange, 300); + const quantity = price < 5 ? randomHighQty : randomLowQty; + const amount = price * quantity; + const now = new Date(); + now.setHours(9, 30, 0); + const currency = (Math.floor(Math.random() * 10)) % 2 ? 'CAD' : 'USD'; + const company = Faker.company.companyName(); + + datasetTmp[i] = { + id: i, + currency, + trsnType: (Math.round(Math.random() * 100)) % 2 ? 'Buy' : 'Sell', + company, + symbol: currency === 'CAD' ? company.substr(0, 3).toUpperCase() + '.TO' : company.substr(0, 4).toUpperCase(), + market: currency === 'CAD' ? 'TSX' : price > 200 ? 'Nasdaq' : 'S&P 500', + duration: (i % 33 === 0) ? null : Math.random() * 100 + '', + percentCompleteNumber: randomPercent, + priceChange, + price, + quantity, + amount, + execution: now, + historic: [price] + }; + } + return datasetTmp; + } + + startSimulation() { + const changes: any = {}; + const numberOfUpdates = this.randomNumber(this.minChangePerCycle, this.maxChangePerCycle); + + for (let i = 0; i < numberOfUpdates; i++) { + const randomLowQty = this.randomNumber(1, 50); + const randomHighQty = this.randomNumber(125, 255); + const rowNumber = Math.round(Math.random() * (this.dataset.length - 1)); + const priceChange = this.randomNumber(-25, 25, false); + const prevItem = deepCopy(this.dataset[rowNumber]); + const itemTmp = { ...this.dataset[rowNumber] }; + itemTmp.priceChange = priceChange; + itemTmp.price = ((itemTmp.price + priceChange) < 0) ? 0 : itemTmp.price + priceChange; + itemTmp.quantity = itemTmp.price < 5 ? randomHighQty : randomLowQty; + itemTmp.amount = itemTmp.price * itemTmp.quantity; + itemTmp.trsnType = (Math.round(Math.random() * 100)) % 2 ? 'Buy' : 'Sell'; + itemTmp.execution = new Date(); + itemTmp.historic.push(itemTmp.price); + itemTmp.historic = itemTmp.historic.slice(-20); // keep a max of X historic values + + if (!changes[rowNumber]) { + changes[rowNumber] = {}; + } + + // highlight whichever cell is being changed + changes[rowNumber]['id'] = 'changed'; + this.renderCellHighlighting(itemTmp, this.findColumnById('priceChange'), priceChange); + if ((prevItem.priceChange < 0 && itemTmp.priceChange > 0) || (prevItem.priceChange > 0 && itemTmp.priceChange < 0)) { + this.renderCellHighlighting(itemTmp, this.findColumnById('price'), priceChange); + } + // if (prevItem.trsnType !== itemTmp.trsnType) { + // this.renderCellHighlighting(itemTmp, this.findColumnById('trsnType'), priceChange); + // } + + // update the data + this.sgb.dataView.updateItem(itemTmp.id, itemTmp); + // NOTE: we should also invalidate/render the row after updating cell data to see the new data rendered in the UI + // but the cell highlight actually does that for us so we can skip it + } + + this.timer = setTimeout(this.startSimulation.bind(this), this.refreshRate || 0); + } + + stopSimulation() { + clearTimeout(this.timer); + } + + findColumnById(columnName: string): Column { + return this.columnDefinitions.find(col => col.field === columnName) as Column; + } + + renderCellHighlighting(item: any, column: Column, priceChange: number) { + if (item && column) { + const row = this.sgb.dataView.getRowByItem(item) as number; + if (row >= 0) { + const hash = { [row]: { [column.id]: priceChange >= 0 ? 'changed-gain' : 'changed-loss' } }; + this.sgb.slickGrid.setCellCssStyles(`highlight_${[column.id]}${row}`, hash); + + // remove highlight after x amount of time + setTimeout(() => this.removeUnsavedStylingFromCell(item, column, row), this.highlightDuration); + } + } + } + + /** remove change highlight css class from that cell */ + removeUnsavedStylingFromCell(_item: any, column: Column, row: number) { + this.sgb.slickGrid.removeCellCssStyles(`highlight_${[column.id]}${row}`); + } + + toggleFullScreen() { + const container = document.querySelector('.trading-platform'); + if (container?.classList.contains('full-screen')) { + container.classList.remove('full-screen'); + this.isFullScreen = false; + } else if (container) { + container.classList.add('full-screen'); + this.isFullScreen = true; + } + this.sgb.resizerService.resizeGrid(); + } + + private randomNumber(min: number, max: number, floor = true) { + const number = Math.random() * (max - min + 1) + min; + return floor ? Math.floor(number) : number; + } +} diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/icons.ts b/examples/webpack-demo-vanilla-bundle/src/examples/icons.ts index 4b0b36143..f8c60ed91 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/icons.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/icons.ts @@ -176,6 +176,8 @@ export class Icons { '.mdi.mdi-message-text-outline', '.mdi.mdi-microsoft-excel', '.mdi.mdi-minus', + '.mdi.mdi-minus-circle', + '.mdi.mdi-minus-circle-outline', '.mdi.mdi-order-bool-ascending-variant', '.mdi.mdi-page-first', '.mdi.mdi-page-last', @@ -188,9 +190,12 @@ export class Icons { '.mdi.mdi-percent-outline', '.mdi.mdi-pin-off-outline', '.mdi.mdi-pin-outline', + '.mdi.mdi-play-circle-outline', '.mdi.mdi-playlist-plus', '.mdi.mdi-playlist-remove', '.mdi.mdi-plus', + '.mdi.mdi-plus-circle', + '.mdi.mdi-plus-circle-outline', '.mdi.mdi-progress-download', '.mdi.mdi-redo', '.mdi.mdi-refresh', @@ -200,6 +205,7 @@ export class Icons { '.mdi.mdi-sort-descending', '.mdi.mdi-sort-variant-remove', '.mdi.mdi-square-edit-outline', + '.mdi.mdi-stop-circle-outline', '.mdi.mdi-subdirectory-arrow-right', '.mdi.mdi-swap-horizontal', '.mdi.mdi-swap-vertical', diff --git a/examples/webpack-demo-vanilla-bundle/src/renderer.ts b/examples/webpack-demo-vanilla-bundle/src/renderer.ts index d87763972..3bd3214c2 100644 --- a/examples/webpack-demo-vanilla-bundle/src/renderer.ts +++ b/examples/webpack-demo-vanilla-bundle/src/renderer.ts @@ -118,6 +118,8 @@ export class Renderer { observer.bind(elements, attribute, 'change').bind(elements, attribute, 'keyup'); break; case 'checked': + case 'min': + case 'max': default: observer.bind(elements, attribute, 'change'); break; diff --git a/packages/common/src/styles/material-svg-icons.scss b/packages/common/src/styles/material-svg-icons.scss index f7d7628bb..29ba1cb18 100644 --- a/packages/common/src/styles/material-svg-icons.scss +++ b/packages/common/src/styles/material-svg-icons.scss @@ -756,6 +756,16 @@ $slick-icon-height: $slick-icon-width; "M19,13H5V11H19V13Z", encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block); +@include loadsvg( + ".mdi.mdi-minus-circle", + "M17,13H7V11H17M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z", + encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block); + +@include loadsvg( + ".mdi.mdi-minus-circle-outline", + "M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M7,13H17V11H7", + encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block); + @include loadsvg( ".mdi.mdi-order-bool-ascending-variant", "M4 13C2.89 13 2 13.89 2 15V19C2 20.11 2.89 21 4 21H8C9.11 21 10 20.11 10 19V15C10 13.89 9.11 13 8 13M8.2 14.5L9.26 15.55L5.27 19.5L2.74 16.95L3.81 15.9L5.28 17.39M4 3C2.89 3 2 3.89 2 5V9C2 10.11 2.89 11 4 11H8C9.11 11 10 10.11 10 9V5C10 3.89 9.11 3 8 3M4 5H8V9H4M12 5H22V7H12M12 19V17H22V19M12 11H22V13H12Z", @@ -821,11 +831,26 @@ $slick-icon-height: $slick-icon-width; "M2,16H10V14H2M18,14V10H16V14H12V16H16V20H18V16H22V14M14,6H2V8H14M14,10H2V12H14V10Z", encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block); +@include loadsvg( + ".mdi.mdi-play-circle-outline", + "M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M10,16.5L16,12L10,7.5V16.5Z", + encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block); + @include loadsvg( ".mdi.mdi-playlist-remove", "M2,6V8H14V6H2M2,10V12H11V10H2M14.17,10.76L12.76,12.17L15.59,15L12.76,17.83L14.17,19.24L17,16.41L19.83,19.24L21.24,17.83L18.41,15L21.24,12.17L19.83,10.76L17,13.59L14.17,10.76M2,14V16H11V14H2Z", encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block); +@include loadsvg( + ".mdi.mdi-plus-circle", + "M17,13H13V17H11V13H7V11H11V7H13V11H17M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z", + encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block); + +@include loadsvg( + ".mdi.mdi-plus-circle-outline", + "M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z", + encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block); + @include loadsvg( ".mdi.mdi-plus", "M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", @@ -876,6 +901,11 @@ $slick-icon-height: $slick-icon-width; "M5,3C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19H5V5H12V3H5M17.78,4C17.61,4 17.43,4.07 17.3,4.2L16.08,5.41L18.58,7.91L19.8,6.7C20.06,6.44 20.06,6 19.8,5.75L18.25,4.2C18.12,4.07 17.95,4 17.78,4M15.37,6.12L8,13.5V16H10.5L17.87,8.62L15.37,6.12Z", encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block); +@include loadsvg( + ".mdi.mdi-stop-circle-outline", + "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M9,9V15H15V9", + encodecolor($slick-icon-color), $slick-icon-height, $slick-icon-width, inline-block); + @include loadsvg( ".mdi.mdi-subdirectory-arrow-right", "M19,15L13,21L11.58,19.58L15.17,16H4V4H6V14H15.17L11.58,10.42L13,9L19,15Z", diff --git a/test/cypress/integration/example18.spec.js b/test/cypress/integration/example18.spec.js new file mode 100644 index 000000000..6c4f95e45 --- /dev/null +++ b/test/cypress/integration/example18.spec.js @@ -0,0 +1,69 @@ +/// + +describe('Example 18 - Real-Time Trading Platform', { retries: 1 }, () => { + const titles = ['Currency', 'Symbol', 'Market', 'Company', 'Type', 'Change', 'Price', 'Quantity', 'Amount', 'Price History', 'Execution Timestamp']; + const GRID_ROW_HEIGHT = 40; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseExampleUrl')}/example18`); + cy.get('h3').should('contain', 'Example 18 - Real-Time Trading Platform'); + }); + + it('should have exact column titles on 1st grid', () => { + cy.get('.grid18') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(titles[index])); + }); + + it('should check first 5 rows and expect certain data', () => { + for (let i = 0; i < 5; i++) { + cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(0)`).contains(/CAD|USD$/); + cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(4)`).contains(/Buy|Sell$/); + cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(5)`).contains(/\$\(?[0-9\.]*\)?/); + cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(6)`).contains(/\$[0-9\.]*/); + cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(7)`).contains(/\d$/); + cy.get(`[style="top:${GRID_ROW_HEIGHT * i}px"] > .slick-cell:nth(8)`).contains(/\$[0-9\.]*/); + } + }); + + it('should find multiple green & pink backgrounds to show gains & losses when in real-time mode', () => { + cy.get('#refreshRateRange').invoke('val', 5).trigger('change'); + + cy.get('.changed-gain').should('have.length.gt', 2); + cy.get('.changed-loss').should('have.length.gt', 2); + }); + + it('should NOT find any green neither pink backgrounds when in real-time is stopped', () => { + cy.get('[data-test="highlight-input"]').type('{backspace}{backspace}'); + cy.get('[data-test="stop-btn"]').click(); + + cy.wait(5); + cy.get('.changed-gain').should('have.length', 0); + cy.get('.changed-loss').should('have.length', 0); + cy.wait(1); + cy.get('.changed-gain').should('have.length', 0); + cy.get('.changed-loss').should('have.length', 0); + }); + + it('should Group by 1st column "Currency" and expect 2 groups with Totals when collapsed', () => { + cy.get('.slick-column-name') + .first() + .trigger('mousedown', { button: 1, force: true }) + + cy.get('.slick-draggable-dropbox-toggle-placeholder') + .trigger('mousemove', 'center') + .trigger('mouseup', 'center', { force: true }); + + cy.get('.slick-group-toggle-all') + .click(); + + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Currency: CAD'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(8)`).contains(/\$[0-9\,\.]*/); + + cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(0) .slick-group-toggle.collapsed`).should('have.length', 1); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(0) .slick-group-title`).should('contain', 'Currency: USD'); + cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(8)`).contains(/\$[0-9\,\.]*/); + }); +}); diff --git a/yarn.lock b/yarn.lock index fb3ff067e..581c3f8ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -576,6 +576,11 @@ unique-filename "^1.1.1" which "^1.3.1" +"@fnando/sparkline@^0.3.10": + version "0.3.10" + resolved "https://registry.yarnpkg.com/@fnando/sparkline/-/sparkline-0.3.10.tgz#0cb6549a232af0f19f75b33d38fddd4f5ed9f086" + integrity sha512-Rwz2swatdSU5F4sCOvYG8EOWdjtLgq5d8nmnqlZ3PXdWJI9Zq9BRUvJ/9ygjajJG8qOyNpMFX3GEVFjZIuB1Jg== + "@humanwhocodes/config-array@^0.9.2": version "0.9.2" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914" @@ -1834,6 +1839,16 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/faker@^5.5.9": + version "5.5.9" + resolved "https://registry.yarnpkg.com/@types/faker/-/faker-5.5.9.tgz#588ede92186dc557bff8341d294335d50d255f0c" + integrity sha512-uCx6mP3UY5SIO14XlspxsGjgaemrxpssJI0Ol+GfhxtcKpv9pgRZYsS4eeKeHVLje6Qtc8lGszuBI461+gVZBA== + +"@types/fnando__sparkline@^0.3.4": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@types/fnando__sparkline/-/fnando__sparkline-0.3.4.tgz#6a4a1a57983c1ecabf782abfb29ce864a9bbbf16" + integrity sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ== + "@types/glob@5.0.30": version "5.0.30" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.30.tgz#1026409c5625a8689074602808d082b2867b8a51" @@ -5300,6 +5315,11 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +faker@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e" + integrity sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"