How to use getCellsText method in Cucumber-gherkin

Best JavaScript code snippet using cucumber-gherkin

Reactable.v2.test.js

Source:Reactable.v2.test.js Github

copy

Full Screen

...697 { name: 'x[0].b', accessor: 'x[0].b' }698 ]699 }700 const { container } = render(<Reactable {...props} />)701 expect(getCellsText(container)).toEqual(['1', 'a', 'b'])702 })703 it('cell formatting', () => {704 const props = {705 data: { a: [1, 2], b: ['a', 'b'] },706 columns: [707 {708 name: 'colA',709 accessor: 'a',710 format: { cell: { prefix: 'cell__', suffix: '__@' }, aggregated: { prefix: 'agg' } },711 cell: cellInfo => `${cellInfo.value}-a`,712 className: 'col-a'713 },714 { name: 'colB', accessor: 'b' }715 ]716 }717 const { container } = render(<Reactable {...props} />)718 expect(getCellsText(container, '.col-a')).toEqual(['cell__1__@-a', 'cell__2__@-a'])719 })720 it('applies cell classes and styles', () => {721 const props = {722 data: { a: [1, 2], b: ['a', 'b'] },723 columns: [724 {725 name: 'colA',726 accessor: 'a',727 className: 'my-cell',728 style: { backgroundColor: 'red' }729 },730 { name: 'colB', accessor: 'b' }731 ]732 }733 const { container } = render(<Reactable {...props} />)734 const cells = getCells(container)735 expect(cells[0]).toHaveClass('my-cell')736 expect(cells[0]).toHaveStyle('background-color: red;')737 expect(cells[2]).toHaveClass('my-cell')738 expect(cells[2]).toHaveStyle('background-color: red;')739 })740 it('applies cell classes and styles from JS functions', () => {741 const assertProps = (rowInfo, colInfo, state) => {742 expect(rowInfo.index >= 0).toEqual(true)743 expect(rowInfo.viewIndex >= 0).toEqual(true)744 expect(rowInfo.level).toEqual(0)745 expect(rowInfo.aggregated).toBeFalsy()746 expect(rowInfo.expanded).toBeFalsy()747 expect(rowInfo.selected).toEqual(false)748 expect(rowInfo.subRows).toEqual([])749 expect(rowInfo.values.a).toEqual(['cellA', 'cellB'][rowInfo.index])750 expect(rowInfo.row.a).toEqual(['cellA', 'cellB'][rowInfo.index])751 expect(colInfo.id).toEqual('a')752 expect(colInfo.name).toEqual('colA')753 expect(state.page).toEqual(0)754 expect(state.pageSize).toEqual(7)755 expect(state.pages).toEqual(1)756 expect(state.sorted).toEqual([{ id: 'a', desc: false }])757 expect(state.groupBy).toEqual([])758 expect(state.filters).toEqual([])759 expect(state.searchValue).toEqual(undefined)760 expect(state.selected).toEqual([])761 expect(state.pageRows).toEqual([{ a: 'cellA' }, { a: 'cellB' }])762 expect(state.sortedData).toEqual([{ a: 'cellA' }, { a: 'cellB' }])763 expect(state.data).toEqual([{ a: 'cellA' }, { a: 'cellB' }])764 }765 const props = {766 data: { a: ['cellA', 'cellB'] },767 columns: [768 {769 name: 'colA',770 accessor: 'a',771 className: (rowInfo, colInfo, state) => {772 assertProps(rowInfo, colInfo, state)773 if (rowInfo.index === 0 && colInfo.id === 'a' && state.page === 0) {774 return 'my-cell'775 }776 },777 style: (rowInfo, colInfo, state) => {778 assertProps(rowInfo, colInfo, state)779 if (rowInfo.index === 0 && colInfo.id === 'a' && state.page === 0) {780 return { backgroundColor: 'red' }781 }782 }783 }784 ],785 defaultSorted: [{ id: 'a', desc: false }],786 defaultPageSize: 7787 }788 const { container } = render(<Reactable {...props} />)789 const [cellA, cellB] = getCells(container)790 expect(cellA).toHaveClass('my-cell')791 expect(cellA).toHaveStyle('background-color: red;')792 expect(cellB).not.toHaveClass('my-cell')793 expect(cellB).not.toHaveStyle('background-color: red;')794 })795 it('applies classes and styles from R functions', () => {796 const props = {797 data: { a: ['cellA', 'cellB'] },798 columns: [799 {800 name: 'colA',801 accessor: 'a',802 className: ['my-cell', null],803 style: [{ backgroundColor: 'red' }, null]804 }805 ]806 }807 const { container } = render(<Reactable {...props} />)808 const [cellA, cellB] = getCells(container)809 expect(cellA).toHaveClass('my-cell')810 expect(cellA).toHaveStyle('background-color: red;')811 expect(cellB).not.toHaveClass('my-cell')812 expect(cellB).not.toHaveStyle('background-color: red;')813 })814 it('cell alignment', () => {815 const props = {816 data: { a: ['a'], b: [1], c: [3], d: [5], e: [8] },817 columns: [818 { name: 'default', accessor: 'a', className: 'default' },819 { name: 'default-num', accessor: 'b', type: 'numeric', className: 'default-num' },820 { name: 'left', accessor: 'c', align: 'left', className: 'left' },821 { name: 'right', accessor: 'd', align: 'right', className: 'right' },822 { name: 'center', accessor: 'e', align: 'center', className: 'center' }823 ]824 }825 const { container } = render(<Reactable {...props} />)826 expect(getCells(container, '.default')[0]).toHaveClass('rt-align-left')827 expect(getCells(container, '.default-num')[0]).toHaveClass('rt-align-right')828 expect(getCells(container, '.left')[0]).toHaveClass('rt-align-left')829 expect(getCells(container, '.right')[0]).toHaveClass('rt-align-right')830 expect(getCells(container, '.center')[0]).toHaveClass('rt-align-center')831 })832 it('cell vertical alignment', () => {833 const props = {834 data: { a: ['a'], b: [1], c: [3], d: [5] },835 columns: [836 { name: 'default', accessor: 'a', className: 'default' },837 { name: 'top', accessor: 'b', vAlign: 'top', className: 'top' },838 { name: 'center', accessor: 'c', vAlign: 'center', className: 'center' },839 { name: 'bottom', accessor: 'd', vAlign: 'bottom', className: 'bottom' }840 ]841 }842 const { container } = render(<Reactable {...props} />)843 expect(getCells(container, '.default')[0]).not.toHaveClass('rt-valign-center')844 expect(getCells(container, '.default')[0]).not.toHaveClass('rt-valign-bottom')845 expect(getCells(container, '.top')[0]).not.toHaveClass('rt-valign-center')846 expect(getCells(container, '.top')[0]).not.toHaveClass('rt-valign-bottom')847 expect(getCells(container, '.center')[0]).toHaveClass('rt-valign-center')848 expect(getCells(container, '.bottom')[0]).toHaveClass('rt-valign-bottom')849 })850 it('cells rerender without unmounting', () => {851 const props = {852 data: { a: [1, 2, 3], b: ['a', 'b', 'c'] },853 columns: [854 { name: 'colA', accessor: 'a', className: 'col-a' },855 {856 name: 'colB',857 accessor: 'b',858 cell: cellInfo => `${cellInfo.index}-${cellInfo.value}`,859 className: 'col-b'860 }861 ],862 defaultPageSize: 2863 }864 const { container } = render(<Reactable {...props} />)865 const cellsA = getCells(container, '.col-a')866 expect(cellsA[0].textContent).toEqual('1')867 expect(cellsA[1].textContent).toEqual('2')868 const cellsB = getCells(container, '.col-b')869 expect(cellsB[0].textContent).toEqual('0-a')870 expect(cellsB[1].textContent).toEqual('1-b')871 fireEvent.click(getNextButton(container))872 expect(cellsA[0].textContent).toEqual('3')873 expect(cellsB[0].textContent).toEqual('2-c')874 })875})876describe('headers', () => {877 it('renders headers', () => {878 const props = {879 data: { a: [1, 2], b: [3, 4], c: ['a', 'b'], d: ['c', 'd'] },880 columns: [881 {882 name: 'colA',883 accessor: 'a',884 headerClassName: 'my-header',885 headerStyle: { color: 'red' }886 },887 // Custom header should override name888 {889 name: 'colB',890 header: 'my-header',891 accessor: 'b'892 },893 // Custom header should override name894 {895 name: 'colC',896 header: '',897 accessor: 'c'898 },899 // Empty column header900 {901 name: '',902 accessor: 'd'903 }904 ]905 }906 const { container } = render(<Reactable {...props} />)907 const thead = getThead(container)908 expect(thead).toHaveAttribute('role', 'rowgroup')909 const headerRows = getHeaderRows(container)910 expect(headerRows).toHaveLength(1)911 expect(headerRows[0]).toHaveAttribute('role', 'row')912 const headers = getHeaders(container)913 expect(headers).toHaveLength(4)914 expect(headers[0].textContent).toEqual('colA')915 expect(headers[0]).toHaveClass('my-header')916 expect(headers[0]).toHaveStyle('color: red;')917 expect(headers[1].textContent).toEqual('my-header')918 expect(headers[2].textContent).toEqual('')919 expect(headers[3].textContent).toEqual('')920 headers.forEach(header => expect(header).toHaveAttribute('role', 'columnheader'))921 headers.forEach(header => expect(header).not.toHaveAttribute('aria-colspan'))922 // Should not have colspan attribute (from react-table)923 headers.forEach(header => expect(header).not.toHaveAttribute('colspan'))924 })925 it('header render function', () => {926 const assertProps = (colInfo, state) => {927 const { column, data } = colInfo928 expect(column.id).toEqual('a')929 expect(column.name).toEqual('colA')930 expect(column.filterValue).toEqual(undefined)931 expect(data).toEqual([932 { a: 1, b: 'a', c: true },933 { a: 2, b: 'b', c: false }934 ])935 expect(state.page).toEqual(0)936 expect(state.pageSize).toEqual(10)937 expect(state.pages).toEqual(1)938 expect(state.sorted).toEqual([])939 expect(state.groupBy).toEqual([])940 expect(state.filters).toEqual([])941 expect(state.searchValue).toEqual(undefined)942 expect(state.selected).toEqual([])943 expect(state.pageRows).toEqual([944 { a: 1, b: 'a', c: true },945 { a: 2, b: 'b', c: false }946 ])947 expect(state.sortedData).toEqual([948 { a: 1, b: 'a', c: true },949 { a: 2, b: 'b', c: false }950 ])951 expect(state.data).toEqual([952 { a: 1, b: 'a', c: true },953 { a: 2, b: 'b', c: false }954 ])955 }956 const props = {957 data: { a: [1, 2], b: ['a', 'b'], c: [true, false] },958 columns: [959 {960 name: 'colA',961 accessor: 'a',962 header: (colInfo, state) => {963 assertProps(colInfo, state)964 const { column, data } = colInfo965 return (966 `<span>${column.name}</span> ` +967 `<span>(${data.length} ${data[0].a} ${data[1].a})</span>`968 )969 },970 html: true971 },972 { name: 'colB', accessor: 'b', header: () => '' },973 { name: 'colC', accessor: 'c' }974 ]975 }976 const { container } = render(<Reactable {...props} />)977 const headers = getHeaders(container)978 expect(headers[0].textContent).toEqual('colA (2 1 2)')979 expect(headers[1].textContent).toEqual('')980 expect(headers[2].textContent).toEqual('colC')981 })982 it('header alignment', () => {983 const props = {984 data: { a: ['a'], b: [1], c: [3], d: [5], e: [8] },985 columns: [986 { name: 'default', accessor: 'a', headerClassName: 'default' },987 { name: 'default-num', accessor: 'b', type: 'numeric', headerClassName: 'default-num' },988 { name: 'left', accessor: 'c', align: 'left', headerClassName: 'left' },989 { name: 'right', accessor: 'd', align: 'right', headerClassName: 'right' },990 { name: 'center', accessor: 'e', align: 'center', headerClassName: 'center' }991 ]992 }993 const { container } = render(<Reactable {...props} />)994 expect(getHeaders(container, '.default')[0]).toHaveClass('rt-align-left')995 expect(getHeaders(container, '.default-num')[0]).toHaveClass('rt-align-right')996 expect(getHeaders(container, '.left')[0]).toHaveClass('rt-align-left')997 expect(getHeaders(container, '.right')[0]).toHaveClass('rt-align-right')998 expect(getHeaders(container, '.center')[0]).toHaveClass('rt-align-center')999 })1000 it('header vertical alignment', () => {1001 const props = {1002 data: { a: ['a'], b: [1], c: [3], d: [5] },1003 columns: [1004 { name: 'default', accessor: 'a', headerClassName: 'default' },1005 { name: 'top', accessor: 'b', headerVAlign: 'top', headerClassName: 'top' },1006 { name: 'center', accessor: 'c', headerVAlign: 'center', headerClassName: 'center' },1007 { name: 'bottom', accessor: 'd', headerVAlign: 'bottom', headerClassName: 'bottom' }1008 ]1009 }1010 const { container } = render(<Reactable {...props} />)1011 expect(getHeaders(container, '.default')[0]).not.toHaveClass('rt-valign-center')1012 expect(getHeaders(container, '.default')[0]).not.toHaveClass('rt-valign-bottom')1013 expect(getHeaders(container, '.top')[0]).not.toHaveClass('rt-valign-center')1014 expect(getHeaders(container, '.top')[0]).not.toHaveClass('rt-valign-bottom')1015 expect(getHeaders(container, '.center')[0]).toHaveClass('rt-valign-center')1016 expect(getHeaders(container, '.bottom')[0]).toHaveClass('rt-valign-bottom')1017 })1018})1019describe('column groups', () => {1020 it('renders column groups', () => {1021 const props = {1022 data: { a: [1, 2], b: [3, 4], c: ['a', 'b'] },1023 columns: [1024 { name: 'colA', accessor: 'a' },1025 { name: 'colB', accessor: 'b' },1026 { name: 'colC', accessor: 'c' }1027 ],1028 columnGroups: [1029 {1030 columns: ['a', 'b'],1031 name: 'group-1',1032 headerClassName: 'my-header',1033 headerStyle: { color: 'red' }1034 },1035 {1036 columns: ['c'],1037 name: 'group-2'1038 }1039 ]1040 }1041 const { container } = render(<Reactable {...props} />)1042 const headerRows = getHeaderRows(container)1043 expect(headerRows).toHaveLength(2)1044 headerRows.forEach(row => expect(row).toHaveAttribute('role', 'row'))1045 const [groupHeaderRow, headerRow] = headerRows1046 expect(groupHeaderRow).toHaveClass('rt-tr-group-header')1047 expect(headerRow).toHaveClass('rt-tr-header')1048 const groupHeaders = getGroupHeaders(groupHeaderRow)1049 expect(groupHeaders).toHaveLength(2)1050 expect(groupHeaders[0].textContent).toEqual('group-1')1051 expect(groupHeaders[1].textContent).toEqual('group-2')1052 expect(groupHeaders[0]).toHaveAttribute('aria-colspan', '2')1053 expect(groupHeaders[1]).toHaveAttribute('aria-colspan', '1')1054 expect(groupHeaders[0]).toHaveClass('my-header')1055 expect(groupHeaders[0]).toHaveStyle('color: red;')1056 // Should not have colspan attribute (from react-table)1057 groupHeaders.forEach(header => expect(header).not.toHaveAttribute('colspan'))1058 })1059 it('renders ungrouped column headers', () => {1060 const props = {1061 data: { a: [1, 2], b: [3, 4], c: ['a', 'b'], d: ['c', 'd'] },1062 columns: [1063 { name: 'colA', accessor: 'a' },1064 { name: 'colB', accessor: 'b' },1065 { name: 'colC', accessor: 'c' },1066 { name: 'colD', accessor: 'd' }1067 ],1068 columnGroups: [{ columns: ['c'], name: 'group-2' }]1069 }1070 const { container } = render(<Reactable {...props} />)1071 const headerRows = getHeaderRows(container)1072 expect(headerRows).toHaveLength(2)1073 headerRows.forEach(row => expect(row).toHaveAttribute('role', 'row'))1074 const [groupHeaderRow] = headerRows1075 expect(groupHeaderRow).toHaveClass('rt-tr-group-header')1076 const groupHeaders = getGroupHeaders(groupHeaderRow)1077 expect(groupHeaders).toHaveLength(1)1078 const ungroupedHeaders = getUngroupedHeaders(groupHeaderRow)1079 expect(ungroupedHeaders).toHaveLength(2)1080 const groupTheadHeaders = getHeaders(groupHeaderRow)1081 expect(groupTheadHeaders).toHaveLength(3)1082 // Group headers should be: ungrouped (2), grouped (1), ungrouped (1)1083 expect(groupTheadHeaders[0]).toEqual(ungroupedHeaders[0])1084 expect(groupTheadHeaders[1]).toEqual(groupHeaders[0])1085 expect(groupTheadHeaders[2]).toEqual(ungroupedHeaders[1])1086 expect(groupTheadHeaders[0].textContent).toEqual('\u200b')1087 expect(groupTheadHeaders[1].textContent).toEqual('group-2')1088 expect(groupTheadHeaders[2].textContent).toEqual('\u200b')1089 expect(groupTheadHeaders[0]).toHaveAttribute('aria-colspan', '2')1090 expect(groupTheadHeaders[1]).toHaveAttribute('aria-colspan', '1')1091 expect(groupTheadHeaders[2]).toHaveAttribute('aria-colspan', '1')1092 // Should not have colspan attribute (from react-table)1093 groupTheadHeaders.forEach(header => expect(header).not.toHaveAttribute('colspan'))1094 })1095 it('header render function', () => {1096 const assertProps = (colInfo, state) => {1097 const { column, data } = colInfo1098 expect(column.id).toEqual('group_0_0')1099 expect(column.name).toEqual('group-1')1100 expect(column.filterValue).toEqual(undefined)1101 expect(column.columns).toHaveLength(2)1102 expect(column.columns[0].id).toEqual('a')1103 expect(column.columns[1].id).toEqual('b')1104 expect(data).toEqual([1105 { a: 1, b: 'a', c: 'c' },1106 { a: 2, b: 'b', c: 'd' }1107 ])1108 expect(column.filterValue).toEqual(undefined)1109 expect(state.page).toEqual(0)1110 expect(state.pageSize).toEqual(10)1111 expect(state.pages).toEqual(1)1112 expect(state.sorted).toEqual([])1113 expect(state.groupBy).toEqual([])1114 expect(state.filters).toEqual([])1115 expect(state.searchValue).toEqual(undefined)1116 expect(state.selected).toEqual([])1117 expect(state.pageRows).toEqual([1118 { a: 1, b: 'a', c: 'c' },1119 { a: 2, b: 'b', c: 'd' }1120 ])1121 expect(state.sortedData).toEqual([1122 { a: 1, b: 'a', c: 'c' },1123 { a: 2, b: 'b', c: 'd' }1124 ])1125 expect(state.data).toEqual([1126 { a: 1, b: 'a', c: 'c' },1127 { a: 2, b: 'b', c: 'd' }1128 ])1129 }1130 const props = {1131 data: { a: [1, 2], b: ['a', 'b'], c: ['c', 'd'] },1132 columns: [1133 { name: 'col-a', accessor: 'a' },1134 { name: 'col-b', accessor: 'b' },1135 { name: 'col-c', accessor: 'c' }1136 ],1137 columnGroups: [1138 {1139 columns: ['a', 'b'],1140 name: 'group-1',1141 header: (colInfo, state) => {1142 assertProps(colInfo, state)1143 return `${colInfo.column.name} (${colInfo.column.columns.length} ${colInfo.data.length})`1144 }1145 },1146 {1147 columns: ['c'],1148 name: 'group-2',1149 header: () => '<span>group</span> <span>2</span>',1150 html: true1151 }1152 ]1153 }1154 const { container } = render(<Reactable {...props} />)1155 const headers = getGroupHeaders(container)1156 expect(headers).toHaveLength(2)1157 expect(headers[0].textContent).toEqual('group-1 (2 2)')1158 expect(headers[1].textContent).toEqual('group 2')1159 })1160 it('renders group headers with blank names', () => {1161 const props = {1162 data: { a: [1, 2], b: ['a', 'b'], c: ['c', 'd'] },1163 columns: [1164 { name: 'col-a', accessor: 'a' },1165 { name: 'col-b', accessor: 'b' },1166 { name: 'col-c', accessor: 'c' }1167 ],1168 columnGroups: [1169 {1170 columns: ['a'],1171 name: ''1172 },1173 {1174 columns: ['b'],1175 header: () => ''1176 },1177 {1178 columns: ['c'],1179 header: ''1180 }1181 ]1182 }1183 const { container } = render(<Reactable {...props} />)1184 const headers = getGroupHeaders(container)1185 expect(headers).toHaveLength(3)1186 expect(headers[0].textContent).toEqual('')1187 expect(headers[1].textContent).toEqual('')1188 expect(headers[2].textContent).toEqual('')1189 })1190 it('handles column groups with hidden columns', () => {1191 const props = {1192 data: { a: [1, 2], b: [3, 4], c: ['a', 'b'], d: ['c', 'd'] },1193 columns: [1194 { name: 'colA', accessor: 'a' },1195 { name: 'colB', accessor: 'b', show: false },1196 { name: 'colC', accessor: 'c', show: false },1197 { name: 'colD', accessor: 'd' }1198 ],1199 columnGroups: [1200 {1201 columns: ['a', 'b'],1202 name: 'group-1',1203 headerClassName: 'my-header',1204 headerStyle: { color: 'red' }1205 },1206 {1207 columns: ['c'],1208 name: 'group-2'1209 }1210 ]1211 }1212 const { container } = render(<Reactable {...props} />)1213 const groupHeaders = getGroupHeaders(container)1214 expect(groupHeaders).toHaveLength(1)1215 expect(groupHeaders[0].textContent).toEqual('group-1')1216 expect(groupHeaders[0]).toHaveAttribute('aria-colspan', '1')1217 const ungroupedHeaders = getUngroupedHeaders(container)1218 expect(ungroupedHeaders).toHaveLength(1)1219 })1220 it('ungrouped grouping columns do not have a default header', () => {1221 // In v6, ungrouped grouping columns had a default "Grouped" header1222 const props = {1223 data: { a: [1, 2], b: [3, 4], c: ['a', 'b'] },1224 columns: [1225 { name: 'colA', accessor: 'a' },1226 { name: 'colB', accessor: 'b' },1227 { name: 'colC', accessor: 'c' }1228 ],1229 columnGroups: [1230 {1231 columns: ['a', 'b'],1232 name: 'group-ab'1233 }1234 ],1235 pivotBy: ['c']1236 }1237 const { container } = render(<Reactable {...props} />)1238 const headers = getGroupHeaders(container)1239 expect(headers).toHaveLength(1)1240 expect(headers[0].textContent).toEqual('group-ab')1241 const ungroupedHeaders = getUngroupedHeaders(container)1242 expect(ungroupedHeaders).toHaveLength(1)1243 })1244 it('group header alignment', () => {1245 const props = {1246 data: { a: ['a'], b: [1], c: [3], d: [5] },1247 columns: [1248 { name: 'default', accessor: 'a' },1249 { name: 'left', accessor: 'b' },1250 { name: 'right', accessor: 'c' },1251 { name: 'center', accessor: 'd' }1252 ],1253 columnGroups: [1254 { name: 'default', columns: ['a'], headerClassName: 'default' },1255 { name: 'left', columns: ['b'], align: 'left', headerClassName: 'left' },1256 { name: 'right', columns: ['c'], align: 'right', headerClassName: 'right' },1257 { name: 'center', columns: ['d'], align: 'center', headerClassName: 'center' }1258 ]1259 }1260 const { container } = render(<Reactable {...props} />)1261 expect(getGroupHeaders(container, '.default')[0]).toHaveClass('rt-align-center')1262 expect(getGroupHeaders(container, '.left')[0]).toHaveClass('rt-align-left')1263 expect(getGroupHeaders(container, '.right')[0]).toHaveClass('rt-align-right')1264 expect(getGroupHeaders(container, '.center')[0]).toHaveClass('rt-align-center')1265 })1266 it('group header vertical alignment', () => {1267 const props = {1268 data: { a: ['a'], b: [1], c: [3], d: [5] },1269 columns: [1270 { name: 'default', accessor: 'a' },1271 { name: 'left', accessor: 'b' },1272 { name: 'right', accessor: 'c' },1273 { name: 'center', accessor: 'd' }1274 ],1275 columnGroups: [1276 { name: 'default', columns: ['a'], accessor: 'a', headerClassName: 'default' },1277 { name: 'top', columns: ['b'], accessor: 'b', headerVAlign: 'top', headerClassName: 'top' },1278 {1279 name: 'center',1280 columns: ['c'],1281 accessor: 'c',1282 headerVAlign: 'center',1283 headerClassName: 'center'1284 },1285 {1286 name: 'bottom',1287 columns: ['d'],1288 accessor: 'd',1289 headerVAlign: 'bottom',1290 headerClassName: 'bottom'1291 }1292 ]1293 }1294 const { container } = render(<Reactable {...props} />)1295 expect(getGroupHeaders(container, '.default')[0]).not.toHaveClass('rt-valign-center')1296 expect(getGroupHeaders(container, '.default')[0]).not.toHaveClass('rt-valign-bottom')1297 expect(getGroupHeaders(container, '.top')[0]).not.toHaveClass('rt-valign-center')1298 expect(getGroupHeaders(container, '.top')[0]).not.toHaveClass('rt-valign-bottom')1299 expect(getGroupHeaders(container, '.center')[0]).toHaveClass('rt-valign-center')1300 expect(getGroupHeaders(container, '.bottom')[0]).toHaveClass('rt-valign-bottom')1301 })1302})1303describe('footers', () => {1304 it('does not render footers by default', () => {1305 const props = {1306 data: { a: [1, 2], b: [3, 4] },1307 columns: [1308 { name: 'a', accessor: 'a' },1309 { name: 'b', accessor: 'b' }1310 ]1311 }1312 const { container } = render(<Reactable {...props} />)1313 const footers = getFooters(container)1314 expect(footers).toHaveLength(0)1315 })1316 it('renders footers', () => {1317 const props = {1318 data: { a: [1, 2], b: [3, 4], c: ['a', 'b'] },1319 columns: [1320 {1321 name: 'a',1322 accessor: 'a',1323 footer: 'my-footer',1324 footerClassName: 'my-footer',1325 footerStyle: { color: 'red' }1326 },1327 {1328 name: 'b',1329 accessor: 'b',1330 footer: ''1331 },1332 { name: 'c', accessor: 'c' }1333 ]1334 }1335 const { container } = render(<Reactable {...props} />)1336 const tfoot = getTfoot(container)1337 expect(tfoot).toBeVisible()1338 expect(tfoot).toHaveAttribute('role', 'rowgroup')1339 const footerRow = getFooterRow(container)1340 expect(footerRow).toHaveAttribute('role', 'row')1341 const footers = getFooters(container)1342 expect(footers).toHaveLength(3)1343 expect(footers[0].textContent).toEqual('my-footer')1344 expect(footers[0]).toHaveClass('my-footer')1345 expect(footers[0]).toHaveStyle('color: red;')1346 expect(footers[1].textContent).toEqual('')1347 expect(footers[2].textContent).toEqual('\u200b')1348 footers.forEach(footer => expect(footer).toHaveAttribute('role', 'cell'))1349 // Should not have colspan attribute (from react-table)1350 footers.forEach(footer => expect(footer).not.toHaveAttribute('colspan'))1351 })1352 it('renders row headers', () => {1353 const props = {1354 data: {1355 a: [1, 2],1356 b: ['a', 'b']1357 },1358 columns: [1359 { name: 'a', accessor: 'a', footer: 'my-footer', rowHeader: true },1360 { name: 'b', accessor: 'b', footer: 'my-footer' }1361 ]1362 }1363 const { container } = render(<Reactable {...props} />)1364 const [footerA, footerB] = getFooters(container)1365 expect(footerA).toHaveAttribute('role', 'rowheader')1366 expect(footerB).toHaveAttribute('role', 'cell')1367 })1368 it('footer render function', () => {1369 const assertProps = (colInfo, state) => {1370 const { column, data } = colInfo1371 expect(column.id).toEqual('a')1372 expect(column.name).toEqual('colA')1373 expect(column.filterValue).toEqual(undefined)1374 expect(data).toEqual([1375 { a: 1, b: 'a', c: true },1376 { a: 2, b: 'b', c: false }1377 ])1378 expect(state.page).toEqual(0)1379 expect(state.pageSize).toEqual(10)1380 expect(state.pages).toEqual(1)1381 expect(state.sorted).toEqual([])1382 expect(state.groupBy).toEqual([])1383 expect(state.filters).toEqual([])1384 expect(state.searchValue).toEqual(undefined)1385 expect(state.selected).toEqual([])1386 expect(state.pageRows).toEqual([1387 { a: 1, b: 'a', c: true },1388 { a: 2, b: 'b', c: false }1389 ])1390 expect(state.sortedData).toEqual([1391 { a: 1, b: 'a', c: true },1392 { a: 2, b: 'b', c: false }1393 ])1394 expect(state.data).toEqual([1395 { a: 1, b: 'a', c: true },1396 { a: 2, b: 'b', c: false }1397 ])1398 }1399 const props = {1400 data: { a: [1, 2], b: ['a', 'b'], c: [true, false] },1401 columns: [1402 {1403 name: 'colA',1404 accessor: 'a',1405 footer: (colInfo, state) => {1406 assertProps(colInfo, state)1407 const { column, data } = colInfo1408 return (1409 `<span>${column.name}</span> ` +1410 `<span>(${data.length} ${data[0].a} ${data[1].a})</span>`1411 )1412 },1413 html: true1414 },1415 { name: 'colB', accessor: 'b', footer: () => '' },1416 { name: 'colC', accessor: 'c' }1417 ]1418 }1419 const { container } = render(<Reactable {...props} />)1420 const footers = getFooters(container)1421 expect(footers).toHaveLength(3)1422 expect(footers[0].textContent).toEqual('colA (2 1 2)')1423 expect(footers[1].textContent).toEqual('')1424 expect(footers[2].textContent).toEqual('\u200b')1425 })1426 it('footer alignment', () => {1427 const props = {1428 data: { a: ['a'], b: [1], c: [3], d: [5], e: [8] },1429 columns: [1430 { name: 'default', accessor: 'a', footer: '', footerClassName: 'default' },1431 { name: 'default-num', accessor: 'b', type: 'numeric', footerClassName: 'default-num' },1432 { name: 'left', accessor: 'c', align: 'left', footerClassName: 'left' },1433 { name: 'right', accessor: 'd', align: 'right', footerClassName: 'right' },1434 { name: 'center', accessor: 'e', align: 'center', footerClassName: 'center' }1435 ]1436 }1437 const { container } = render(<Reactable {...props} />)1438 expect(getFooters(container, '.default')[0]).toHaveClass('rt-align-left')1439 expect(getFooters(container, '.default-num')[0]).toHaveClass('rt-align-right')1440 expect(getFooters(container, '.left')[0]).toHaveClass('rt-align-left')1441 expect(getFooters(container, '.right')[0]).toHaveClass('rt-align-right')1442 expect(getFooters(container, '.center')[0]).toHaveClass('rt-align-center')1443 })1444 it('footer vertical alignment', () => {1445 const props = {1446 data: { a: ['a'], b: [1], c: [3], d: [5] },1447 columns: [1448 { name: 'default', accessor: 'a', footer: '', footerClassName: 'default' },1449 { name: 'top', accessor: 'b', vAlign: 'top', footerClassName: 'top' },1450 { name: 'center', accessor: 'c', vAlign: 'center', footerClassName: 'center' },1451 { name: 'bottom', accessor: 'd', vAlign: 'bottom', footerClassName: 'bottom' }1452 ]1453 }1454 const { container } = render(<Reactable {...props} />)1455 expect(getFooters(container, '.default')[0]).not.toHaveClass('rt-valign-center')1456 expect(getFooters(container, '.default')[0]).not.toHaveClass('rt-valign-bottom')1457 expect(getFooters(container, '.top')[0]).not.toHaveClass('rt-valign-center')1458 expect(getFooters(container, '.top')[0]).not.toHaveClass('rt-valign-bottom')1459 expect(getFooters(container, '.center')[0]).toHaveClass('rt-valign-center')1460 expect(getFooters(container, '.bottom')[0]).toHaveClass('rt-valign-bottom')1461 })1462 it('renders footers with column groups', () => {1463 const props = {1464 data: { a: [1, 2], b: [3, 4], c: ['a', 'b'] },1465 columns: [1466 { name: 'a', accessor: 'a', footer: 'my-footer' },1467 { name: 'b', accessor: 'b', footer: '' },1468 { name: 'c', accessor: 'c' }1469 ],1470 columnGroups: [1471 {1472 columns: ['a', 'b'],1473 name: 'group-1'1474 }1475 ]1476 }1477 const { container } = render(<Reactable {...props} />)1478 const footers = getFooters(container)1479 expect(footers).toHaveLength(3)1480 expect(footers[0].textContent).toEqual('my-footer')1481 expect(footers[1].textContent).toEqual('')1482 expect(footers[2].textContent).toEqual('\u200b')1483 })1484 it('renders footers with hidden columns', () => {1485 const props = {1486 data: { a: [1, 2], b: [3, 4], c: ['a', 'b'] },1487 columns: [1488 { name: 'a', accessor: 'a', footer: 'footer-a' },1489 { name: 'b', accessor: 'b', footer: '', show: false },1490 { name: 'c', accessor: 'c', footer: 'footer-c' }1491 ]1492 }1493 const { container } = render(<Reactable {...props} />)1494 const footers = getFooters(container)1495 expect(footers).toHaveLength(2)1496 expect(footers[0].textContent).toEqual('footer-a')1497 expect(footers[1].textContent).toEqual('footer-c')1498 })1499 it('does not apply cell classes and styles to footers', () => {1500 // Bug from v61501 const props = {1502 data: { a: [1, 2] },1503 columns: [1504 {1505 name: 'a',1506 accessor: 'a',1507 footer: 'my-footer',1508 className: 'cell',1509 style: { color: 'red' }1510 }1511 ]1512 }1513 const { container } = render(<Reactable {...props} />)1514 const footers = getFooters(container)1515 expect(footers).toHaveLength(1)1516 expect(footers[0]).not.toHaveClass('cell')1517 expect(footers[0]).not.toHaveStyle('color: red;')1518 })1519 it('does not render tfoot when there are no footers', () => {1520 const { container } = render(1521 <Reactable1522 data={{ a: [1, 2], b: ['aa', 'bb'], c: [true, false] }}1523 columns={[1524 { name: 'colA', accessor: 'a' },1525 { name: 'colB', accessor: 'b' },1526 { name: 'colC', accessor: 'c' }1527 ]}1528 />1529 )1530 const tfoot = getTfoot(container)1531 expect(tfoot).toEqual(null)1532 })1533})1534describe('hidden columns', () => {1535 it('some columns hidden', () => {1536 const props = {1537 data: { a: [1, 2], b: ['aaa', 'bbb'], c: [3, 4] },1538 columns: [1539 {1540 name: 'col-a',1541 accessor: 'a'1542 },1543 {1544 name: 'col-b',1545 accessor: 'b',1546 show: false1547 },1548 {1549 name: 'col-c',1550 accessor: 'c'1551 }1552 ]1553 }1554 const { container, queryByText } = render(<Reactable {...props} />)1555 const headers = getHeaders(container)1556 expect(headers).toHaveLength(2)1557 expect(queryByText('col-b')).toEqual(null)1558 expect(queryByText('bbb')).toEqual(null)1559 expect(queryByText('aaa')).toEqual(null)1560 })1561 it('all columns hidden', () => {1562 const props = {1563 data: { a: [1, 2], b: ['aaa', 'bbb'] },1564 columns: [1565 {1566 name: 'col-a',1567 accessor: 'a',1568 show: false1569 },1570 {1571 name: 'col-b',1572 accessor: 'b',1573 show: false1574 }1575 ]1576 }1577 const { container, queryByText } = render(<Reactable {...props} />)1578 const headers = getHeaders(container)1579 expect(headers).toHaveLength(0)1580 expect(queryByText('col-a')).toEqual(null)1581 expect(queryByText('col-b')).toEqual(null)1582 expect(getCells(container)).toHaveLength(0)1583 })1584 it('hidden column state updates when columns changes', () => {1585 const props = {1586 data: { a: [1, 2], b: ['a', 'b'] },1587 columns: [1588 { name: 'col-a', accessor: 'a', show: false },1589 { name: 'col-b', accessor: 'b' }1590 ]1591 }1592 const { container, queryByText, rerender } = render(<Reactable {...props} />)1593 expect(getHeaders(container)).toHaveLength(1)1594 expect(queryByText('col-a')).toEqual(null)1595 expect(queryByText('col-b')).toBeVisible()1596 const columns = [1597 { name: 'col-a', accessor: 'a', show: false },1598 { name: 'col-b', accessor: 'b', show: false }1599 ]1600 rerender(<Reactable {...props} columns={columns} />)1601 expect(getHeaders(container)).toHaveLength(0)1602 expect(queryByText('col-a')).toEqual(null)1603 expect(queryByText('col-b')).toEqual(null)1604 })1605})1606describe('column widths and flex layout', () => {1607 const getFlex = elements => [...elements].map(el => el.style.flex)1608 const getWidths = elements => [...elements].map(el => el.style.width)1609 const getMinWidths = elements => [...elements].map(el => el.style.minWidth)1610 const getMaxWidths = elements => [...elements].map(el => el.style.maxWidth)1611 it('default column widths', () => {1612 const props = {1613 data: { a: [1, 2], b: ['a', 'b'] },1614 columns: [1615 { name: 'colA', accessor: 'a', footer: 'footer' },1616 { name: 'colB', accessor: 'b' }1617 ],1618 // Test pad rows1619 minRows: 31620 }1621 const { container } = render(<Reactable {...props} />)1622 const headers = getHeaders(container)1623 expect(getFlex(headers)).toEqual(['100 0 auto', '100 0 auto'])1624 expect(getMinWidths(headers)).toEqual(['100px', '100px'])1625 expect(getWidths(headers)).toEqual(['100px', '100px'])1626 expect(getMaxWidths(headers)).toEqual(['', ''])1627 const cells = getCells(container)1628 expect(getFlex(cells)).toEqual(Array(6).fill('100 0 auto'))1629 expect(getMinWidths(cells)).toEqual(Array(6).fill('100px'))1630 expect(getWidths(cells)).toEqual(Array(6).fill('100px'))1631 expect(getMaxWidths(cells)).toEqual(Array(6).fill(''))1632 const footers = getFooters(container)1633 expect(getFlex(footers)).toEqual(['100 0 auto', '100 0 auto'])1634 expect(getMinWidths(footers)).toEqual(['100px', '100px'])1635 expect(getWidths(footers)).toEqual(['100px', '100px'])1636 expect(getMaxWidths(footers)).toEqual(['', ''])1637 })1638 it('min column widths', () => {1639 const props = {1640 data: { a: [1, 2], b: ['a', 'b'] },1641 columns: [1642 { name: 'colA', accessor: 'a', footer: 'footer', minWidth: 50 },1643 { name: 'colB', accessor: 'b' }1644 ],1645 // Test pad rows1646 minRows: 31647 }1648 const { container } = render(<Reactable {...props} />)1649 const headers = getHeaders(container)1650 expect(getFlex(headers)).toEqual(['50 0 auto', '100 0 auto'])1651 expect(getMinWidths(headers)).toEqual(['50px', '100px'])1652 expect(getWidths(headers)).toEqual(['50px', '100px'])1653 expect(getMaxWidths(headers)).toEqual(['', ''])1654 const cells = getCells(container)1655 expect(getFlex(cells)).toEqual([1656 '50 0 auto',1657 '100 0 auto',1658 '50 0 auto',1659 '100 0 auto',1660 '50 0 auto',1661 '100 0 auto'1662 ])1663 expect(getMinWidths(cells)).toEqual(['50px', '100px', '50px', '100px', '50px', '100px'])1664 expect(getWidths(cells)).toEqual(['50px', '100px', '50px', '100px', '50px', '100px'])1665 expect(getMaxWidths(cells)).toEqual(Array(6).fill(''))1666 const footers = getFooters(container)1667 expect(getFlex(footers)).toEqual(['50 0 auto', '100 0 auto'])1668 expect(getWidths(footers)).toEqual(['50px', '100px'])1669 expect(getMaxWidths(footers)).toEqual(['', ''])1670 })1671 it('max column widths', () => {1672 const props = {1673 data: { a: [1, 2], b: ['a', 'b'] },1674 columns: [1675 { name: 'colA', accessor: 'a', footer: 'footer', maxWidth: 50 },1676 { name: 'colB', accessor: 'b', maxWidth: 220 }1677 ],1678 // Test pad rows1679 minRows: 31680 }1681 const { container } = render(<Reactable {...props} />)1682 const headers = getHeaders(container)1683 expect(getFlex(headers)).toEqual(['0 0 auto', '100 0 auto'])1684 expect(getMinWidths(headers)).toEqual(['50px', '100px'])1685 expect(getWidths(headers)).toEqual(['50px', '100px'])1686 expect(getMaxWidths(headers)).toEqual(['50px', '220px'])1687 const cells = getCells(container)1688 expect(getFlex(cells)).toEqual([1689 '0 0 auto',1690 '100 0 auto',1691 '0 0 auto',1692 '100 0 auto',1693 '0 0 auto',1694 '100 0 auto'1695 ])1696 expect(getMinWidths(cells)).toEqual(['50px', '100px', '50px', '100px', '50px', '100px'])1697 expect(getWidths(cells)).toEqual(['50px', '100px', '50px', '100px', '50px', '100px'])1698 expect(getMaxWidths(cells)).toEqual(['50px', '220px', '50px', '220px', '50px', '220px'])1699 const footers = getFooters(container)1700 expect(getFlex(footers)).toEqual(['0 0 auto', '100 0 auto'])1701 expect(getMinWidths(headers)).toEqual(['50px', '100px'])1702 expect(getWidths(footers)).toEqual(['50px', '100px'])1703 expect(getMaxWidths(footers)).toEqual(['50px', '220px'])1704 })1705 it('fixed column widths', () => {1706 const props = {1707 data: { a: [1, 2], b: ['a', 'b'] },1708 columns: [1709 { name: 'colA', accessor: 'a', footer: 'footer', width: 70, minWidth: 50 },1710 { name: 'colB', accessor: 'b', width: 120, maxWidth: 50 }1711 ],1712 // Test pad rows1713 minRows: 31714 }1715 const { container } = render(<Reactable {...props} />)1716 const headers = getHeaders(container)1717 expect(getFlex(headers)).toEqual(['0 0 auto', '0 0 auto'])1718 expect(getMinWidths(headers)).toEqual(['70px', '120px'])1719 expect(getWidths(headers)).toEqual(['70px', '120px'])1720 expect(getMaxWidths(headers)).toEqual(['70px', '120px'])1721 const cells = getCells(container)1722 expect(getFlex(cells)).toEqual(Array(6).fill('0 0 auto'))1723 expect(getMinWidths(cells)).toEqual(['70px', '120px', '70px', '120px', '70px', '120px'])1724 expect(getWidths(cells)).toEqual(['70px', '120px', '70px', '120px', '70px', '120px'])1725 expect(getMaxWidths(cells)).toEqual(['70px', '120px', '70px', '120px', '70px', '120px'])1726 const footers = getFooters(container)1727 expect(getFlex(footers)).toEqual(['0 0 auto', '0 0 auto'])1728 expect(getMinWidths(footers)).toEqual(['70px', '120px'])1729 expect(getWidths(footers)).toEqual(['70px', '120px'])1730 expect(getMaxWidths(footers)).toEqual(['70px', '120px'])1731 })1732 it('column group widths', () => {1733 const props = {1734 data: { a: [1], b: [1], c: [1], d: [1], e: [1], f: [1], g: [1], h: [1], i: [1], j: [1] },1735 columns: [1736 { name: 'a', accessor: 'a' },1737 { name: 'b', accessor: 'b' },1738 { name: 'c', accessor: 'c', minWidth: 40 },1739 { name: 'd', accessor: 'd', width: 50 },1740 { name: 'e', accessor: 'e' },1741 { name: 'f', accessor: 'f', width: 120 },1742 { name: 'g', accessor: 'g', minWidth: 50, maxWidth: 140 },1743 { name: 'h', accessor: 'h' },1744 { name: 'i', accessor: 'i', maxWidth: 60 },1745 { name: 'j', accessor: 'j' }1746 ],1747 columnGroups: [1748 { name: 'default-1-col', columns: ['a'], headerClassName: 'default-1-col' },1749 { name: 'minWidth-2-col', columns: ['b', 'c'], headerClassName: 'minWidth-2-col' },1750 { name: 'width-1-col', columns: ['d'], headerClassName: 'width-1-col' },1751 { name: 'width-2-col', columns: ['e', 'f'], headerClassName: 'width-2-col' },1752 { name: 'maxWidth-1-col', columns: ['g'], headerClassName: 'maxWidth-1-col' },1753 { name: 'maxWidth-2-col', columns: ['h', 'i'], headerClassName: 'maxWidth-2-col' }1754 ]1755 }1756 const { container } = render(<Reactable {...props} />)1757 expect(getGroupHeaders(container)).toHaveLength(6)1758 expect(getGroupHeaders(container, '.default-1-col')[0]).toHaveStyle(1759 'flex: 100 0 auto; min-width: 100px; width: 100px; max-width:'1760 )1761 expect(getGroupHeaders(container, '.minWidth-2-col')[0]).toHaveStyle(1762 'flex: 140 0 auto; min-width: 140px; width: 140px; max-width:'1763 )1764 // Fixed width columns should be ignored when calculating flex width1765 expect(getGroupHeaders(container, '.width-1-col')[0]).toHaveStyle(1766 'flex: 0 0 auto; min-width: 50px; width: 50px; max-width: 50px'1767 )1768 expect(getGroupHeaders(container, '.width-2-col')[0]).toHaveStyle(1769 'flex: 100 0 auto; min-width: 220px; width: 220px; max-width:'1770 )1771 expect(getGroupHeaders(container, '.maxWidth-1-col')[0]).toHaveStyle(1772 'flex: 50 0 auto; min-width: 50px; width: 50px; max-width: 140px'1773 )1774 // Should not have max width if at least one column has no max width.1775 // Known issue: this can cause group headers to be misaligned if the column1776 // grows to hit its max width.1777 expect(getGroupHeaders(container, '.maxWidth-2-col')[0]).toHaveStyle(1778 'flex: 100 0 auto; min-width: 160px; width: 160px; max-width:'1779 )1780 const ungroupedHeaders = getUngroupedHeaders(container)1781 expect(ungroupedHeaders).toHaveLength(1)1782 expect(ungroupedHeaders[0]).toHaveStyle(1783 'flex: 100 0 auto; min-width: 100px; width: 100px; max-width:'1784 )1785 })1786 it('column group widths with groupBy columns', () => {1787 const props = {1788 data: { a: [1], b: [1], c: [1], d: [1] },1789 columns: [1790 { name: 'a', accessor: 'a' },1791 { name: 'b', accessor: 'b' },1792 { name: 'c', accessor: 'c' },1793 { name: 'd', accessor: 'd' }1794 ],1795 columnGroups: [{ name: 'group', columns: ['a', 'b'] }],1796 pivotBy: ['d']1797 }1798 const { container } = render(<Reactable {...props} />)1799 const groupHeaders = getGroupHeaders(container)1800 expect(groupHeaders).toHaveLength(1)1801 const ungroupedHeaders = getUngroupedHeaders(container)1802 expect(ungroupedHeaders).toHaveLength(2)1803 expect(groupHeaders[0]).toHaveStyle(1804 'flex: 200 0 auto; min-width: 200px; width: 200px; max-width:'1805 )1806 // Column groups for groupBy columns pulled out of a 2+ column group should1807 // have correct widths for the new column count (1 column in this case).1808 expect(ungroupedHeaders[0]).toHaveStyle(1809 'flex: 100 0 auto; min-width: 100px; width: 100px; max-width:'1810 )1811 expect(ungroupedHeaders[1]).toHaveStyle(1812 'flex: 100 0 auto; min-width: 100px; width: 100px; max-width:'1813 )1814 })1815 it('should have min-width on thead, tbody, and tfoot', () => {1816 const props = {1817 data: { a: [1, 2], b: ['a', 'b'], c: ['c', 'd'] },1818 columns: [1819 { name: 'colA', accessor: 'a', footer: 'footer', minWidth: 50 },1820 { name: 'colB', accessor: 'b', width: 120, maxWidth: 50 },1821 { name: 'colC', accessor: 'c' }1822 ],1823 columnGroups: [{ columns: ['a', 'b'], name: 'group-ab' }],1824 filterable: true1825 }1826 const { container } = render(<Reactable {...props} />)1827 // Table element should not have min-width for horizontal scrolling1828 expect(getTable(container).style.minWidth).toEqual('')1829 expect(getThead(container)).toHaveStyle('min-width: 270px')1830 expect(getTbody(container)).toHaveStyle('min-width: 270px')1831 expect(getTfoot(container)).toHaveStyle('min-width: 270px')1832 // Min width should also be set on rows, but it's not really necessary1833 const rows = getRows(container)1834 expect(rows).toHaveLength(2)1835 rows.forEach(row => expect(row).toHaveStyle('min-width: 270px'))1836 })1837})1838describe('column resizing', () => {1839 beforeEach(() => {1840 jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb())1841 })1842 afterEach(() => {1843 window.requestAnimationFrame.mockRestore()1844 })1845 it('is not resizable by default', () => {1846 const props = {1847 data: { a: [1, 3, 2], b: ['aa', 'bb', 'cc'] },1848 columns: [1849 { name: 'colA', accessor: 'a' },1850 { name: 'colB', accessor: 'b' }1851 ]1852 }1853 const { container } = render(<Reactable {...props} />)1854 expect(getResizableHeaders(container)).toHaveLength(0)1855 expect(getResizers(container)).toHaveLength(0)1856 })1857 it('enables resizing', () => {1858 const props = {1859 data: { a: [1, 3, 2], b: ['aa', 'bb', 'cc'] },1860 columns: [1861 { name: 'colA', accessor: 'a' },1862 { name: 'colB', accessor: 'b' }1863 ],1864 resizable: true1865 }1866 const { container } = render(<Reactable {...props} />)1867 const resizableHeaders = getResizableHeaders(container)1868 const resizers = getResizers(container)1869 expect(resizableHeaders).toHaveLength(2)1870 expect(resizers).toHaveLength(2)1871 expect(getResizers(resizableHeaders[1])).toHaveLength(1)1872 expect(getResizers(resizableHeaders[1])[0]).toEqual(resizers[0])1873 })1874 it('disables resizing', () => {1875 // Resizing disabled globally1876 const props = {1877 data: { a: [1, 3, 2, 5], b: ['aa', 'CC', 'dd', 'BB'] },1878 columns: [1879 { name: 'colA', accessor: 'a' },1880 { name: 'colB', accessor: 'b' }1881 ],1882 resizable: false1883 }1884 const { container, rerender } = render(<Reactable {...props} />)1885 expect(getResizableHeaders(container)).toHaveLength(0)1886 expect(getResizers(container)).toHaveLength(0)1887 // Resizing disabled globally with column enable override1888 let columns = [1889 { name: 'colA', accessor: 'a', headerClassName: 'col-a', resizable: true },1890 { name: 'colB', accessor: 'b', headerClassName: 'col-b' }1891 ]1892 rerender(<Reactable {...props} columns={columns} />)1893 expect(getResizableHeaders(container)).toHaveLength(1)1894 expect(getResizableHeaders(container)[0]).toHaveClass('col-a')1895 expect(getResizers(container)).toHaveLength(1)1896 // Resizing enabled globally with column disable override1897 columns = [1898 { name: 'colA', accessor: 'a', headerClassName: 'col-a', resizable: false },1899 { name: 'colB', accessor: 'b', headerClassName: 'col-b' }1900 ]1901 rerender(<Reactable {...props} columns={columns} resizable={true} />)1902 expect(getResizableHeaders(container)).toHaveLength(1)1903 expect(getResizableHeaders(container)[0]).toHaveClass('col-b')1904 expect(getResizers(container)).toHaveLength(1)1905 // Resizing should be disabled on fixed width columns1906 columns = [1907 { name: 'colA', accessor: 'a', headerClassName: 'col-a', width: 30 },1908 { name: 'colB', accessor: 'b', headerClassName: 'col-b', width: 50, resizable: true }1909 ]1910 rerender(<Reactable {...props} columns={columns} resizable={true} />)1911 expect(getResizableHeaders(container)).toHaveLength(0)1912 expect(getResizers(container)).toHaveLength(0)1913 })1914 it('enables resizing for column groups', () => {1915 const props = {1916 data: { a: [1, 3], b: ['aa', 'bb'], c: ['c', 'd'], d: ['d', 'e'], e: ['e', 'f'] },1917 columns: [1918 { name: 'colA', accessor: 'a' },1919 { name: 'colB', accessor: 'b' },1920 { name: 'colC', accessor: 'c' },1921 // Column groups with at least one resizable column should be resizable1922 { name: 'colD', accessor: 'd', resizable: false },1923 // Ungrouped column headers should be resizable too1924 { name: 'colE', accessor: 'e' }1925 ],1926 columnGroups: [1927 { columns: ['a', 'b'], name: 'group-ab' },1928 { columns: ['c', 'd'], name: 'group-cd' }1929 ],1930 resizable: true1931 }1932 const { container } = render(<Reactable {...props} />)1933 expect(getResizableHeaders(container)).toHaveLength(7)1934 expect(getResizers(container)).toHaveLength(7)1935 })1936 it('disables resizing for column groups', () => {1937 const props = {1938 data: { a: [1, 3], b: ['aa', 'bb'], c: ['c', 'd'], d: ['d', 'e'] },1939 columns: [1940 { name: 'colA', accessor: 'a', resizable: false },1941 { name: 'colB', accessor: 'b', resizable: false },1942 { name: 'colC', accessor: 'c', resizable: false },1943 { name: 'colD', accessor: 'd', headerClassName: 'col-resizable' }1944 ],1945 columnGroups: [1946 { columns: ['a', 'b'], name: 'group-all-cols-no-resize' },1947 { columns: ['c'], name: 'group-single-col-no-resize' },1948 { columns: ['d'], name: 'group-resizable', headerClassName: 'group-resizable' }1949 ],1950 resizable: true1951 }1952 const { container } = render(<Reactable {...props} />)1953 expect(getResizableHeaders(container)).toHaveLength(2)1954 expect(getResizableHeaders(container)[0]).toHaveClass('group-resizable')1955 expect(getResizableHeaders(container)[1]).toHaveClass('col-resizable')1956 expect(getResizers(container)).toHaveLength(2)1957 })1958 it('mouse resizing works for headers', () => {1959 const props = {1960 data: { a: [1, 3, 2], b: ['aa', 'bb', 'cc'] },1961 columns: [1962 { name: 'colA', accessor: 'a', className: 'col-a', footer: 'footer' },1963 { name: 'colB', accessor: 'b', minWidth: 30, maxWidth: 130 },1964 { name: 'colC', accessor: 'c', width: 70 }1965 ],1966 columnGroups: [{ name: 'group-bc', columns: ['b', 'c'] }],1967 resizable: true,1968 minRows: 41969 }1970 const { container } = render(<Reactable {...props} />)1971 const [headerA, headerB, headerC] = getColumnHeaders(container)1972 const [ungroupedHeaderA] = getUngroupedHeaders(container)1973 const [groupHeaderBC] = getGroupHeaders(container)1974 const resizerA = getResizers(headerA)[0]1975 const resizerB = getResizers(headerB)[0]1976 expect(headerA).toHaveStyle('width: 100px; flex: 100 0 auto')1977 expect(ungroupedHeaderA).toHaveStyle('width: 100px; flex: 100 0 auto')1978 expect(headerB).toHaveStyle('width: 30px; flex: 30 0 auto')1979 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')1980 expect(groupHeaderBC).toHaveStyle('width: 100px; flex: 30 0 auto')1981 const cellsA = [1982 ...getCells(container, '.col-a'),1983 getFooters(container)[0],1984 getCells(getPadRows(container)[0])[0]1985 ]1986 expect(cellsA).toHaveLength(5)1987 cellsA.forEach(cell => expect(cell).toHaveStyle('width: 100px; flex: 100 0 auto'))1988 // Mock the DOM widths, which can be different from style.width1989 headerA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))1990 headerB.getBoundingClientRect = jest.fn(() => ({ width: 50 }))1991 ungroupedHeaderA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))1992 // Resizing header 120+70px1993 fireEvent.mouseDown(resizerA, { clientX: 0 })1994 fireEvent.mouseMove(resizerA, { clientX: 70 })1995 fireEvent.mouseUp(resizerA, { clientX: 70 })1996 expect(headerA).toHaveStyle('width: 190px; flex: 0 0 auto')1997 expect(ungroupedHeaderA).toHaveStyle('width: 190px; flex: 0 0 auto')1998 cellsA.forEach(cell => expect(cell).toHaveStyle('width: 190px; flex: 0 0 auto'))1999 // Resizing header 120-10px2000 fireEvent.mouseDown(resizerA, { clientX: 70 })2001 fireEvent.mouseMove(resizerA, { clientX: 60 })2002 fireEvent.mouseUp(resizerA, { clientX: 60 })2003 expect(headerA).toHaveStyle('width: 110px; flex: 0 0 auto')2004 expect(ungroupedHeaderA).toHaveStyle('width: 110px; flex: 0 0 auto')2005 cellsA.forEach(cell => expect(cell).toHaveStyle('width: 110px; flex: 0 0 auto'))2006 // Resizing should be limited by max width (50+300px limited at 130px)2007 fireEvent.mouseDown(resizerB, { clientX: 0 })2008 fireEvent.mouseMove(resizerB, { clientX: 300 })2009 fireEvent.mouseUp(resizerB, { clientX: 300 })2010 expect(headerB).toHaveStyle('width: 130px; flex: 0 0 auto')2011 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2012 expect(groupHeaderBC).toHaveStyle('width: 200px; flex: 0 0 auto')2013 // Resizing should be limited by min width (50-300px limited at 30px)2014 fireEvent.mouseDown(resizerB, { clientX: 300 })2015 fireEvent.mouseMove(resizerB, { clientX: 0 })2016 fireEvent.mouseUp(resizerB, { clientX: 0 })2017 expect(headerB).toHaveStyle('width: 30px; flex: 0 0 auto')2018 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2019 expect(groupHeaderBC).toHaveStyle('width: 100px; flex: 0 0 auto')2020 })2021 it('touch resizing works for headers', () => {2022 const props = {2023 data: { a: [1, 3, 2], b: ['aa', 'bb', 'cc'] },2024 columns: [2025 { name: 'colA', accessor: 'a', className: 'col-a', footer: 'footer' },2026 { name: 'colB', accessor: 'b', minWidth: 30, maxWidth: 130 },2027 { name: 'colC', accessor: 'c', width: 70 }2028 ],2029 columnGroups: [{ name: 'group-bc', columns: ['b', 'c'] }],2030 resizable: true,2031 minRows: 42032 }2033 const { container } = render(<Reactable {...props} />)2034 const [headerA, headerB, headerC] = getColumnHeaders(container)2035 const [ungroupedHeaderA] = getUngroupedHeaders(container)2036 const [groupHeaderBC] = getGroupHeaders(container)2037 const resizerA = getResizers(headerA)[0]2038 const resizerB = getResizers(headerB)[0]2039 expect(headerA).toHaveStyle('width: 100px; flex: 100 0 auto')2040 expect(ungroupedHeaderA).toHaveStyle('width: 100px; flex: 100 0 auto')2041 expect(headerB).toHaveStyle('width: 30px; flex: 30 0 auto')2042 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2043 expect(groupHeaderBC).toHaveStyle('width: 100px; flex: 30 0 auto')2044 const cellsA = [2045 ...getCells(container, '.col-a'),2046 getFooters(container)[0],2047 getCells(getPadRows(container)[0])[0]2048 ]2049 expect(cellsA).toHaveLength(5)2050 cellsA.forEach(cell => expect(cell).toHaveStyle('width: 100px; flex: 100 0 auto'))2051 // Mock the DOM widths, which can be different from style.width2052 headerA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2053 headerB.getBoundingClientRect = jest.fn(() => ({ width: 50 }))2054 ungroupedHeaderA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2055 // Resizing header 120+70px2056 fireEvent.touchStart(resizerA, { touches: [{ clientX: 0 }] })2057 fireEvent.touchMove(resizerA, { touches: [{ clientX: 70 }] })2058 fireEvent.touchEnd(resizerA, { touches: [{ clientX: 70 }] })2059 expect(headerA).toHaveStyle('width: 190px; flex: 0 0 auto')2060 expect(ungroupedHeaderA).toHaveStyle('width: 190px; flex: 0 0 auto')2061 cellsA.forEach(cell => expect(cell).toHaveStyle('width: 190px; flex: 0 0 auto'))2062 // Resizing header 120-10px2063 fireEvent.touchStart(resizerA, { touches: [{ clientX: 70 }] })2064 fireEvent.touchMove(resizerA, { touches: [{ clientX: 60 }] })2065 fireEvent.touchEnd(resizerA, { touches: [{ clientX: 60 }] })2066 expect(headerA).toHaveStyle('width: 110px; flex: 0 0 auto')2067 expect(ungroupedHeaderA).toHaveStyle('width: 110px; flex: 0 0 auto')2068 cellsA.forEach(cell => expect(cell).toHaveStyle('width: 110px; flex: 0 0 auto'))2069 // Resizing should be limited by max width (50+300px limited at 130px)2070 fireEvent.touchStart(resizerB, { touches: [{ clientX: 0 }] })2071 fireEvent.touchMove(resizerB, { touches: [{ clientX: 300 }] })2072 fireEvent.touchEnd(resizerB, { touches: [{ clientX: 300 }] })2073 expect(headerB).toHaveStyle('width: 130px; flex: 0 0 auto')2074 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2075 expect(groupHeaderBC).toHaveStyle('width: 200px; flex: 0 0 auto')2076 // Resizing should be limited by min width (50-300px limited at 30px)2077 fireEvent.touchStart(resizerB, { touches: [{ clientX: 300 }] })2078 fireEvent.touchMove(resizerB, { touches: [{ clientX: 0 }] })2079 fireEvent.touchEnd(resizerB, { touches: [{ clientX: 0 }] })2080 expect(headerB).toHaveStyle('width: 30px; flex: 0 0 auto')2081 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2082 expect(groupHeaderBC).toHaveStyle('width: 100px; flex: 0 0 auto')2083 })2084 it('mouse resizing works for column group headers', () => {2085 const props = {2086 data: { a: [1, 3, 2], b: ['aa', 'bb', 'cc'] },2087 columns: [2088 { name: 'colA', accessor: 'a' },2089 { name: 'colB', accessor: 'b', minWidth: 30, maxWidth: 130 },2090 { name: 'colC', accessor: 'c', width: 70 }2091 ],2092 columnGroups: [{ name: 'group-bc', columns: ['b', 'c'] }],2093 resizable: true2094 }2095 const { container } = render(<Reactable {...props} />)2096 const [headerA, headerB, headerC] = getColumnHeaders(container)2097 const [ungroupedHeaderA] = getUngroupedHeaders(container)2098 const [groupHeaderBC] = getGroupHeaders(container)2099 const resizers = getResizers(container)2100 expect(resizers).toHaveLength(4)2101 const [resizerGroupA, resizerGroupBC] = resizers2102 expect(headerA).toHaveStyle('width: 100px; flex: 100 0 auto')2103 expect(ungroupedHeaderA).toHaveStyle('width: 100px; flex: 100 0 auto')2104 expect(headerB).toHaveStyle('width: 30px; flex: 30 0 auto')2105 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2106 expect(groupHeaderBC).toHaveStyle('width: 100px; flex: 30 0 auto')2107 // Mock the DOM widths, which can be different from style.width2108 headerA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2109 ungroupedHeaderA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2110 headerB.getBoundingClientRect = jest.fn(() => ({ width: 50 }))2111 groupHeaderBC.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2112 // Resizing single group header 120+70px2113 fireEvent.mouseDown(resizerGroupA, { clientX: 0 })2114 fireEvent.mouseMove(resizerGroupA, { clientX: 70 })2115 fireEvent.mouseUp(resizerGroupA, { clientX: 70 })2116 expect(headerA).toHaveStyle('width: 190px; flex: 0 0 auto')2117 expect(ungroupedHeaderA).toHaveStyle('width: 190px; flex: 0 0 auto')2118 // Resizing single group header 120-10px2119 fireEvent.mouseDown(resizerGroupA, { clientX: 70 })2120 fireEvent.mouseMove(resizerGroupA, { clientX: 60 })2121 fireEvent.mouseUp(resizerGroupA, { clientX: 60 })2122 expect(headerA).toHaveStyle('width: 110px; flex: 0 0 auto')2123 expect(ungroupedHeaderA).toHaveStyle('width: 110px; flex: 0 0 auto')2124 // Resizing double group header 120+70px2125 fireEvent.mouseDown(resizerGroupBC, { clientX: 0 })2126 fireEvent.mouseMove(resizerGroupBC, { clientX: 70 })2127 fireEvent.mouseUp(resizerGroupBC, { clientX: 70 })2128 expect(headerB).toHaveStyle('width: 79.16666666666667px; flex: 0 0 auto') // 50 + 50 * (70/120)2129 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2130 expect(groupHeaderBC).toHaveStyle('width: 149.16666666666669px; flex: 0 0 auto') // 50 + 50 * (70/120) + 702131 // Resizing should be limited by max width (50+300px limited at 130px)2132 fireEvent.mouseDown(resizerGroupBC, { clientX: 0 })2133 fireEvent.mouseMove(resizerGroupBC, { clientX: 300 })2134 fireEvent.mouseUp(resizerGroupBC, { clientX: 300 })2135 expect(headerB).toHaveStyle('width: 130px; flex: 0 0 auto')2136 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2137 expect(groupHeaderBC).toHaveStyle('width: 200px; flex: 0 0 auto')2138 // Resizing should be limited by min width (50-300px limited at 30px)2139 fireEvent.mouseDown(resizerGroupBC, { clientX: 300 })2140 fireEvent.mouseMove(resizerGroupBC, { clientX: 0 })2141 fireEvent.mouseUp(resizerGroupBC, { clientX: 0 })2142 expect(headerB).toHaveStyle('width: 30px; flex: 0 0 auto')2143 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2144 expect(groupHeaderBC).toHaveStyle('width: 100px; flex: 0 0 auto')2145 })2146 it('touch resizing works for column group headers', () => {2147 const props = {2148 data: { a: [1, 3, 2], b: ['aa', 'bb', 'cc'] },2149 columns: [2150 { name: 'colA', accessor: 'a' },2151 { name: 'colB', accessor: 'b', minWidth: 30, maxWidth: 130 },2152 { name: 'colC', accessor: 'c', width: 70 }2153 ],2154 columnGroups: [{ name: 'group-bc', columns: ['b', 'c'] }],2155 resizable: true2156 }2157 const { container } = render(<Reactable {...props} />)2158 const [headerA, headerB, headerC] = getColumnHeaders(container)2159 const [ungroupedHeaderA] = getUngroupedHeaders(container)2160 const [groupHeaderBC] = getGroupHeaders(container)2161 const resizers = getResizers(container)2162 expect(resizers).toHaveLength(4)2163 const [resizerGroupA, resizerGroupBC] = resizers2164 expect(headerA).toHaveStyle('width: 100px; flex: 100 0 auto')2165 expect(ungroupedHeaderA).toHaveStyle('width: 100px; flex: 100 0 auto')2166 expect(headerB).toHaveStyle('width: 30px; flex: 30 0 auto')2167 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2168 expect(groupHeaderBC).toHaveStyle('width: 100px; flex: 30 0 auto')2169 // Mock the DOM widths, which can be different from style.width2170 headerA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2171 ungroupedHeaderA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2172 headerB.getBoundingClientRect = jest.fn(() => ({ width: 50 }))2173 groupHeaderBC.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2174 // Resizing single group header 120+70px2175 fireEvent.touchStart(resizerGroupA, { touches: [{ clientX: 0 }] })2176 fireEvent.touchMove(resizerGroupA, { touches: [{ clientX: 70 }] })2177 fireEvent.touchEnd(resizerGroupA, { touches: [{ clientX: 70 }] })2178 expect(headerA).toHaveStyle('width: 190px; flex: 0 0 auto')2179 expect(ungroupedHeaderA).toHaveStyle('width: 190px; flex: 0 0 auto')2180 // Resizing single group header 120-10px2181 fireEvent.touchStart(resizerGroupA, { touches: [{ clientX: 70 }] })2182 fireEvent.touchMove(resizerGroupA, { touches: [{ clientX: 60 }] })2183 fireEvent.touchEnd(resizerGroupA, { touches: [{ clientX: 60 }] })2184 expect(headerA).toHaveStyle('width: 110px; flex: 0 0 auto')2185 expect(ungroupedHeaderA).toHaveStyle('width: 110px; flex: 0 0 auto')2186 // Resizing double group header 120+70px2187 fireEvent.touchStart(resizerGroupBC, { touches: [{ clientX: 0 }] })2188 fireEvent.touchMove(resizerGroupBC, { touches: [{ clientX: 70 }] })2189 fireEvent.touchEnd(resizerGroupBC, { touches: [{ clientX: 70 }] })2190 expect(headerB).toHaveStyle('width: 79.16666666666667px; flex: 0 0 auto') // 50 + 50 * (70/120)2191 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2192 expect(groupHeaderBC).toHaveStyle('width: 149.16666666666669px; flex: 0 0 auto') // 50 + 50 * (70/120) + 702193 // Resizing should be limited by max width (50+300px limited at 130px)2194 fireEvent.touchStart(resizerGroupBC, { touches: [{ clientX: 0 }] })2195 fireEvent.touchMove(resizerGroupBC, { touches: [{ clientX: 300 }] })2196 fireEvent.touchEnd(resizerGroupBC, { touches: [{ clientX: 300 }] })2197 expect(headerB).toHaveStyle('width: 130px; flex: 0 0 auto')2198 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2199 expect(groupHeaderBC).toHaveStyle('width: 200px; flex: 0 0 auto')2200 // Resizing should be limited by min width (50-300px limited at 30px)2201 fireEvent.touchStart(resizerGroupBC, { touches: [{ clientX: 300 }] })2202 fireEvent.touchMove(resizerGroupBC, { touches: [{ clientX: 0 }] })2203 fireEvent.touchEnd(resizerGroupBC, { touches: [{ clientX: 0 }] })2204 expect(headerB).toHaveStyle('width: 30px; flex: 0 0 auto')2205 expect(headerC).toHaveStyle('width: 70px; flex: 0 0 auto')2206 expect(groupHeaderBC).toHaveStyle('width: 100px; flex: 0 0 auto')2207 })2208 it('columns cannot be resized with multiple touches', () => {2209 const props = {2210 data: { a: [1, 3, 2], b: ['aa', 'bb', 'cc'] },2211 columns: [{ name: 'colA', accessor: 'a' }],2212 columnGroups: [{ name: 'group-a', columns: ['a'] }],2213 resizable: true2214 }2215 const { container } = render(<Reactable {...props} />)2216 const [headerA] = getColumnHeaders(container)2217 const [groupedHeaderA] = getGroupHeaders(container)2218 const resizerA = getResizers(headerA)[0]2219 expect(headerA).toHaveStyle('width: 100px; flex: 100 0 auto')2220 expect(groupedHeaderA).toHaveStyle('width: 100px; flex: 100 0 auto')2221 // Mock the DOM widths, which can be different from style.width2222 headerA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2223 groupedHeaderA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2224 fireEvent.touchStart(resizerA, { touches: [{ clientX: 0 }, { clientX: 0 }] })2225 fireEvent.touchMove(resizerA, { touches: [{ clientX: 70 }, { clientX: 70 }] })2226 fireEvent.touchEnd(resizerA, { touches: [{ clientX: 70 }, { clientX: 70 }] })2227 expect(headerA).toHaveStyle('width: 100px; flex: 100 0 auto')2228 expect(groupedHeaderA).toHaveStyle('width: 100px; flex: 100 0 auto')2229 })2230 it('headers and cells are styled when resizing the table', () => {2231 const props = {2232 data: { a: [1, 3, 2], b: ['aa', 'bb', 'cc'] },2233 columns: [2234 { name: 'colA', accessor: 'a' },2235 { name: 'colB', accessor: 'b', minWidth: 30, maxWidth: 130 },2236 { name: 'colC', accessor: 'c', width: 70 }2237 ],2238 columnGroups: [{ name: 'group-bc', columns: ['b', 'c'] }],2239 resizable: true2240 }2241 const { container } = render(<Reactable {...props} />)2242 const [headerA] = getColumnHeaders(container)2243 const [groupHeaderBC] = getGroupHeaders(container)2244 const resizerA = getResizers(headerA)[0]2245 const resizerGroupBC = getResizers(groupHeaderBC)[0]2246 const table = getTable(container)2247 expect(table).not.toHaveClass('rt-resizing')2248 // Resizing header2249 fireEvent.mouseDown(resizerA, { clientX: 0 })2250 expect(table).toHaveClass('rt-resizing')2251 fireEvent.mouseMove(resizerA, { clientX: 70 })2252 expect(table).toHaveClass('rt-resizing')2253 fireEvent.mouseUp(resizerA, { clientX: 70 })2254 expect(table).not.toHaveClass('rt-resizing')2255 // Resizing group header2256 fireEvent.mouseDown(resizerGroupBC, { clientX: 0 })2257 expect(table).toHaveClass('rt-resizing')2258 fireEvent.mouseMove(resizerGroupBC, { clientX: 70 })2259 expect(table).toHaveClass('rt-resizing')2260 fireEvent.mouseUp(resizerGroupBC, { clientX: 70 })2261 expect(table).not.toHaveClass('rt-resizing')2262 })2263 it('min-width on table body, table head, and table foot should include resized widths', () => {2264 const props = {2265 data: { a: [1, 2], b: ['a', 'b'], c: ['c', 'd'] },2266 columns: [2267 { name: 'colA', accessor: 'a', footer: 'footer', minWidth: 50 },2268 { name: 'colB', accessor: 'b', width: 120, maxWidth: 50 },2269 { name: 'colC', accessor: 'c' }2270 ],2271 columnGroups: [{ columns: ['a', 'b'], name: 'group-ab' }],2272 filterable: true,2273 resizable: true2274 }2275 const { container } = render(<Reactable {...props} />)2276 const headerA = getColumnHeaders(container)[0]2277 const resizerA = getResizers(headerA)[0]2278 // Mock the DOM widths, which can be different from style.width2279 headerA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2280 // Resizing header 120+70px2281 fireEvent.touchStart(resizerA, { touches: [{ clientX: 0 }] })2282 fireEvent.touchMove(resizerA, { touches: [{ clientX: 70 }] })2283 fireEvent.touchEnd(resizerA, { touches: [{ clientX: 70 }] })2284 expect(headerA).toHaveStyle('width: 190px; flex: 0 0 auto')2285 // Table element should not have min-width for horizontal scrolling2286 expect(getTable(container).style.minWidth).toEqual('')2287 expect(getThead(container)).toHaveStyle('min-width: 410px') // 190+120+100px2288 expect(getTbody(container)).toHaveStyle('min-width: 410px')2289 expect(getTfoot(container)).toHaveStyle('min-width: 410px')2290 })2291 it('resized state persists when data changes', () => {2292 const props = {2293 data: { a: [1, 2], b: ['a', 'b'] },2294 columns: [2295 { name: 'a', accessor: 'a' },2296 { name: 'b', accessor: 'b' }2297 ],2298 columnGroups: [{ name: 'group-ab', columns: ['a', 'b'] }],2299 resizable: true2300 }2301 const { container, rerender } = render(<Reactable {...props} />)2302 const [headerA, headerB] = getColumnHeaders(container)2303 const [groupHeader] = getGroupHeaders(container)2304 const resizerA = getResizers(container)[1]2305 expect(headerA).toHaveStyle('width: 100px; flex: 100 0 auto')2306 expect(headerB).toHaveStyle('width: 100px; flex: 100 0 auto')2307 expect(groupHeader).toHaveStyle('width: 200px; flex: 200 0 auto')2308 // Mock the DOM widths, which can be different from style.width2309 headerA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2310 headerB.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2311 fireEvent.mouseDown(resizerA, { clientX: 120 })2312 fireEvent.mouseMove(resizerA, { clientX: 200 })2313 fireEvent.mouseUp(resizerA, { clientX: 200 })2314 expect(headerA).toHaveStyle('width: 200px; flex: 0 0 auto')2315 expect(headerB).toHaveStyle('width: 100px; flex: 100 0 auto')2316 expect(groupHeader).toHaveStyle('width: 300px; flex: 100 0 auto')2317 rerender(<Reactable {...props} data={{ a: ['a', 'b', 'c'], b: ['x', 'y', 'bz'] }} />)2318 expect(headerA).toHaveStyle('width: 200px; flex: 0 0 auto')2319 expect(headerB).toHaveStyle('width: 100px; flex: 100 0 auto')2320 expect(groupHeader).toHaveStyle('width: 300px; flex: 100 0 auto')2321 })2322})2323describe('sticky columns', () => {2324 // For testing resizing2325 beforeEach(() => {2326 jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb())2327 })2328 afterEach(() => {2329 window.requestAnimationFrame.mockRestore()2330 })2331 it('sticky left', () => {2332 const props = {2333 data: { a: [1, 2], b: ['a', 'b'], c: ['c', 'd'], d: ['e', 'f'] },2334 columns: [2335 {2336 name: 'a',2337 accessor: 'a',2338 footer: 'ftr',2339 sticky: 'left',2340 className: 'col-a',2341 headerClassName: 'col-a',2342 footerClassName: 'col-a'2343 },2344 {2345 name: 'b',2346 accessor: 'b',2347 footer: 'ftr',2348 sticky: 'left',2349 className: 'col-b',2350 headerClassName: 'col-b',2351 footerClassName: 'col-b'2352 },2353 {2354 name: 'c',2355 accessor: 'c',2356 footer: 'ftr',2357 className: 'col-c',2358 headerClassName: 'col-c',2359 footerClassName: 'col-c'2360 },2361 {2362 name: 'd',2363 accessor: 'd',2364 footer: 'ftr',2365 sticky: 'left',2366 className: 'col-d',2367 headerClassName: 'col-d',2368 footerClassName: 'col-d'2369 }2370 ],2371 columnGroups: [2372 { name: 'group-ab', columns: ['a', 'b'], sticky: 'left' },2373 { name: 'group-c', columns: ['c'] },2374 { name: 'group-d', columns: ['d'], sticky: 'left' }2375 ],2376 filterable: true,2377 minRows: 32378 }2379 const { container } = render(<Reactable {...props} />)2380 const [padCellA, padCellB, padCellC, padCellD] = getCells(getPadRows(container)[0])2381 const cellsA = [...container.querySelectorAll('.col-a'), padCellA]2382 cellsA.forEach(cell => expect(cell).toHaveStyle('position: sticky; left: 0'))2383 cellsA.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2384 const cellsB = [...container.querySelectorAll('.col-b'), padCellB]2385 cellsB.forEach(cell => expect(cell).toHaveStyle('position: sticky; left: 100px'))2386 cellsB.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2387 const cellsC = [...container.querySelectorAll('.col-c'), padCellC]2388 cellsC.forEach(cell => expect(cell).not.toHaveStyle('position: sticky'))2389 cellsC.forEach(cell => expect(cell).not.toHaveClass('rt-sticky'))2390 const cellsD = [...container.querySelectorAll('.col-d'), padCellD]2391 cellsD.forEach(cell => expect(cell).toHaveStyle('position: sticky; left: 200px'))2392 cellsD.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2393 const [groupAB, groupC, groupD] = getGroupHeaders(container)2394 expect(groupAB).toHaveStyle('position: sticky; left: 0')2395 expect(groupAB).toHaveClass('rt-sticky')2396 expect(groupC).not.toHaveStyle('position: sticky')2397 expect(groupC).not.toHaveClass('rt-sticky')2398 expect(groupD).toHaveStyle('position: sticky; left: 200px')2399 expect(groupD).toHaveClass('rt-sticky')2400 })2401 it('sticky right', () => {2402 const props = {2403 data: { a: [1, 2], b: ['a', 'b'], c: ['c', 'd'], d: ['e', 'f'] },2404 columns: [2405 {2406 name: 'a',2407 accessor: 'a',2408 footer: 'ftr',2409 sticky: 'right',2410 className: 'col-a',2411 headerClassName: 'col-a',2412 footerClassName: 'col-a'2413 },2414 {2415 name: 'b',2416 accessor: 'b',2417 footer: 'ftr',2418 sticky: 'right',2419 className: 'col-b',2420 headerClassName: 'col-b',2421 footerClassName: 'col-b'2422 },2423 {2424 name: 'c',2425 accessor: 'c',2426 footer: 'ftr',2427 className: 'col-c',2428 headerClassName: 'col-c',2429 footerClassName: 'col-c'2430 },2431 {2432 name: 'd',2433 accessor: 'd',2434 footer: 'ftr',2435 sticky: 'right',2436 className: 'col-d',2437 headerClassName: 'col-d',2438 footerClassName: 'col-d'2439 }2440 ],2441 columnGroups: [2442 { name: 'group-ab', columns: ['a', 'b'], sticky: 'right' },2443 { name: 'group-c', columns: ['c'] },2444 { name: 'group-d', columns: ['d'], sticky: 'right' }2445 ],2446 filterable: true,2447 minRows: 32448 }2449 const { container } = render(<Reactable {...props} />)2450 const [padCellA, padCellB, padCellC, padCellD] = getCells(getPadRows(container)[0])2451 const cellsD = [...container.querySelectorAll('.col-d'), padCellD]2452 cellsD.forEach(cell => expect(cell).toHaveStyle('position: sticky; right: 0'))2453 cellsD.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2454 const cellsC = [...container.querySelectorAll('.col-c'), padCellC]2455 cellsC.forEach(cell => expect(cell).not.toHaveStyle('position: sticky'))2456 cellsC.forEach(cell => expect(cell).not.toHaveClass('rt-sticky'))2457 const cellsB = [...container.querySelectorAll('.col-b'), padCellB]2458 cellsB.forEach(cell => expect(cell).toHaveStyle('position: sticky; right: 100px'))2459 cellsB.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2460 const cellsA = [...container.querySelectorAll('.col-a'), padCellA]2461 cellsA.forEach(cell => expect(cell).toHaveStyle('position: sticky; right: 200px'))2462 cellsA.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2463 const [groupAB, groupC, groupD] = getGroupHeaders(container)2464 expect(groupD).toHaveStyle('position: sticky; right: 0')2465 expect(groupD).toHaveClass('rt-sticky')2466 expect(groupC).not.toHaveStyle('position: sticky')2467 expect(groupC).not.toHaveClass('rt-sticky')2468 expect(groupAB).toHaveStyle('position: sticky; right: 100px')2469 expect(groupAB).toHaveClass('rt-sticky')2470 })2471 it('sticky columns work with resizing', () => {2472 const props = {2473 data: { a: [1, 2], b: ['a', 'b'], c: ['c', 'd'], d: ['e', 'f'] },2474 columns: [2475 {2476 name: 'a',2477 accessor: 'a',2478 footer: 'ftr',2479 sticky: 'left',2480 className: 'col-a',2481 headerClassName: 'col-a',2482 footerClassName: 'col-a'2483 },2484 {2485 name: 'b',2486 accessor: 'b',2487 footer: 'ftr',2488 sticky: 'left',2489 className: 'col-b',2490 headerClassName: 'col-b',2491 footerClassName: 'col-b'2492 },2493 {2494 name: 'c',2495 accessor: 'c',2496 footer: 'ftr',2497 className: 'col-c',2498 headerClassName: 'col-c',2499 footerClassName: 'col-c'2500 },2501 {2502 name: 'd',2503 accessor: 'd',2504 footer: 'ftr',2505 sticky: 'left',2506 className: 'col-d',2507 headerClassName: 'col-d',2508 footerClassName: 'col-d'2509 }2510 ],2511 columnGroups: [2512 { name: 'group-ab', columns: ['a', 'b'], sticky: 'left' },2513 { name: 'group-c', columns: ['c'] },2514 { name: 'group-d', columns: ['d'], sticky: 'left' }2515 ],2516 filterable: true,2517 resizable: true2518 }2519 const { container } = render(<Reactable {...props} />)2520 const [headerA, headerB] = getColumnHeaders(container)2521 const resizerA = getResizers(headerA)[0]2522 const resizerB = getResizers(headerB)[0]2523 expect(headerA).toHaveStyle('width: 100px; flex: 100 0 auto')2524 expect(headerB).toHaveStyle('width: 100px; flex: 100 0 auto')2525 // Mock the DOM widths, which can be different from style.width2526 headerA.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2527 headerB.getBoundingClientRect = jest.fn(() => ({ width: 120 }))2528 // Resizing header 120+70px2529 fireEvent.mouseDown(resizerA, { clientX: 0 })2530 fireEvent.mouseMove(resizerA, { clientX: 70 })2531 fireEvent.mouseUp(resizerA, { clientX: 70 })2532 expect(headerA).toHaveStyle('width: 190px; flex: 0 0 auto')2533 // Resizing header 120+50px2534 fireEvent.mouseDown(resizerB, { clientX: 0 })2535 fireEvent.mouseMove(resizerB, { clientX: 50 })2536 fireEvent.mouseUp(resizerB, { clientX: 50 })2537 expect(headerB).toHaveStyle('width: 170px; flex: 0 0 auto')2538 const cellsA = container.querySelectorAll('.col-a')2539 cellsA.forEach(cell => expect(cell).toHaveStyle('position: sticky; left: 0'))2540 cellsA.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2541 const cellsB = container.querySelectorAll('.col-b')2542 cellsB.forEach(cell => expect(cell).toHaveStyle('position: sticky; left: 190px'))2543 cellsB.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2544 const cellsC = container.querySelectorAll('.col-c')2545 cellsC.forEach(cell => expect(cell).not.toHaveStyle('position: sticky'))2546 cellsC.forEach(cell => expect(cell).not.toHaveClass('rt-sticky'))2547 const cellsD = container.querySelectorAll('.col-d')2548 cellsD.forEach(cell => expect(cell).toHaveStyle('position: sticky; left: 360px'))2549 cellsD.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2550 const [groupAB, groupC, groupD] = getGroupHeaders(container)2551 expect(groupAB).toHaveStyle('position: sticky; left: 0')2552 expect(groupAB).toHaveClass('rt-sticky')2553 expect(groupC).not.toHaveStyle('position: sticky')2554 expect(groupC).not.toHaveClass('rt-sticky')2555 expect(groupD).toHaveStyle('position: sticky; left: 360px')2556 expect(groupD).toHaveClass('rt-sticky')2557 })2558 it('all columns in a group have the same sticky property', () => {2559 const props = {2560 data: { a: [1, 2], b: ['a', 'b'], c: ['c', 'd'], d: ['e', 'f'], e: [3, 4] },2561 columns: [2562 {2563 name: 'a - sticky col with ungrouped header',2564 accessor: 'a',2565 sticky: 'left',2566 className: 'col-a',2567 headerClassName: 'col-a'2568 },2569 {2570 name: 'b - group with different sticky props',2571 accessor: 'b',2572 sticky: 'left',2573 className: 'col-b',2574 headerClassName: 'col-b'2575 },2576 {2577 name: 'c - group with different sticky props',2578 accessor: 'c',2579 sticky: 'right',2580 className: 'col-c',2581 headerClassName: 'col-c'2582 },2583 {2584 name: 'd - non-sticky col with sticky group header',2585 accessor: 'd',2586 className: 'col-d',2587 headerClassName: 'col-d'2588 },2589 {2590 name: 'e - sticky col with sticky group header',2591 accessor: 'e',2592 sticky: 'left',2593 className: 'col-e',2594 headerClassName: 'col-e'2595 }2596 ],2597 columnGroups: [2598 { name: 'group-bc', columns: ['b', 'c'] },2599 { name: 'group-de', columns: ['d', 'e'], sticky: 'right' }2600 ]2601 }2602 const { container } = render(<Reactable {...props} />)2603 const [ungroupedA] = getUngroupedHeaders(container)2604 const [groupBC, groupDE] = getGroupHeaders(container)2605 expect(ungroupedA).toHaveStyle('position: sticky; left: 0')2606 expect(ungroupedA).toHaveClass('rt-sticky')2607 const cellsA = container.querySelectorAll('.col-a')2608 cellsA.forEach(cell => expect(cell).toHaveStyle('position: sticky; left: 0'))2609 cellsA.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2610 expect(groupBC).toHaveStyle('position: sticky; left: 100px')2611 expect(groupBC).toHaveClass('rt-sticky')2612 const cellsB = container.querySelectorAll('.col-b')2613 cellsB.forEach(cell => expect(cell).toHaveStyle('position: sticky; left: 100px'))2614 cellsB.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2615 const cellsC = container.querySelectorAll('.col-c')2616 cellsC.forEach(cell => expect(cell).toHaveStyle('position: sticky; left: 200px'))2617 cellsC.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2618 expect(groupDE).toHaveStyle('position: sticky; right: 0')2619 expect(groupDE).toHaveClass('rt-sticky')2620 const cellsD = container.querySelectorAll('.col-d')2621 cellsD.forEach(cell => expect(cell).toHaveStyle('position: sticky; right: 100px'))2622 cellsD.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2623 const cellsE = container.querySelectorAll('.col-e')2624 cellsE.forEach(cell => expect(cell).toHaveStyle('position: sticky; right: 0'))2625 cellsE.forEach(cell => expect(cell).toHaveClass('rt-sticky'))2626 })2627 it('sticky columns work with row highlighting and row striping', () => {2628 const props = {2629 data: { a: [1, 2], b: ['a', 'b'] },2630 columns: [2631 { name: 'a', accessor: 'a' },2632 { name: 'b', accessor: 'b' }2633 ],2634 columnGroups: [{ name: 'group-ab', columns: ['a', 'b'] }],2635 highlight: true,2636 striped: true,2637 minRows: 22638 }2639 const { container, rerender } = render(<Reactable {...props} />)2640 let rows = getRows(container)2641 rows.forEach((row, index) => {2642 expect(row).toHaveClass('rt-tr-highlight')2643 if (index % 2 === 0) {2644 expect(row).toHaveClass('rt-tr-striped')2645 }2646 })2647 const columns = [2648 { name: 'a', accessor: 'a', sticky: 'left' },2649 { name: 'b', accessor: 'b' }2650 ]2651 rerender(<Reactable {...props} columns={columns} />)2652 rows = getRows(container)2653 rows.forEach((row, index) => {2654 expect(row).not.toHaveClass('rt-tr-highlight')2655 expect(row).toHaveClass('rt-tr-highlight-sticky')2656 if (index % 2 === 0) {2657 expect(row).not.toHaveClass('rt-tr-striped')2658 expect(row).toHaveClass('rt-tr-striped-sticky')2659 }2660 })2661 })2662})2663describe('no data', () => {2664 it('renders no data message in table body', () => {2665 const props = {2666 data: { a: [] },2667 columns: [{ name: 'a', accessor: 'a' }]2668 }2669 const { container, queryAllByText, rerender } = render(<Reactable {...props} />)2670 const noData = queryAllByText('No rows found')2671 expect(noData).toHaveLength(1)2672 expect(noData[0]).toHaveAttribute('aria-live', 'assertive')2673 const tbody = getTbody(container)2674 expect(getNoData(tbody)).toBeVisible()2675 expect(tbody).toHaveClass('rt-tbody-no-data')2676 const dataRows = getDataRows(container)2677 expect(dataRows).toHaveLength(0)2678 const padRows = getPadRows(container)2679 expect(padRows).toHaveLength(1)2680 // Language2681 rerender(<Reactable {...props} language={{ noData: '_No rows found' }} />)2682 expect(getNoData(tbody).textContent).toEqual('_No rows found')2683 })2684 it('does not show message with data present', () => {2685 const props = {2686 data: { a: [1] },2687 columns: [{ name: 'a', accessor: 'a' }]2688 }2689 const { container, queryByText } = render(<Reactable {...props} />)2690 const noData = queryByText('No rows found')2691 expect(noData).toEqual(null)2692 const tbody = getTbody(container)2693 expect(tbody).not.toHaveClass('rt-tbody-no-data')2694 })2695 it('no data message element exists with data present', () => {2696 // Element must exist on page for ARIA live region to be announced2697 const props = {2698 data: { a: [1] },2699 columns: [{ name: 'a', accessor: 'a' }]2700 }2701 const { container } = render(<Reactable {...props} />)2702 const tbody = getTbody(container)2703 const noData = getNoData(tbody)2704 expect(noData).toHaveTextContent('')2705 expect(noData).toHaveAttribute('aria-live', 'assertive')2706 })2707})2708describe('keyboard focus styles', () => {2709 it('applies keyboard focus styles when using the keyboard', () => {2710 const props = {2711 data: { a: [1, 2] },2712 columns: [{ name: 'colA', accessor: 'a' }]2713 }2714 const { container } = render(<Reactable {...props} />)2715 const rootContainer = getRoot(container)2716 expect(rootContainer).not.toHaveClass('rt-keyboard-active')2717 fireEvent.mouseDown(rootContainer)2718 expect(rootContainer).not.toHaveClass('rt-keyboard-active')2719 fireEvent.keyDown(rootContainer)2720 expect(rootContainer).toHaveClass('rt-keyboard-active')2721 fireEvent.mouseDown(rootContainer)2722 expect(rootContainer).not.toHaveClass('rt-keyboard-active')2723 // Should detect tabbing into the table2724 fireEvent.keyUp(rootContainer)2725 expect(rootContainer).not.toHaveClass('rt-keyboard-active')2726 fireEvent.keyUp(rootContainer, { key: 'Tab', keyCode: 9, charCode: 9 })2727 expect(rootContainer).toHaveClass('rt-keyboard-active')2728 })2729})2730describe('scrollable tables are keyboard accessible', () => {2731 afterEach(() => {2732 delete window.ResizeObserver2733 })2734 it('table is not focusable when unscrollable', () => {2735 const props = {2736 data: { a: [1, 2], b: ['aa', 'bb'] },2737 columns: [2738 { name: 'colA', accessor: 'a' },2739 { name: 'colB', accessor: 'b' }2740 ]2741 }2742 const { container } = render(<Reactable {...props} />)2743 const table = getTable(container)2744 expect(table).toBeVisible()2745 expect(table).toHaveAttribute('tabindex', '-1')2746 })2747 it('table is focusable when horizontally scrollable', () => {2748 const props = {2749 data: { a: [1, 2], b: ['aa', 'bb'] },2750 columns: [2751 { name: 'colA', accessor: 'a' },2752 { name: 'colB', accessor: 'b' }2753 ]2754 }2755 let disconnectCount = 02756 window.ResizeObserver = class ResizeObserver {2757 constructor(cb) {2758 this.cb = cb2759 }2760 observe(el) {2761 // Element height/widths are all 0 in jsdom2762 Object.defineProperty(el, 'scrollWidth', { value: 2, configurable: true })2763 Object.defineProperty(el, 'clientWidth', { value: 1, configurable: true })2764 this.cb()2765 }2766 disconnect() {2767 disconnectCount += 12768 }2769 }2770 const { container, unmount } = render(<Reactable {...props} />)2771 const table = getTable(container)2772 expect(table).toBeVisible()2773 expect(table).toHaveAttribute('tabindex', '0')2774 unmount(<Reactable {...props} />)2775 expect(disconnectCount).toEqual(1)2776 })2777 it('table is focusable when vertically scrollable', () => {2778 const props = {2779 data: { a: [1, 2], b: ['aa', 'bb'] },2780 columns: [2781 { name: 'colA', accessor: 'a' },2782 { name: 'colB', accessor: 'b' }2783 ]2784 }2785 let disconnectCount = 02786 window.ResizeObserver = class ResizeObserver {2787 constructor(cb) {2788 this.cb = cb2789 }2790 observe(el) {2791 // Element height/widths are all 0 in jsdom2792 Object.defineProperty(el, 'scrollHeight', { value: 2, configurable: true })2793 Object.defineProperty(el, 'scrollHeight', { value: 1, configurable: true })2794 this.cb()2795 }2796 disconnect() {2797 disconnectCount += 12798 }2799 }2800 const { container, unmount } = render(<Reactable {...props} />)2801 const table = getTable(container)2802 expect(table).toBeVisible()2803 expect(table).toHaveAttribute('tabindex', '0')2804 unmount(<Reactable {...props} />)2805 expect(disconnectCount).toEqual(1)2806 })2807})2808describe('sorting', () => {2809 it('enables sorting', () => {2810 const props = {2811 data: { a: [1, 3, 2, 5], b: ['aa', 'CC', 'dd', 'BB'] },2812 columns: [2813 { name: 'colA', accessor: 'a', type: 'numeric', className: 'col-a' },2814 { name: 'colB', accessor: 'b', className: 'col-b' }2815 ]2816 }2817 const { container } = render(<Reactable {...props} />)2818 // Should be sortable by default2819 const headers = getSortableHeaders(container)2820 expect(headers.length).toEqual(2)2821 fireEvent.click(headers[0])2822 const cellsA = getCells(container, '.col-a')2823 expect([...cellsA].map(cell => cell.textContent)).toEqual(['1', '2', '3', '5'])2824 fireEvent.click(headers[0])2825 expect([...cellsA].map(cell => cell.textContent)).toEqual(['5', '3', '2', '1'])2826 fireEvent.click(headers[0])2827 expect([...cellsA].map(cell => cell.textContent)).toEqual(['1', '2', '3', '5'])2828 fireEvent.click(headers[1])2829 const cellsB = getCells(container, '.col-b')2830 expect([...cellsB].map(cell => cell.textContent)).toEqual(['aa', 'BB', 'CC', 'dd'])2831 fireEvent.click(headers[1])2832 expect([...cellsB].map(cell => cell.textContent)).toEqual(['dd', 'CC', 'BB', 'aa'])2833 fireEvent.click(headers[1])2834 expect([...cellsB].map(cell => cell.textContent)).toEqual(['aa', 'BB', 'CC', 'dd'])2835 })2836 it('disables sorting', () => {2837 // Sorting disabled globally2838 const props = {2839 data: { a: [1, 3, 2, 5], b: ['aa', 'CC', 'dd', 'BB'] },2840 columns: [2841 { name: 'colA', accessor: 'a', type: 'numeric', className: 'col-a' },2842 { name: 'colB', accessor: 'b', className: 'col-b' }2843 ],2844 sortable: false2845 }2846 const { container, rerender } = render(<Reactable {...props} />)2847 let sortHeaders = getSortableHeaders(container)2848 expect(sortHeaders.length).toEqual(0)2849 let headers = getHeaders(container)2850 fireEvent.click(headers[0])2851 const colA = container.querySelectorAll('.col-a')2852 expect([...colA].map(el => el.textContent)).toEqual(['1', '3', '2', '5'])2853 // Sorting disabled globally with column enable override2854 let columns = [2855 { name: 'colA', accessor: 'a', type: 'numeric', className: 'col-a', sortable: true },2856 { name: 'colB', accessor: 'b', className: 'col-b' }2857 ]2858 rerender(<Reactable {...props} columns={columns} />)2859 sortHeaders = container.querySelectorAll('[aria-sort]')2860 expect(sortHeaders.length).toEqual(1)2861 expect(sortHeaders[0].textContent).toEqual('colA')2862 // Sorting enabled globally with column disable override2863 columns = [2864 { name: 'colA', accessor: 'a', type: 'numeric', className: 'col-a', sortable: false },2865 { name: 'colB', accessor: 'b', className: 'col-b' }2866 ]2867 rerender(<Reactable {...props} columns={columns} sortable={true} />)2868 sortHeaders = container.querySelectorAll('[aria-sort]')2869 expect(sortHeaders.length).toEqual(1)2870 expect(sortHeaders[0].textContent).toEqual('colB')2871 })2872 it('multi-sorting', () => {2873 const { container } = render(2874 <Reactable2875 data={{ a: [1, 3, 1, 1], b: ['aa', 'CC', 'dd', 'BB'] }}2876 columns={[2877 { name: 'colA', accessor: 'a', type: 'numeric', className: 'col-a' },2878 { name: 'colB', accessor: 'b', className: 'col-b' }2879 ]}2880 />2881 )2882 const headers = getSortableHeaders(container)2883 expect(headers.length).toEqual(2)2884 // First multi-sort should work just like a regular sort2885 fireEvent.click(headers[0], { shiftKey: true })2886 const colA = container.querySelectorAll('.col-a')2887 expect([...colA].map(el => el.textContent)).toEqual(['1', '1', '1', '3'])2888 const colB = container.querySelectorAll('.col-b')2889 expect([...colB].map(el => el.textContent)).toEqual(['aa', 'dd', 'BB', 'CC'])2890 // Second multi-sort should resort the second column2891 fireEvent.click(headers[1], { shiftKey: true })2892 expect([...colB].map(el => el.textContent)).toEqual(['aa', 'BB', 'dd', 'CC'])2893 fireEvent.click(headers[1], { shiftKey: true })2894 expect([...colB].map(el => el.textContent)).toEqual(['dd', 'BB', 'aa', 'CC'])2895 // Multi-sort should reset sorted state on the third toggle2896 fireEvent.click(headers[1], { shiftKey: true })2897 expect([...colB].map(el => el.textContent)).toEqual(['aa', 'dd', 'BB', 'CC'])2898 // Regular sorting should clear multi-sort state2899 fireEvent.click(headers[1])2900 expect([...colB].map(el => el.textContent)).toEqual(['aa', 'BB', 'CC', 'dd'])2901 expect([...colA].map(el => el.textContent)).toEqual(['1', '1', '3', '1'])2902 })2903 it('defaultSortOrder', () => {2904 const props = {2905 data: { a: [1, 3, 1, 1], b: ['aa', 'CC', 'dd', 'BB'] },2906 columns: [2907 { name: 'colA', accessor: 'a', type: 'numeric', className: 'col-a' },2908 { name: 'colB', accessor: 'b', className: 'col-b' }2909 ]2910 }2911 const { container, rerender } = render(<Reactable {...props} />)2912 const headers = getSortableHeaders(container)2913 expect(headers.length).toEqual(2)2914 // Should default to ascending order2915 fireEvent.click(headers[0])2916 const colA = container.querySelectorAll('.col-a')2917 expect([...colA].map(el => el.textContent)).toEqual(['1', '1', '1', '3'])2918 // Descending order2919 rerender(<Reactable {...props} defaultSortDesc />)2920 fireEvent.click(headers[1])2921 const colB = container.querySelectorAll('.col-b')2922 expect([...colB].map(el => el.textContent)).toEqual(['dd', 'CC', 'BB', 'aa'])2923 // Ascending order with column override for descending order2924 let columns = [2925 { name: 'colA', accessor: 'a', type: 'numeric', className: 'col-a', defaultSortDesc: true },2926 { name: 'colB', accessor: 'b', className: 'col-b' }2927 ]2928 rerender(<Reactable {...props} columns={columns} />)2929 fireEvent.click(headers[0])2930 expect([...colA].map(el => el.textContent)).toEqual(['3', '1', '1', '1'])2931 // Descending order with column override for ascending order2932 columns = [2933 { name: 'colA', accessor: 'a', type: 'numeric', className: 'col-a' },2934 { name: 'colB', accessor: 'b', className: 'col-b', defaultSortDesc: false }2935 ]2936 rerender(<Reactable {...props} columns={columns} defaultSortDesc />)2937 fireEvent.click(headers[1])2938 expect([...colB].map(el => el.textContent)).toEqual(['aa', 'BB', 'CC', 'dd'])2939 })2940 it('defaultSorted', () => {2941 const props = {2942 data: { a: [1, 3, 1, 1], b: ['aa', 'CC', 'dd', 'BB'] },2943 columns: [2944 { name: 'colA', accessor: 'a', type: 'numeric', className: 'col-a' },2945 { name: 'colB', accessor: 'b', className: 'col-b' }2946 ]2947 }2948 // Default sorted in ascending order2949 const { container, rerender } = render(2950 <Reactable {...props} defaultSorted={[{ id: 'a', desc: false }]} />2951 )2952 const headers = getSortableHeaders(container)2953 expect(headers.length).toEqual(2)2954 const colA = container.querySelectorAll('.col-a')2955 expect([...colA].map(el => el.textContent)).toEqual(['1', '1', '1', '3'])2956 // Default sorted in descending order2957 rerender(<Reactable {...props} defaultSorted={[{ id: 'b', desc: true }]} />)2958 const colB = container.querySelectorAll('.col-b')2959 expect([...colB].map(el => el.textContent)).toEqual(['dd', 'CC', 'BB', 'aa'])2960 // Multiple default sorted2961 rerender(2962 <Reactable2963 {...props}2964 defaultSorted={[2965 { id: 'a', desc: false },2966 { id: 'b', desc: true }2967 ]}2968 />2969 )2970 expect([...colA].map(el => el.textContent)).toEqual(['1', '1', '1', '3'])2971 expect([...colB].map(el => el.textContent)).toEqual(['dd', 'BB', 'aa', 'CC'])2972 })2973 it('table updates when defaultSorted changes', () => {2974 const props = {2975 data: { a: [1, 3, 5, 1], b: ['aa', 'CC', 'dd', 'BB'] },2976 columns: [2977 { name: 'colA', accessor: 'a', type: 'numeric', className: 'col-a' },2978 { name: 'colB', accessor: 'b', className: 'col-b' }2979 ],2980 defaultSorted: [{ id: 'a', desc: false }]2981 }2982 const { container, rerender } = render(<Reactable {...props} />)2983 let colA = container.querySelectorAll('.col-a')2984 expect([...colA].map(el => el.textContent)).toEqual(['1', '1', '3', '5'])2985 rerender(<Reactable {...props} defaultSorted={[{ id: 'a', desc: true }]} />)2986 colA = container.querySelectorAll('.col-a')2987 expect([...colA].map(el => el.textContent)).toEqual(['5', '3', '1', '1'])2988 })2989 it('sorted state persists when data changes', () => {2990 const props = {2991 data: { a: [1, 3, 2, 5], b: ['aa', 'CC', 'dd', 'BB'] },2992 columns: [2993 { name: 'colA', accessor: 'a', type: 'numeric', className: 'col-a' },2994 { name: 'colB', accessor: 'b', className: 'col-b' }2995 ]2996 }2997 const { container, rerender } = render(<Reactable {...props} />)2998 const headers = getSortableHeaders(container)2999 expect(headers.length).toEqual(2)3000 fireEvent.click(headers[0])3001 let cellsA = getCells(container, '.col-a')3002 expect([...cellsA].map(cell => cell.textContent)).toEqual(['1', '2', '3', '5'])3003 rerender(<Reactable {...props} />)3004 cellsA = getCells(container, '.col-a')3005 expect([...cellsA].map(cell => cell.textContent)).toEqual(['1', '2', '3', '5'])3006 rerender(<Reactable {...props} data={{ a: [4, 2, 0], b: ['a', 'b', 'c'] }} />)3007 cellsA = getCells(container, '.col-a')3008 expect([...cellsA].map(cell => cell.textContent)).toEqual(['0', '2', '4'])3009 })3010 it('headers have aria attributes', () => {3011 const { container } = render(3012 <Reactable3013 data={{ a: [1, 2], b: ['aa', 'bb'], c: [true, false] }}3014 columns={[3015 { name: 'colA', accessor: 'a' },3016 { name: 'colB', accessor: 'b' },3017 { name: 'colC', accessor: 'c', sortable: false }3018 ]}3019 sortable3020 />3021 )3022 const headers = getHeaders(container)3023 expect(headers[0]).toHaveAttribute('aria-sort', 'none')3024 expect(headers[1]).toHaveAttribute('aria-sort', 'none')3025 expect(headers[0]).toHaveAttribute('aria-label', 'Sort colA')3026 expect(headers[1]).toHaveAttribute('aria-label', 'Sort colB')3027 expect(headers[0]).toHaveAttribute('role', 'columnheader')3028 expect(headers[1]).toHaveAttribute('role', 'columnheader')3029 fireEvent.click(headers[1])3030 expect(headers[1]).toHaveAttribute('aria-sort', 'ascending')3031 fireEvent.click(headers[1])3032 expect(headers[1]).toHaveAttribute('aria-sort', 'descending')3033 fireEvent.click(headers[1], { shiftKey: true })3034 expect(headers[1]).toHaveAttribute('aria-sort', 'none')3035 expect(headers[2]).not.toHaveAttribute('aria-sort')3036 expect(headers[2]).not.toHaveAttribute('aria-label')3037 expect(headers[2]).toHaveAttribute('role', 'columnheader')3038 })3039 it('sorting works with grouping and sub-rows', () => {3040 const props = {3041 data: {3042 group: ['group-x', 'group-x', 'group-x', 'group-y'],3043 a: [2, -3, 0, 5],3044 b: ['aaa', 'bbb', 'ccc', 'ddd']3045 },3046 columns: [3047 { name: 'col-group', accessor: 'group', className: 'col-group' },3048 {3049 name: 'col-a',3050 accessor: 'a',3051 type: 'numeric',3052 aggregate: values => (values.length === 3 ? 10 : 1),3053 className: 'col-a'3054 },3055 { name: 'col-b', accessor: 'b', aggregate: values => values.length, className: 'col-b' }3056 ],3057 pivotBy: ['group']3058 }3059 const { container, getByText } = render(<Reactable {...props} />)3060 // Should sort grouped cells3061 fireEvent.click(getByText('col-group'))3062 expect(getCellsText(container, '.col-group')).toEqual([3063 '\u200bgroup-x (3)',3064 '\u200bgroup-y (1)'3065 ])3066 fireEvent.click(getByText('col-group'))3067 expect(getCellsText(container, '.col-group')).toEqual([3068 '\u200bgroup-y (1)',3069 '\u200bgroup-x (3)'3070 ])3071 // Numeric columns3072 fireEvent.click(getByText('col-a'))3073 // Should sort by aggregate values first, then leaf values.3074 // group-y has the highest leaf value, but lowest aggregate value.3075 expect(getCellsText(container, '.col-group')).toEqual([3076 '\u200bgroup-y (1)',3077 '\u200bgroup-x (3)'3078 ])3079 fireEvent.click(getExpanders(container)[0])3080 fireEvent.click(getExpanders(container)[1])3081 expect(getCellsText(container, '.col-a')).toEqual(['1', '5', '10', '-3', '0', '2'])3082 fireEvent.click(getByText('col-a'))3083 expect(getCellsText(container, '.col-a')).toEqual(['10', '2', '0', '-3', '1', '5'])3084 // Non-numeric columns3085 fireEvent.click(getByText('col-b'))3086 expect(getCellsText(container, '.col-b')).toEqual(['1', 'ddd', '3', 'aaa', 'bbb', 'ccc'])3087 fireEvent.click(getByText('col-b'))3088 expect(getCellsText(container, '.col-b')).toEqual(['3', 'ccc', 'bbb', 'aaa', '1', 'ddd'])3089 })3090 it('sort language', () => {3091 const props = {3092 data: { a: [1, 2], b: ['aa', 'bb'] },3093 columns: [3094 { name: 'colA', accessor: 'a' },3095 { name: 'colB', accessor: 'b' }3096 ],3097 language: { sortLabel: '_Sort {name}' }3098 }3099 const { container } = render(<Reactable {...props} />)3100 const headers = getSortableHeaders(container)3101 expect(headers[0]).toHaveAttribute('aria-label', '_Sort colA')3102 expect(headers[1]).toHaveAttribute('aria-label', '_Sort colB')3103 })3104 it('can be navigated with keyboard', () => {3105 const props = {3106 data: { a: [1, 2], b: ['aa', 'bb'], c: [true, false] },3107 columns: [3108 { name: 'colA', accessor: 'a' },3109 { name: 'colB', accessor: 'b', defaultSortDesc: true },3110 { name: 'colC', accessor: 'c', sortable: false }3111 ],3112 sortable: true3113 }3114 const { container } = render(<Reactable {...props} />)3115 const headers = getHeaders(container)3116 expect(headers[0]).toHaveAttribute('tabindex', '0')3117 expect(headers[1]).toHaveAttribute('tabindex', '0')3118 expect(headers[2]).not.toHaveAttribute('tabindex')3119 // Should be toggleable using enter or space key3120 fireEvent.keyPress(headers[0], { key: 'Enter', keyCode: 13, charCode: 13 })3121 expect(headers[0]).toHaveAttribute('aria-sort', 'ascending')3122 fireEvent.keyPress(headers[0], { key: ' ', keyCode: 32, charCode: 32 })3123 expect(headers[0]).toHaveAttribute('aria-sort', 'descending')3124 fireEvent.keyPress(headers[0], { key: 'Enter', keyCode: 13, charCode: 13, shiftKey: true })3125 expect(headers[0]).toHaveAttribute('aria-sort', 'none')3126 })3127 it('shows focus indicators when navigating using keyboard', () => {3128 const props = {3129 data: { a: [1, 2], b: ['aa', 'bb'], c: [true, false] },3130 columns: [3131 { name: 'colA', accessor: 'a' },3132 { name: 'colB', accessor: 'b', defaultSortDesc: true },3133 { name: 'colC', accessor: 'c', sortable: false }3134 ],3135 sortable: true3136 }3137 const { container } = render(<Reactable {...props} />)3138 const headers = getHeaders(container)3139 expect(headers[0]).toHaveAttribute('data-sort-hint', 'ascending')3140 expect(headers[1]).toHaveAttribute('data-sort-hint', 'descending')3141 expect(headers[2]).not.toHaveAttribute('data-sort-hint')3142 })3143 it('clicking resizer does not toggle sorting', () => {3144 const props = {3145 data: { a: [1, 2], b: ['aa', 'bb'], c: [true, false] },3146 columns: [3147 { name: 'colA', accessor: 'a' },3148 { name: 'colB', accessor: 'b' },3149 { name: 'colC', accessor: 'c' }3150 ],3151 resizable: true3152 }3153 const { container } = render(<Reactable {...props} />)3154 const [headerA] = getHeaders(container)3155 expect(headerA).toHaveAttribute('aria-sort', 'none')3156 // Resize header3157 const [resizerA] = getResizers(container)3158 fireEvent.mouseDown(resizerA, { clientX: 0 })3159 fireEvent.mouseMove(resizerA, { clientX: 70 })3160 fireEvent.mouseUp(resizerA, { clientX: 70 })3161 fireEvent.click(headerA)3162 expect(headerA).toHaveAttribute('aria-sort', 'none')3163 })3164 it('shows or hides sort icons', () => {3165 const props = {3166 data: { a: [1, 2], b: ['aa', 'bb'] },3167 columns: [3168 { name: 'colA', accessor: 'a', type: 'numeric' },3169 { name: 'colB', accessor: 'b' }3170 ]3171 }3172 const { container, rerender } = render(<Reactable {...props} />)3173 const numericSortIcon = container.querySelectorAll('.rt-sort-left')3174 expect(numericSortIcon).toHaveLength(1)3175 const defaultSortIcon = container.querySelectorAll('.rt-sort-right')3176 expect(defaultSortIcon).toHaveLength(1)3177 // Hide sort icons3178 rerender(<Reactable {...props} showSortIcon={false} />)3179 expect(container.querySelector('.rt-sort-left')).toEqual(null)3180 expect(container.querySelector('.rt-sort-right')).toEqual(null)3181 })3182 it('shows sortable columns', () => {3183 const props = {3184 data: { a: [1, 2], b: ['aa', 'bb'], c: [true, false] },3185 columns: [3186 { name: 'colA', accessor: 'a', type: 'numeric' },3187 { name: 'colB', accessor: 'b', sortable: false },3188 { name: 'colC', accessor: 'c' }3189 ],3190 showSortable: true3191 }3192 const { container } = render(<Reactable {...props} />)3193 const headers = getHeaders(container)3194 expect(headers[0].querySelector('.rt-sort-left.rt-sort')).toBeVisible()3195 expect(headers[1].querySelector('.rt-sort')).toEqual(null)3196 expect(headers[2].querySelector('.rt-sort-right.rt-sort')).toBeVisible()3197 })3198 it('sorts missing values last', () => {3199 const props = {3200 data: { a: [2, 'NA', 1, 3], b: ['aa', null, null, 'BB'] },3201 columns: [3202 {3203 name: 'colA',3204 accessor: 'a',3205 type: 'numeric',3206 sortNALast: true,3207 className: 'col-a'3208 },3209 { name: 'colB', accessor: 'b', sortNALast: true, className: 'col-b' }3210 ],3211 minRows: 43212 }3213 const { container } = render(<Reactable {...props} />)3214 const headers = getSortableHeaders(container)3215 expect(headers.length).toEqual(2)3216 fireEvent.click(headers[0])3217 const cellsA = getCells(container, '.col-a')3218 expect([...cellsA].map(cell => cell.textContent)).toEqual(['1', '2', '3', '\u200b'])3219 fireEvent.click(headers[0])3220 expect([...cellsA].map(cell => cell.textContent)).toEqual(['3', '2', '1', '\u200b'])3221 fireEvent.click(headers[1])3222 const cellsB = getCells(container, '.col-b')3223 expect([...cellsB].map(cell => cell.textContent)).toEqual(['aa', 'BB', '\u200b', '\u200b'])3224 fireEvent.click(headers[1])3225 expect([...cellsB].map(cell => cell.textContent)).toEqual(['BB', 'aa', '\u200b', '\u200b'])3226 })3227})3228describe('filtering', () => {3229 it('renders filters', () => {3230 const props = {3231 data: { a: [1, 2], b: ['a', 'b'] },3232 columns: [3233 { name: 'colA', accessor: 'a', headerClassName: 'header', headerStyle: { color: 'red' } },3234 { name: 'colB', accessor: 'b', className: 'cell', style: { color: 'blue' } }3235 ],3236 filterable: true3237 }3238 const { container } = render(<Reactable {...props} />)3239 const filterRow = getFilterRow(container)3240 expect(filterRow).toHaveAttribute('role', 'row')3241 const filterCells = getFilterCells(container)3242 expect(filterCells).toHaveLength(2)3243 filterCells.forEach(cell => expect(cell).toHaveAttribute('role', 'cell'))3244 // Should not have colspan attribute (from react-table)3245 filterCells.forEach(cell => expect(cell).not.toHaveAttribute('colspan'))3246 expect(filterCells[0]).toHaveClass('header')3247 expect(filterCells[0]).toHaveStyle('color: red')3248 expect(filterCells[1]).not.toHaveClass('cell')3249 expect(filterCells[1]).not.toHaveStyle('color: blue')3250 const filters = getFilters(container)3251 expect(filters).toHaveLength(2)3252 expect(filters[0]).toHaveAttribute('aria-label', 'Filter colA')3253 expect(filters[1]).toHaveAttribute('aria-label', 'Filter colB')3254 expect(filters[0].placeholder).toEqual('')3255 expect(filters[1].placeholder).toEqual('')3256 })3257 it('is not filterable by default', () => {3258 const props = {3259 data: { a: [1, 2], b: ['a', 'b'] },3260 columns: [3261 { name: 'a', accessor: 'a' },3262 { name: 'b', accessor: 'b' }3263 ]3264 }3265 const { container } = render(<Reactable {...props} />)3266 const filterRow = getFilterRow(container)3267 expect(filterRow).toEqual(null)3268 const filters = getFilters(container)3269 const filterCells = getFilterCells(container)3270 expect(filters).toHaveLength(0)3271 expect(filterCells).toHaveLength(0)3272 })3273 it('column filterable should override global filterable', () => {3274 // Column with filtering disabled3275 let props = {3276 data: { a: [1, 2], b: ['a', 'b'], c: ['c', 'd'] },3277 columns: [3278 { name: 'colA', accessor: 'a', headerClassName: 'header', headerStyle: { color: 'red' } },3279 { name: 'colB', accessor: 'b', className: 'cell', style: { color: 'blue' } },3280 { name: 'colC', accessor: 'c', filterable: false }3281 ],3282 filterable: true3283 }3284 const { container, rerender } = render(<Reactable {...props} />)3285 let filterCells = getFilterCells(container)3286 expect(filterCells).toHaveLength(3)3287 let filters = getFilters(container)3288 expect(filters).toHaveLength(2)3289 expect(getFilters(filterCells[2])).toHaveLength(0)3290 // Column with filtering enabled3291 props = {3292 ...props,3293 columns: [3294 { name: 'colA', accessor: 'a', headerClassName: 'header', headerStyle: { color: 'red' } },3295 { name: 'colB', accessor: 'b', className: 'cell', style: { color: 'blue' } },3296 { name: 'colC', accessor: 'c', filterable: true }3297 ],3298 filterable: false3299 }3300 rerender(<Reactable {...props} />)3301 filterCells = getFilterCells(container)3302 expect(filterCells).toHaveLength(3)3303 filters = getFilters(container)3304 expect(filters).toHaveLength(1)3305 expect(getFilters(filterCells[2])).toHaveLength(1)3306 })3307 it('should not render filters for hidden columns', () => {3308 const props = {3309 data: { a: [1, 2], b: ['a', 'b'] },3310 columns: [3311 { name: 'colA', accessor: 'a', show: false },3312 { name: 'colB', accessor: 'b' }3313 ],3314 filterable: true3315 }3316 const { container } = render(<Reactable {...props} />)3317 const filterCells = getFilterCells(container)3318 expect(filterCells).toHaveLength(1)3319 const filters = getFilters(container)3320 expect(filters).toHaveLength(1)3321 expect(filters[0]).toHaveAttribute('aria-label', 'Filter colB')3322 })3323 it('filters numeric columns', () => {3324 const { container, getByText } = render(3325 <Reactable3326 data={{ a: [111, 115, 32.11] }}3327 columns={[{ name: 'a', accessor: 'a', type: 'numeric' }]}3328 filterable3329 />3330 )3331 const filter = getFilters(container)[0]3332 fireEvent.change(filter, { target: { value: '11' } })3333 let rows = getDataRows(container)3334 expect(rows).toHaveLength(2)3335 expect(getByText('111')).toBeVisible()3336 expect(getByText('115')).toBeVisible()3337 // No matches3338 fireEvent.change(filter, { target: { value: '5' } })3339 rows = getDataRows(container)3340 expect(rows).toHaveLength(0)3341 expect(getByText('No rows found')).toBeVisible()3342 const padRows = getPadRows(container)3343 expect(padRows).toHaveLength(1)3344 // Clear filter3345 fireEvent.change(filter, { target: { value: '' } })3346 rows = getDataRows(container)3347 expect(rows).toHaveLength(3)3348 })3349 it('filters string columns', () => {3350 const { container, getByText, queryByText } = render(3351 <Reactable3352 data={{ a: ['aaac', 'bbb', 'CCC'], b: ['ááád', 'bAb', 'CC'] }}3353 columns={[3354 { name: 'a', accessor: 'a', type: 'factor' },3355 { name: 'b', accessor: 'b', type: 'character' }3356 ]}3357 filterable3358 />3359 )3360 const filters = getFilters(container)3361 // Case-insensitive3362 fireEvent.change(filters[0], { target: { value: 'Bb' } })3363 let rows = getDataRows(container)3364 expect(rows).toHaveLength(1)3365 expect(getByText('bbb')).toBeVisible()3366 // Substring matches3367 fireEvent.change(filters[0], { target: { value: 'c' } })3368 rows = getDataRows(container)3369 expect(rows).toHaveLength(2)3370 expect(getByText('aaac')).toBeVisible()3371 expect(getByText('CCC')).toBeVisible()3372 // No matches3373 fireEvent.change(filters[0], { target: { value: 'cccc' } })3374 rows = getDataRows(container)3375 expect(rows).toHaveLength(0)3376 expect(getByText('No rows found')).toBeVisible()3377 // Clear filter3378 fireEvent.change(filters[0], { target: { value: '' } })3379 rows = getDataRows(container)3380 expect(rows).toHaveLength(3)3381 // Diacritics3382 fireEvent.change(filters[1], { target: { value: 'á' } })3383 rows = getDataRows(container)3384 expect(rows).toHaveLength(1)3385 expect(queryByText('ááád')).toBeVisible()3386 })3387 it('filters other types of columns', () => {3388 const { container, getByText } = render(3389 <Reactable3390 data={{ a: ['ááád', '123', 'acCC', '2018-03-05'] }}3391 columns={[{ name: 'a', accessor: 'a' }]}3392 filterable3393 />3394 )3395 const filter = getFilters(container)[0]3396 // Case-insensitive3397 fireEvent.change(filter, { target: { value: 'acc' } })3398 let rows = getDataRows(container)3399 expect(rows).toHaveLength(1)3400 expect(getByText('acCC')).toBeVisible()3401 // Substring matches3402 fireEvent.change(filter, { target: { value: '03-05' } })3403 rows = getDataRows(container)3404 expect(rows).toHaveLength(1)3405 expect(getByText('2018-03-05')).toBeVisible()3406 // Not locale-sensitive3407 fireEvent.change(filter, { target: { value: 'aaa' } })3408 rows = getDataRows(container)3409 expect(rows).toHaveLength(0)3410 expect(getByText('No rows found')).toBeVisible()3411 // Clear filter3412 fireEvent.change(filter, { target: { value: '' } })3413 rows = getDataRows(container)3414 expect(rows).toHaveLength(4)3415 })3416 it('filtering works with column groups', () => {3417 const { container, getByText } = render(3418 <Reactable3419 data={{ a: ['aaac', 'bbb', 'CCC'], b: ['ááád', 'bAb', 'CC'] }}3420 columns={[3421 { name: 'a', accessor: 'a' },3422 { name: 'b', accessor: 'b' }3423 ]}3424 columnGroups={[3425 {3426 columns: ['a', 'b'],3427 name: 'group-1'3428 }3429 ]}3430 filterable3431 />3432 )3433 const filterCells = getFilterCells(container)3434 expect(filterCells).toHaveLength(2)3435 const filters = getFilters(container)3436 expect(filters).toHaveLength(2)3437 fireEvent.change(filters[0], { target: { value: 'Bb' } })3438 let rows = getDataRows(container)3439 expect(rows).toHaveLength(1)3440 expect(getByText('bbb')).toBeVisible()3441 })3442 it('filtering works with grouping and sub-rows', () => {3443 const props = {3444 data: {3445 group: ['group-x', 'group-x', 'group-x', 'group-y'],3446 a: [1, 1, 2, 41],3447 b: ['aaa', 'bbb', 'aaa', 'bbb']3448 },3449 columns: [3450 { name: 'group', accessor: 'group' },3451 { name: 'col-a', accessor: 'a', type: 'numeric', aggregate: 'sum', className: 'col-a' },3452 { name: 'col-b', accessor: 'b', aggregate: () => 'ccc' }3453 ],3454 filterable: true,3455 pivotBy: ['group']3456 }3457 const { container } = render(<Reactable {...props} />)3458 const filters = getFilters(container)3459 // Numeric column3460 expect(getCellsText(container, '.col-a')).toEqual(['4', '41'])3461 fireEvent.change(filters[1], { target: { value: '1' } })3462 expect(getRows(container)).toHaveLength(1)3463 expect([...getDataCells(container)].map(cell => cell.textContent)).toEqual([3464 '\u200bgroup-x (2)',3465 '2', // Aggregate functions should work on filtered data3466 'ccc'3467 ])3468 // Non-numeric column3469 fireEvent.change(filters[2], { target: { value: 'b' } })3470 expect([...getDataCells(container)].map(cell => cell.textContent)).toEqual([3471 '\u200bgroup-x (1)',3472 '1',3473 'ccc'3474 ])3475 // Searching should work on grouped cells3476 fireEvent.change(filters[1], { target: { value: '' } })3477 fireEvent.change(filters[2], { target: { value: '' } })3478 fireEvent.change(filters[0], { target: { value: 'group-x' } })3479 expect([...getDataCells(container)].map(cell => cell.textContent)).toEqual([3480 '\u200bgroup-x (3)',3481 '4',3482 'ccc'3483 ])3484 })3485 it('filtered state should be available in cellInfo, colInfo, and state info', () => {3486 let lastCellInfo = {}3487 let lastState = {}3488 let lastColInfo = {}3489 const props = {3490 data: { a: ['aaa1', 'aaa2'], b: ['aaa', 'bbb'] },3491 columns: [3492 { name: 'a', accessor: 'a' },3493 {3494 name: 'b',3495 accessor: 'b',3496 cell: (cellInfo, state) => {3497 lastCellInfo.cell = cellInfo3498 lastState.cell = state3499 },3500 header: (colInfo, state) => {3501 lastColInfo.header = colInfo3502 lastState.header = state3503 },3504 footer: (colInfo, state) => {3505 lastColInfo.footer = colInfo3506 lastState.footer = state3507 }3508 }3509 ],3510 details: (rowInfo, state) => (lastState.details = state),3511 filterable: true3512 }3513 const { container } = render(<Reactable {...props} />)3514 const [filterA, filterB] = getFilters(container)3515 Object.values(lastCellInfo).forEach(cellInfo => expect(cellInfo.filterValue).toEqual(undefined))3516 Object.values(lastColInfo).forEach(colInfo =>3517 expect(colInfo.column.filterValue).toEqual(undefined)3518 )3519 Object.values(lastState).forEach(state => expect(state.filters).toEqual([]))3520 fireEvent.change(filterB, { target: { value: 'bb' } })3521 Object.values(lastCellInfo).forEach(cellInfo => expect(cellInfo.filterValue).toEqual('bb'))3522 Object.values(lastColInfo).forEach(colInfo => expect(colInfo.column.filterValue).toEqual('bb'))3523 Object.values(lastState).forEach(state =>3524 expect(state.filters).toEqual([{ id: 'b', value: 'bb' }])3525 )3526 fireEvent.change(filterA, { target: { value: 'a' } })3527 Object.values(lastCellInfo).forEach(cellInfo => expect(cellInfo.filterValue).toEqual('bb'))3528 Object.values(lastColInfo).forEach(colInfo => expect(colInfo.column.filterValue).toEqual('bb'))3529 Object.values(lastState).forEach(state =>3530 expect(state.filters).toEqual([3531 { id: 'b', value: 'bb' },3532 { id: 'a', value: 'a' }3533 ])3534 )3535 // When filter is cleared, filter value should be unset, not an empty string3536 fireEvent.change(filterB, { target: { value: '' } })3537 Object.values(lastCellInfo).forEach(cellInfo => expect(cellInfo.filterValue).toEqual(undefined))3538 Object.values(lastColInfo).forEach(colInfo =>3539 expect(colInfo.column.filterValue).toEqual(undefined)3540 )3541 Object.values(lastState).forEach(state =>3542 expect(state.filters).toEqual([{ id: 'a', value: 'a' }])3543 )3544 })3545 it('filtered state persists when data changes', () => {3546 const props = {3547 data: { a: ['aaa1', 'aaa2'], b: ['a', 'b'] },3548 columns: [3549 { name: 'a', accessor: 'a' },3550 { name: 'b', accessor: 'b' }3551 ],3552 filterable: true3553 }3554 const { container, getByText, rerender } = render(<Reactable {...props} />)3555 let filter = getFilters(container)[0]3556 fireEvent.change(filter, { target: { value: 'aaa2' } })3557 let rows = getDataRows(container)3558 expect(rows).toHaveLength(1)3559 expect(getByText('aaa2')).toBeVisible()3560 rerender(<Reactable {...props} data={{ a: ['aaa2', 'aaa222', 'bcd'], b: ['a', 'b', 'c'] }} />)3561 rows = getDataRows(container)3562 expect(rows).toHaveLength(2)3563 expect(getByText('aaa2')).toBeVisible()3564 expect(getByText('aaa222')).toBeVisible()3565 expect(filter.value).toEqual('aaa2')3566 })3567 it('filtered state resets when filterable changes to false', () => {3568 const props = {3569 data: { a: ['aaa1', 'aaa2'], b: ['a', 'b'] },3570 columns: [3571 { name: 'a', accessor: 'a' },3572 { name: 'b', accessor: 'b' }3573 ],3574 searchable: true3575 }3576 const { container, getByText, rerender } = render(<Reactable {...props} filterable />)3577 let filter = getFilters(container)[0]3578 fireEvent.change(filter, { target: { value: 'aaa2' } })3579 expect(getDataRows(container)).toHaveLength(1)3580 expect(getByText('aaa2')).toBeVisible()3581 // All other state should persist, including searched state3582 let searchInput = getSearchInput(container)3583 fireEvent.change(searchInput, { target: { value: 'a' } })3584 rerender(<Reactable {...props} />)3585 expect(getFilters(container)).toHaveLength(0)3586 expect(getDataRows(container)).toHaveLength(2)3587 searchInput = getSearchInput(container)3588 expect(searchInput.value).toEqual('a')3589 rerender(<Reactable {...props} filterable />)3590 expect(getDataRows(container)).toHaveLength(2)3591 filter = getFilters(container)[0]3592 expect(filter.value).toEqual('')3593 fireEvent.change(filter, { target: { value: 'aaa2' } })3594 rerender(<Reactable {...props} />)3595 expect(getDataRows(container)).toHaveLength(2)3596 })3597 it('filter language', () => {3598 const props = {3599 data: { a: [1, 2], b: ['a', 'b'] },3600 columns: [3601 { name: 'column-a', accessor: 'a' },3602 { name: 'column-b', accessor: 'b' }3603 ],3604 filterable: true,3605 language: {3606 filterPlaceholder: 'All',3607 filterLabel: '_Filter {name}'3608 }3609 }3610 const { container } = render(<Reactable {...props} />)3611 const filters = getFilters(container)3612 expect(filters[0]).toHaveAttribute('aria-label', '_Filter column-a')3613 expect(filters[1]).toHaveAttribute('aria-label', '_Filter column-b')3614 expect(filters[0].placeholder).toEqual('All')3615 expect(filters[1].placeholder).toEqual('All')3616 })3617 it('searchable should not make unfilterable columns filterable', () => {3618 // Should not rely on column.canFilter for column filtering, since3619 // useGlobalFilter sets column.canFilter on globally filterable columns.3620 // https://github.com/tannerlinsley/react-table/issues/27873621 const props = {3622 data: { a: [1, 2], b: ['a', 'b'] },3623 columns: [3624 { name: 'column-a', accessor: 'a' },3625 { name: 'column-b', accessor: 'b', filterable: true }3626 ],3627 searchable: true3628 }3629 const { container } = render(<Reactable {...props} />)3630 expect(getFilters(container)).toHaveLength(1)3631 const searchInput = getSearchInput(container)3632 fireEvent.change(searchInput, { target: { value: 'Bb' } })3633 expect(getFilters(container)).toHaveLength(1)3634 })3635 it('custom filter method', () => {3636 const props = {3637 data: { a: ['aaa1', 'aaa2', 'aaa3'], b: ['a', 'b', 'c'] },3638 columns: [3639 {3640 name: 'a',3641 accessor: 'a',3642 filterMethod: function exactMatch(rows, columnId, filterValue) {3643 expect(rows).toHaveLength(3)3644 expect(columnId).toEqual('a')3645 return rows.filter(row => {3646 return row.values[columnId] === filterValue3647 })3648 }3649 },3650 {3651 name: 'b',3652 accessor: 'b',3653 filterMethod: function rowIndexMatch(rows, columnId, filterValue) {3654 const indices = filterValue.split(',').map(Number)3655 return rows.filter(row => {3656 return indices.includes(row.index)3657 })3658 }3659 }3660 ],3661 filterable: true3662 }3663 const { container, getByText } = render(<Reactable {...props} />)3664 const [filterA, filterB] = getFilters(container)3665 fireEvent.change(filterA, { target: { value: 'aaa' } })3666 expect(getDataRows(container)).toHaveLength(0)3667 fireEvent.change(filterA, { target: { value: 'aaa2' } })3668 expect(getDataRows(container)).toHaveLength(1)3669 expect(getByText('aaa2')).toBeVisible()3670 fireEvent.change(filterA, { target: { value: '' } })3671 expect(getDataRows(container)).toHaveLength(3)3672 fireEvent.change(filterB, { target: { value: 'a' } })3673 expect(getDataRows(container)).toHaveLength(0)3674 fireEvent.change(filterB, { target: { value: '2,1' } })3675 expect(getDataRows(container)).toHaveLength(2)3676 expect(getByText('aaa3')).toBeVisible()3677 expect(getByText('aaa2')).toBeVisible()3678 fireEvent.change(filterB, { target: { value: '' } })3679 expect(getDataRows(container)).toHaveLength(3)3680 })3681 it('custom filter inputs', () => {3682 const CustomSelectFilter = (column, state) => {3683 expect(column.id).toEqual('a')3684 expect(column.name).toEqual('filter-component')3685 expect(state.page).toEqual(0)3686 expect(state.pageSize).toEqual(10)3687 expect(state.data).toEqual([3688 { a: 'aaac', b: 1, c: 4 },3689 { a: 'bbb', b: 2, c: 5 },3690 { a: 'CCC', b: 3, c: 6 }3691 ])3692 return (3693 <select3694 className="filter-component"3695 value={column.filterValue}3696 onChange={e => column.setFilter(e.target.value || undefined)}3697 >3698 <option value="">All</option>3699 <option value="Bb">Bb</option>3700 <option value="c">c</option>3701 <option value="cccc">cccc</option>3702 </select>3703 )3704 }3705 const { container, getByText } = render(3706 <Reactable3707 data={{ a: ['aaac', 'bbb', 'CCC'], b: [1, 2, 3], c: [4, 5, 6] }}3708 columns={[3709 { name: 'filter-component', accessor: 'a', filterInput: CustomSelectFilter },3710 {3711 name: 'filter-html',3712 accessor: 'b',3713 filterInput: '<input type="text" class="filter-html">',3714 html: true3715 },3716 {3717 name: 'filter-element',3718 accessor: 'c',3719 filterInput: <input className="filter-element"></input>3720 }3721 ]}3722 filterable3723 />3724 )3725 const defaultFilters = getFilters(container)3726 expect(defaultFilters).toHaveLength(0)3727 const filterHTMLCell = getFilterCells(container)[1]3728 expect(filterHTMLCell.innerHTML).toEqual(3729 '<div class="rt-td-inner"><div class="rt-text-content"><input type="text" class="filter-html"></div></div>'3730 )3731 const filterElement = container.querySelector('.filter-element')3732 expect(filterElement).toBeVisible()3733 const filter = container.querySelector('.filter-component')3734 expect(filter).toBeVisible()3735 // Case-insensitive3736 fireEvent.change(filter, { target: { value: 'Bb' } })3737 let rows = getDataRows(container)3738 expect(rows).toHaveLength(1)3739 expect(getByText('bbb')).toBeVisible()3740 // Substring matches3741 fireEvent.change(filter, { target: { value: 'c' } })3742 rows = getDataRows(container)3743 expect(rows).toHaveLength(2)3744 expect(getByText('aaac')).toBeVisible()3745 expect(getByText('CCC')).toBeVisible()3746 // No matches3747 fireEvent.change(filter, { target: { value: 'cccc' } })3748 rows = getDataRows(container)3749 expect(rows).toHaveLength(0)3750 expect(getByText('No rows found')).toBeVisible()3751 // Clear filter3752 fireEvent.change(filter, { target: { value: '' } })3753 rows = getDataRows(container)3754 expect(rows).toHaveLength(3)3755 })3756})3757describe('searching', () => {3758 it('enables searching', () => {3759 const props = {3760 data: { a: [1, 2], b: ['a', 'b'] },3761 columns: [3762 { name: 'a', accessor: 'a' },3763 { name: 'b', accessor: 'b' }3764 ]3765 }3766 const { container, rerender } = render(<Reactable {...props} />)3767 let searchInput = getSearchInput(container)3768 expect(searchInput).toEqual(null)3769 rerender(<Reactable {...props} searchable />)3770 searchInput = getSearchInput(container)3771 expect(searchInput).toBeVisible()3772 })3773 it('searches numeric columns', () => {3774 const { container, getByText } = render(3775 <Reactable3776 data={{ a: [111, 115, 32.11] }}3777 columns={[{ name: 'a', accessor: 'a', type: 'numeric' }]}3778 searchable3779 />3780 )3781 const searchInput = getSearchInput(container)3782 fireEvent.change(searchInput, { target: { value: '11' } })3783 let rows = getDataRows(container)3784 expect(rows).toHaveLength(2)3785 expect(getByText('111')).toBeVisible()3786 expect(getByText('115')).toBeVisible()3787 // No matches3788 fireEvent.change(searchInput, { target: { value: '5' } })3789 rows = getDataRows(container)3790 expect(rows).toHaveLength(0)3791 expect(getByText('No rows found')).toBeVisible()3792 // Clear search3793 fireEvent.change(searchInput, { target: { value: '' } })3794 rows = getDataRows(container)3795 expect(rows).toHaveLength(3)3796 })3797 it('searches string columns', () => {3798 const { container, getByText, queryByText } = render(3799 <Reactable3800 data={{ a: ['aaac', 'bbb', 'CCC'], b: ['ááád', 'bAb', 'CC'] }}3801 columns={[3802 { name: 'a', accessor: 'a', type: 'factor' },3803 { name: 'b', accessor: 'b', type: 'character' }3804 ]}3805 searchable3806 />3807 )3808 const searchInput = getSearchInput(container)3809 // Case-insensitive3810 fireEvent.change(searchInput, { target: { value: 'Bb' } })3811 let rows = getDataRows(container)3812 expect(rows).toHaveLength(1)3813 expect(getByText('bbb')).toBeVisible()3814 // Substring matches3815 fireEvent.change(searchInput, { target: { value: 'c' } })3816 rows = getDataRows(container)3817 expect(rows).toHaveLength(2)3818 expect(getByText('aaac')).toBeVisible()3819 expect(getByText('CCC')).toBeVisible()3820 // No matches3821 fireEvent.change(searchInput, { target: { value: 'cccc' } })3822 rows = getDataRows(container)3823 expect(rows).toHaveLength(0)3824 expect(getByText('No rows found')).toBeVisible()3825 // Clear search3826 fireEvent.change(searchInput, { target: { value: '' } })3827 rows = getDataRows(container)3828 expect(rows).toHaveLength(3)3829 // Diacritics3830 fireEvent.change(searchInput, { target: { value: 'á' } })3831 rows = getDataRows(container)3832 expect(rows).toHaveLength(1)3833 expect(queryByText('ááád')).toBeVisible()3834 })3835 it('searches other types of columns', () => {3836 const { container, getByText } = render(3837 <Reactable3838 data={{ a: ['ááád', '123', 'acCC', '2018-03-05'] }}3839 columns={[{ name: 'a', accessor: 'a' }]}3840 searchable3841 />3842 )3843 const searchInput = getSearchInput(container)3844 // Case-insensitive3845 fireEvent.change(searchInput, { target: { value: 'acc' } })3846 let rows = getDataRows(container)3847 expect(rows).toHaveLength(1)3848 expect(getByText('acCC')).toBeVisible()3849 // Substring matches3850 fireEvent.change(searchInput, { target: { value: '03-05' } })3851 rows = getDataRows(container)3852 expect(rows).toHaveLength(1)3853 expect(getByText('2018-03-05')).toBeVisible()3854 // Not locale-sensitive3855 fireEvent.change(searchInput, { target: { value: 'aaa' } })3856 rows = getDataRows(container)3857 expect(rows).toHaveLength(0)3858 expect(getByText('No rows found')).toBeVisible()3859 // Clear search3860 fireEvent.change(searchInput, { target: { value: '' } })3861 rows = getDataRows(container)3862 expect(rows).toHaveLength(4)3863 })3864 it('searching works with column groups', () => {3865 const { container, getByText } = render(3866 <Reactable3867 data={{ a: ['aaac', 'bbb', 'CCC'], b: ['ááád', 'bAb', 'CC'] }}3868 columns={[3869 { name: 'a', accessor: 'a' },3870 { name: 'b', accessor: 'b' }3871 ]}3872 columnGroups={[3873 {3874 columns: ['a', 'b'],3875 name: 'group-1'3876 }3877 ]}3878 searchable3879 />3880 )3881 const searchInput = getSearchInput(container)3882 fireEvent.change(searchInput, { target: { value: 'Bb' } })3883 let rows = getDataRows(container)3884 expect(rows).toHaveLength(1)3885 expect(getByText('bbb')).toBeVisible()3886 })3887 it('ignores columns with searching disabled', () => {3888 // Should ignore selection and details columns3889 const props = {3890 data: { a: [1, 2, 3], b: ['b', 'b', 'b'] },3891 columns: [3892 { name: 'a', accessor: 'a' },3893 { name: 'b', accessor: 'b', searchable: false }3894 ],3895 searchable: true3896 }3897 const { container } = render(<Reactable {...props} />)3898 const searchInput = getSearchInput(container)3899 fireEvent.change(searchInput, { target: { value: 'b' } })3900 expect(getDataRows(container)).toHaveLength(0)3901 })3902 it('ignores hidden columns by default', () => {3903 const props = {3904 data: { a: [1, 2, 3], b: ['b', 'b', 'b'] },3905 columns: [3906 { name: 'a', accessor: 'a' },3907 { name: 'b', accessor: 'b', show: false }3908 ],3909 searchable: true3910 }3911 const { container } = render(<Reactable {...props} />)3912 const searchInput = getSearchInput(container)3913 fireEvent.change(searchInput, { target: { value: 'b' } })3914 expect(getDataRows(container)).toHaveLength(0)3915 })3916 it('searches hidden columns with searching enabled', () => {3917 const props = {3918 data: { a: ['a1', 'a2', 'a3'], b: ['b11', 'b12', 'b2'] },3919 columns: [3920 { name: 'a', accessor: 'a' },3921 { name: 'b', accessor: 'b', show: false, searchable: true }3922 ],3923 searchable: true3924 }3925 const { container, getByText } = render(<Reactable {...props} />)3926 const searchInput = getSearchInput(container)3927 fireEvent.change(searchInput, { target: { value: 'b1' } })3928 expect(getDataRows(container)).toHaveLength(2)3929 expect(getByText('a1')).toBeVisible()3930 expect(getByText('a2')).toBeVisible()3931 })3932 it('ignores columns without data', () => {3933 const props = {3934 data: { a: [1, 2, 3], b: ['b', 'b', 'b'] },3935 columns: [3936 { name: 'a', accessor: 'a' },3937 { name: 'b', accessor: 'b' },3938 // Fake column for testing. Selection and row details columns now have3939 // searching disabled by default, so this shouldn't exist unless searching3940 // was manually enabled for the details column.3941 { name: '', accessor: '.fake_column' }3942 ],3943 searchable: true3944 }3945 const { container } = render(<Reactable {...props} />)3946 const searchInput = getSearchInput(container)3947 // If a column without data is searched, it should not string match on "undefined"3948 fireEvent.change(searchInput, { target: { value: 'undefined' } })3949 expect(getDataRows(container)).toHaveLength(0)3950 })3951 it('searching works when table has no rows', () => {3952 const props = {3953 data: { a: [], b: [] },3954 columns: [3955 { name: 'a', accessor: 'a' },3956 { name: 'b', accessor: 'b' }3957 ],3958 searchable: true3959 }3960 const { container } = render(<Reactable {...props} />)3961 const searchInput = getSearchInput(container)3962 fireEvent.change(searchInput, { target: { value: 'blargh' } })3963 let rows = getDataRows(container)3964 expect(rows).toHaveLength(0)3965 })3966 it('searching works with grouping and sub-rows', () => {3967 const props = {3968 data: {3969 group: ['group-x', 'group-x', 'group-x', 'group-y'],3970 a: [1, 1, 2, 41],3971 b: ['aaa', 'bbb', 'aaa', 'bbb']3972 },3973 columns: [3974 { name: 'group', accessor: 'group' },3975 { name: 'col-a', accessor: 'a', type: 'numeric', aggregate: 'sum', className: 'col-a' },3976 { name: 'col-b', accessor: 'b', aggregate: () => 'ccc' }3977 ],3978 searchable: true,3979 pivotBy: ['group']3980 }3981 const { container } = render(<Reactable {...props} />)3982 const searchInput = getSearchInput(container)3983 // Numeric column3984 expect(getCellsText(container, '.col-a')).toEqual(['4', '41'])3985 fireEvent.change(searchInput, { target: { value: '1' } })3986 expect(getRows(container)).toHaveLength(1)3987 expect(getCellsText(container)).toEqual([3988 '\u200bgroup-x (2)',3989 '2', // Aggregate functions should work on filtered data3990 'ccc'3991 ])3992 // Non-numeric column3993 fireEvent.change(searchInput, { target: { value: 'b' } })3994 expect(getCellsText(container)).toEqual([3995 '\u200bgroup-x (1)',3996 '1',3997 'ccc',3998 '\u200bgroup-y (1)',3999 '41',4000 'ccc'4001 ])4002 // Searching should work on grouped cells4003 fireEvent.change(searchInput, { target: { value: 'group-x' } })4004 expect(getCellsText(container)).toEqual(['\u200bgroup-x (3)', '4', 'ccc'])4005 })4006 it('searched state should be available in state info', () => {4007 let lastState = {}4008 const props = {4009 data: { a: ['aaa1', 'aaa2'], b: ['aaa', 'bbb'] },4010 columns: [4011 { name: 'a', accessor: 'a' },4012 {4013 name: 'b',4014 accessor: 'b',4015 cell: (cellInfo, state) => (lastState.cell = state),4016 header: (colInfo, state) => (lastState.header = state),4017 footer: (colInfo, state) => (lastState.footer = state)4018 }4019 ],4020 details: (rowInfo, state) => (lastState.details = state),4021 searchable: true4022 }4023 const { container } = render(<Reactable {...props} />)4024 const searchInput = getSearchInput(container)4025 Object.values(lastState).forEach(state => expect(state.searchValue).toEqual(undefined))4026 fireEvent.change(searchInput, { target: { value: 'aa' } })4027 Object.values(lastState).forEach(state => expect(state.searchValue).toEqual('aa'))4028 // When search is cleared, search value should be unset, not an empty string4029 fireEvent.change(searchInput, { target: { value: '' } })4030 Object.values(lastState).forEach(state => expect(state.searchValue).toEqual(undefined))4031 })4032 it('searched state persists when data changes', () => {4033 const props = {4034 data: { a: [1, 2], b: ['a', 'b'] },4035 columns: [4036 { name: 'a', accessor: 'a' },4037 { name: 'b', accessor: 'b' }4038 ],4039 searchable: true4040 }4041 const { container, getByText, rerender } = render(<Reactable {...props} />)4042 let searchInput = getSearchInput(container)4043 fireEvent.change(searchInput, { target: { value: 'b' } })4044 let rows = getDataRows(container)4045 expect(rows).toHaveLength(1)4046 rerender(<Reactable {...props} data={{ a: ['a', 'b', 'c'], b: ['x', 'y', 'bz'] }} />)4047 rows = getDataRows(container)4048 expect(rows).toHaveLength(2)4049 expect(getByText('y')).toBeVisible()4050 expect(getByText('bz')).toBeVisible()4051 expect(searchInput.value).toEqual('b')4052 })4053 it('searching updates when columns change', () => {4054 const props = {4055 data: { a: [111, 115, 32.11] },4056 columns: [{ name: 'a', accessor: 'a', type: 'numeric' }],4057 searchable: true4058 }4059 const { container, getByText, rerender } = render(<Reactable {...props} />)4060 const searchInput = getSearchInput(container)4061 fireEvent.change(searchInput, { target: { value: '11' } })4062 expect(getDataRows(container)).toHaveLength(2)4063 expect(getByText('111')).toBeVisible()4064 expect(getByText('115')).toBeVisible()4065 rerender(<Reactable {...props} columns={[{ name: 'a', accessor: 'a', type: 'character' }]} />)4066 expect(getDataRows(container)).toHaveLength(3)4067 expect(getByText('111')).toBeVisible()4068 expect(getByText('115')).toBeVisible()4069 expect(getByText('32.11')).toBeVisible()4070 })4071 it('searched state resets when searchable changes to false', () => {4072 const props = {4073 data: { a: ['aaa1', 'aaa2'], b: ['a', 'b'] },4074 columns: [4075 { name: 'a', accessor: 'a' },4076 { name: 'b', accessor: 'b' }4077 ],4078 filterable: true4079 }4080 const { container, getByText, rerender } = render(<Reactable {...props} searchable />)4081 let searchInput = getSearchInput(container)4082 fireEvent.change(searchInput, { target: { value: 'b' } })4083 expect(getDataRows(container)).toHaveLength(1)4084 expect(getByText('aaa2')).toBeVisible()4085 // All other state should persist, including filtered state4086 let filter = getFilters(container)[0]4087 fireEvent.change(filter, { target: { value: 'a' } })4088 rerender(<Reactable {...props} />)4089 expect(getDataRows(container)).toHaveLength(2)4090 expect(getSearchInput(container)).toEqual(null)4091 filter = getFilters(container)[0]4092 expect(filter.value).toEqual('a')4093 rerender(<Reactable {...props} searchable />)4094 expect(getDataRows(container)).toHaveLength(2)4095 searchInput = getSearchInput(container)4096 expect(searchInput.value).toEqual('')4097 fireEvent.change(searchInput, { target: { value: 'b' } })4098 rerender(<Reactable {...props} />)4099 expect(getDataRows(container)).toHaveLength(2)4100 })4101 it('search language', () => {4102 const props = {4103 data: { a: [1, 2] },4104 columns: [{ name: 'a', accessor: 'a' }],4105 searchable: true4106 }4107 const { container, rerender } = render(<Reactable {...props} />)4108 const searchInput = getSearchInput(container)4109 expect(searchInput.placeholder).toEqual('Search')4110 expect(searchInput).toHaveAttribute('aria-label', 'Search')4111 rerender(4112 <Reactable4113 {...props}4114 language={{ searchPlaceholder: '_search...', searchLabel: '_Search' }}4115 />4116 )4117 expect(searchInput.placeholder).toEqual('_search...')4118 expect(searchInput).toHaveAttribute('aria-label', '_Search')4119 })4120 it('custom search method', () => {4121 const props = {4122 data: { a: ['aaa1', 'aaa2', 'aaa3'], b: ['a', 'b', 'c'] },4123 columns: [4124 { name: 'a', accessor: 'a' },4125 { name: 'b', accessor: 'b' }4126 ],4127 searchable: true,4128 searchMethod: function exactTextAndRowIndexMatch(rows, columnIds, filterValue) {4129 expect(rows).toHaveLength(3)4130 expect(columnIds).toEqual(['a', 'b'])4131 const [text, index] = filterValue.split(',')4132 return rows.filter(row => {4133 return columnIds.some(id => {4134 return row.values[id] === text && row.index === Number(index)4135 })4136 })4137 }4138 }4139 const { container, getByText } = render(<Reactable {...props} />)4140 const searchInput = getSearchInput(container)4141 fireEvent.change(searchInput, { target: { value: 'a' } })4142 expect(getDataRows(container)).toHaveLength(0)4143 fireEvent.change(searchInput, { target: { value: 'aaa2' } })4144 expect(getDataRows(container)).toHaveLength(0)4145 fireEvent.change(searchInput, { target: { value: 'aaa2,1' } })4146 expect(getDataRows(container)).toHaveLength(1)4147 expect(getByText('aaa2')).toBeVisible()4148 fireEvent.change(searchInput, { target: { value: '' } })4149 expect(getDataRows(container)).toHaveLength(3)4150 })4151})4152describe('row selection', () => {4153 beforeEach(() => {4154 window.Shiny = {4155 onInputChange: jest.fn(),4156 addCustomMessageHandler: jest.fn(),4157 bindAll: jest.fn(),4158 unbindAll: jest.fn()4159 }4160 })4161 afterEach(() => {4162 delete window.Shiny4163 })4164 it('selection is disabled by default', () => {4165 const props = {4166 data: { a: [1, 2] },4167 columns: [{ name: 'a', accessor: 'a' }]4168 }4169 const { container } = render(<Reactable {...props} />)4170 const headers = getHeaders(container)4171 expect(headers).toHaveLength(1)4172 expect(getSelectRowCheckboxes(container)).toHaveLength(0)4173 expect(getSelectRowRadios(container)).toHaveLength(0)4174 })4175 it('selection column headers have cell role', () => {4176 const props = {4177 data: { a: [1, 2], b: [3, 4] },4178 columns: [4179 { name: 'a', accessor: 'a' },4180 { name: 'b', accessor: 'b' }4181 ],4182 selection: 'multiple'4183 }4184 const { container } = render(<Reactable {...props} />)4185 const headers = getHeaders(container)4186 expect(headers).toHaveLength(3)4187 expect(headers[0]).toHaveAttribute('role', 'cell')4188 expect(headers[1]).toHaveAttribute('role', 'columnheader')4189 expect(headers[2]).toHaveAttribute('role', 'columnheader')4190 })4191 it('multiple selection', () => {4192 const props = {4193 data: { a: [1, 2] },4194 columns: [{ name: 'a', accessor: 'a' }],4195 selection: 'multiple',4196 selectionId: 'selected'4197 }4198 const { container, rerender } = render(<Reactable {...props} />)4199 expect(getSelectRowCheckboxes(container)).toHaveLength(3)4200 expect(window.Shiny.onInputChange).toHaveBeenCalledWith('selected', [])4201 const selectRowCheckboxes = getSelectRowCheckboxes(container)4202 expect(selectRowCheckboxes).toHaveLength(3)4203 const selectAllCheckbox = selectRowCheckboxes[0]4204 const selectRow1Checkbox = selectRowCheckboxes[1]4205 const selectRow2Checkbox = selectRowCheckboxes[2]4206 expect(selectAllCheckbox).toHaveAttribute('aria-label', 'Select all rows')4207 expect(selectRow1Checkbox).toHaveAttribute('aria-label', 'Select row')4208 expect(selectRow2Checkbox).toHaveAttribute('aria-label', 'Select row')4209 const rows = getRows(container)4210 rows.forEach(row => expect(row).not.toHaveClass('rt-tr-selected'))4211 fireEvent.click(selectRow2Checkbox)4212 expect(selectRow1Checkbox.checked).toEqual(false)4213 expect(selectRow2Checkbox.checked).toEqual(true)4214 expect(selectAllCheckbox.checked).toEqual(false)4215 fireEvent.click(selectRow1Checkbox)4216 expect(selectRow1Checkbox.checked).toEqual(true)4217 expect(selectRow2Checkbox.checked).toEqual(true)4218 expect(selectAllCheckbox.checked).toEqual(true)4219 expect(selectAllCheckbox).toHaveAttribute('aria-label', 'Select all rows')4220 expect(selectRow1Checkbox).toHaveAttribute('aria-label', 'Select row')4221 // Selected row indexes should be sorted numerically, not by selection order4222 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [1, 2])4223 rows.forEach(row => expect(row).toHaveClass('rt-tr-selected'))4224 fireEvent.click(selectAllCheckbox)4225 expect(selectAllCheckbox.checked).toEqual(false)4226 expect(selectRow1Checkbox.checked).toEqual(false)4227 expect(selectRow2Checkbox.checked).toEqual(false)4228 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [])4229 rows.forEach(row => expect(row).not.toHaveClass('rt-tr-selected'))4230 fireEvent.click(selectAllCheckbox)4231 expect(selectAllCheckbox.checked).toEqual(true)4232 expect(selectRow1Checkbox.checked).toEqual(true)4233 expect(selectRow2Checkbox.checked).toEqual(true)4234 fireEvent.click(selectAllCheckbox)4235 // Language4236 rerender(4237 <Reactable4238 {...props}4239 language={{4240 selectAllRowsLabel: '_Select all rows',4241 selectRowLabel: '_Select row'4242 }}4243 />4244 )4245 expect(selectAllCheckbox).toHaveAttribute('aria-label', '_Select all rows')4246 expect(selectRow2Checkbox).toHaveAttribute('aria-label', '_Select row')4247 // Theme4248 rerender(4249 <Reactable4250 {...props}4251 theme={{4252 rowSelectedStyle: {4253 color: 'orange'4254 }4255 }}4256 />4257 )4258 expect(selectRow1Checkbox).toHaveAttribute('aria-label', 'Select row')4259 fireEvent.click(selectRow1Checkbox)4260 expect(selectRow1Checkbox.checked).toEqual(true)4261 expect(rows[0]).toHaveClass('rt-tr-selected')4262 expect(rows[0]).toHaveStyle('color: orange')4263 expect(rows[1]).not.toHaveClass('rt-tr-selected')4264 expect(rows[1]).not.toHaveStyle('color: orange')4265 })4266 it('multiple selection with sub rows', () => {4267 const props = {4268 data: { a: ['x', 'x', 'y', 'y'], b: [1, 1, 2, 41] },4269 columns: [4270 { name: 'a', accessor: 'a' },4271 { name: 'b', accessor: 'b' }4272 ],4273 selection: 'multiple',4274 selectionId: 'selected',4275 pivotBy: ['a'],4276 defaultExpanded: true4277 }4278 const { container, getAllByLabelText, getByLabelText, rerender } = render(4279 <Reactable {...props} />4280 )4281 expect(getSelectRowCheckboxes(container)).toHaveLength(7)4282 expect(window.Shiny.onInputChange).toHaveBeenCalledWith('selected', [])4283 const selectAllSubRowsCheckboxes = getAllByLabelText('Select all rows in group')4284 expect(selectAllSubRowsCheckboxes).toHaveLength(2)4285 const selectRowCheckboxes = getAllByLabelText('Select row')4286 expect(selectRowCheckboxes).toHaveLength(4)4287 const rows = getRows(container)4288 rows.forEach(row => expect(row).not.toHaveClass('rt-tr-selected'))4289 fireEvent.click(selectAllSubRowsCheckboxes[0])4290 expect(selectAllSubRowsCheckboxes[0].checked).toEqual(true)4291 expect(selectRowCheckboxes[0].checked).toEqual(true)4292 expect(selectRowCheckboxes[1].checked).toEqual(true)4293 expect(selectRowCheckboxes[2].checked).toEqual(false)4294 expect(selectRowCheckboxes[3].checked).toEqual(false)4295 expect(selectAllSubRowsCheckboxes[0]).toHaveAttribute('aria-label', 'Select all rows in group')4296 expect(selectRowCheckboxes[0]).toHaveAttribute('aria-label', 'Select row')4297 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [1, 2])4298 rows.forEach((row, i) => {4299 if (i < 3) {4300 expect(row).toHaveClass('rt-tr-selected')4301 } else {4302 expect(row).not.toHaveClass('rt-tr-selected')4303 }4304 })4305 fireEvent.click(selectAllSubRowsCheckboxes[0])4306 expect(selectAllSubRowsCheckboxes[0].checked).toEqual(false)4307 expect(selectRowCheckboxes[0].checked).toEqual(false)4308 expect(selectRowCheckboxes[1].checked).toEqual(false)4309 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [])4310 rows.forEach(row => expect(row).not.toHaveClass('rt-tr-selected'))4311 // Should be able to select all grouped rows4312 const selectAllCheckbox = getByLabelText('Select all rows')4313 fireEvent.click(selectAllCheckbox)4314 selectAllSubRowsCheckboxes.forEach(checkbox => expect(checkbox.checked).toEqual(true))4315 selectRowCheckboxes.forEach(checkbox => expect(checkbox.checked).toEqual(true))4316 fireEvent.click(selectAllCheckbox)4317 // Selection columns should always be first, even before grouped columns4318 const headers = getHeaders(container)4319 expect([...headers].map(header => header.textContent)).toEqual(['\u200b', 'a', 'b'])4320 // Language4321 rerender(4322 <Reactable4323 {...props}4324 language={{4325 selectAllSubRowsLabel: '_Select all rows in group'4326 }}4327 />4328 )4329 expect(selectAllSubRowsCheckboxes[0]).toHaveAttribute('aria-label', '_Select all rows in group')4330 })4331 it('multiple selection of sub rows works when not paginating sub rows', () => {4332 // Known issue with original useRowSelect hook: when paginateExpandedRows = false,4333 // sub rows aren't selected when selecting the parent row.4334 // https://github.com/tannerlinsley/react-table/issues/29084335 const props = {4336 data: { a: ['x', 'x', 'y', 'y'], b: [1, 1, 2, 41] },4337 columns: [4338 { name: 'a', accessor: 'a' },4339 { name: 'b', accessor: 'b' }4340 ],4341 selection: 'multiple',4342 pivotBy: ['a'],4343 defaultExpanded: true4344 }4345 const { getAllByLabelText } = render(<Reactable {...props} />)4346 const selectAllSubRowsCheckboxes = getAllByLabelText('Select all rows in group')4347 expect(selectAllSubRowsCheckboxes).toHaveLength(2)4348 const selectRowCheckboxes = getAllByLabelText('Select row')4349 expect(selectRowCheckboxes).toHaveLength(4)4350 fireEvent.click(selectAllSubRowsCheckboxes[0])4351 expect(selectAllSubRowsCheckboxes[0].checked).toEqual(true)4352 expect(selectRowCheckboxes[0].checked).toEqual(true)4353 expect(selectRowCheckboxes[1].checked).toEqual(true)4354 expect(selectRowCheckboxes[2].checked).toEqual(false)4355 expect(selectRowCheckboxes[3].checked).toEqual(false)4356 })4357 it('multiple selection select all checkbox should not render when table has no rows', () => {4358 const props = {4359 data: { a: [1, 2] },4360 columns: [{ name: 'col-a', accessor: 'a' }],4361 selection: 'multiple',4362 searchable: true4363 }4364 const { container } = render(<Reactable {...props} />)4365 expect(getSelectRowCheckboxes(container)).toHaveLength(3)4366 const searchInput = getSearchInput(container)4367 fireEvent.change(searchInput, { target: { value: 'nonono' } })4368 expect(getDataRows(container)).toHaveLength(0)4369 expect(getSelectRowCheckboxes(container)).toHaveLength(0)4370 })4371 it('single selection', () => {4372 const props = {4373 data: { a: [1, 2] },4374 columns: [{ name: 'a', accessor: 'a' }],4375 selection: 'single',4376 selectionId: 'selected'4377 }4378 const { container, rerender } = render(<Reactable {...props} />)4379 const selectRowRadios = getSelectRowRadios(container)4380 expect(selectRowRadios).toHaveLength(2)4381 expect(window.Shiny.onInputChange).toHaveBeenCalledWith('selected', [])4382 const selectRow1Radio = selectRowRadios[0]4383 const selectRow2Radio = selectRowRadios[1]4384 fireEvent.click(selectRow1Radio)4385 expect(selectRow1Radio.checked).toEqual(true)4386 expect(selectRow2Radio.checked).toEqual(false)4387 expect(selectRow1Radio).toHaveAttribute('aria-label', 'Select row')4388 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [1])4389 fireEvent.click(selectRow2Radio)4390 expect(selectRow1Radio.checked).toEqual(false)4391 expect(selectRow2Radio.checked).toEqual(true)4392 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [2])4393 // De-selection should work4394 fireEvent.click(selectRow2Radio)4395 expect(selectRow1Radio.checked).toEqual(false)4396 expect(selectRow2Radio.checked).toEqual(false)4397 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [])4398 // Language4399 rerender(4400 <Reactable4401 {...props}4402 selection="single"4403 language={{4404 selectRowLabel: '_Select row'4405 }}4406 />4407 )4408 expect(selectRow2Radio).toHaveAttribute('aria-label', '_Select row')4409 })4410 it('defaultSelected', () => {4411 const props = {4412 data: { a: [1, 2, 3] },4413 columns: [{ name: 'a', accessor: 'a' }],4414 selection: 'multiple',4415 selectionId: 'selected',4416 defaultSelected: [1, 0]4417 }4418 const { container } = render(<Reactable {...props} />)4419 const selectRowCheckboxes = getSelectRowCheckboxes(container)4420 expect(selectRowCheckboxes).toHaveLength(4)4421 const selectAllCheckbox = selectRowCheckboxes[0]4422 const selectRow1Checkbox = selectRowCheckboxes[1]4423 const selectRow2Checkbox = selectRowCheckboxes[2]4424 const selectRow3Checkbox = selectRowCheckboxes[3]4425 expect(selectAllCheckbox.checked).toEqual(false)4426 expect(selectRow1Checkbox.checked).toEqual(true)4427 expect(selectRow2Checkbox.checked).toEqual(true)4428 expect(selectRow3Checkbox.checked).toEqual(false)4429 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [1, 2])4430 })4431 it('defaultSelected works on filtered rows', () => {4432 const props = {4433 data: { a: [1, 2, 3] },4434 columns: [{ name: 'a', accessor: 'a' }],4435 selection: 'multiple',4436 searchable: true4437 }4438 const { container, rerender } = render(<Reactable {...props} />)4439 const searchInput = getSearchInput(container)4440 fireEvent.change(searchInput, { target: { value: 'no' } })4441 expect(getDataRows(container)).toHaveLength(0)4442 rerender(<Reactable {...props} defaultSelected={[0, 1]} />)4443 fireEvent.change(searchInput, { target: { value: '' } })4444 expect(getDataRows(container)).toHaveLength(3)4445 const selectRowCheckboxes = getSelectRowCheckboxes(container)4446 const selectRow1Checkbox = selectRowCheckboxes[1]4447 const selectRow2Checkbox = selectRowCheckboxes[2]4448 expect(selectRow1Checkbox.checked).toEqual(true)4449 expect(selectRow2Checkbox.checked).toEqual(true)4450 })4451 it('defaultSelected handles invalid rows', () => {4452 const props = {4453 data: { a: [1, 2, 3] },4454 columns: [{ name: 'a', accessor: 'a' }],4455 selection: 'multiple',4456 defaultSelected: [3]4457 }4458 const { container } = render(<Reactable {...props} />)4459 const selectRowCheckboxes = getSelectRowCheckboxes(container)4460 selectRowCheckboxes.forEach(checkbox => expect(checkbox.checked).toEqual(false))4461 })4462 it('single selection works with filtered rows', () => {4463 const props = {4464 data: { a: ['a-row0', 'b-row1'] },4465 columns: [{ name: 'a', accessor: 'a' }],4466 selection: 'single',4467 searchable: true4468 }4469 const { container } = render(<Reactable {...props} />)4470 let selectRowRadios = getSelectRowRadios(container)4471 expect(selectRowRadios).toHaveLength(2)4472 fireEvent.click(selectRowRadios[0])4473 expect(selectRowRadios[0].checked).toEqual(true)4474 expect(selectRowRadios[1].checked).toEqual(false)4475 // Selected rows that have been filtered out should be deselected when4476 // another row is selected.4477 const searchInput = getSearchInput(container)4478 fireEvent.change(searchInput, { target: { value: 'b-row1' } })4479 selectRowRadios = getSelectRowRadios(container)4480 expect(selectRowRadios).toHaveLength(1)4481 fireEvent.click(selectRowRadios[0])4482 fireEvent.change(searchInput, { target: { value: '' } })4483 selectRowRadios = getSelectRowRadios(container)4484 expect(selectRowRadios).toHaveLength(2)4485 expect(selectRowRadios[0].checked).toEqual(false)4486 expect(selectRowRadios[1].checked).toEqual(true)4487 })4488 it('multiple selection works with filtered rows', () => {4489 const props = {4490 data: { a: ['a-row0-group0', 'b-row1-group0', 'c-row2-group1'] },4491 columns: [{ name: 'a', accessor: 'a' }],4492 selection: 'multiple',4493 searchable: true4494 }4495 const { container } = render(<Reactable {...props} />)4496 // The select all checkbox should only select rows that are currently in4497 // the table, and not filtered out.4498 const searchInput = getSearchInput(container)4499 fireEvent.change(searchInput, { target: { value: 'group0' } })4500 const selectAllCheckbox = getSelectRowCheckboxes(container)[0]4501 fireEvent.click(selectAllCheckbox)4502 expect(selectAllCheckbox.checked).toEqual(true)4503 fireEvent.change(searchInput, { target: { value: '' } })4504 expect(selectAllCheckbox.checked).toEqual(false)4505 const selectRowCheckboxes = getSelectRowCheckboxes(container)4506 expect(selectRowCheckboxes[1].checked).toEqual(true)4507 expect(selectRowCheckboxes[2].checked).toEqual(true)4508 expect(selectRowCheckboxes[3].checked).toEqual(false)4509 })4510 it('table updates when defaultSelected changes', () => {4511 const props = {4512 data: { a: [1, 2, 3] },4513 columns: [{ name: 'a', accessor: 'a' }],4514 selection: 'multiple',4515 selectionId: 'selected',4516 defaultSelected: [1, 0]4517 }4518 const { container, rerender } = render(<Reactable {...props} />)4519 const selectRowCheckboxes = getSelectRowCheckboxes(container)4520 expect(selectRowCheckboxes).toHaveLength(4)4521 const selectAllCheckbox = selectRowCheckboxes[0]4522 const selectRow1Checkbox = selectRowCheckboxes[1]4523 const selectRow2Checkbox = selectRowCheckboxes[2]4524 const selectRow3Checkbox = selectRowCheckboxes[3]4525 expect(selectAllCheckbox.checked).toEqual(false)4526 expect(selectRow1Checkbox.checked).toEqual(true)4527 expect(selectRow2Checkbox.checked).toEqual(true)4528 expect(selectRow3Checkbox.checked).toEqual(false)4529 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [1, 2])4530 fireEvent.click(selectAllCheckbox)4531 rerender(<Reactable {...props} defaultSelected={[0]} />)4532 expect(selectAllCheckbox.checked).toEqual(false)4533 expect(selectRow1Checkbox.checked).toEqual(true)4534 expect(selectRow2Checkbox.checked).toEqual(false)4535 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [1])4536 // Clear selection4537 rerender(<Reactable {...props} defaultSelected={undefined} />)4538 expect(selectAllCheckbox.checked).toEqual(false)4539 expect(selectRow1Checkbox.checked).toEqual(false)4540 expect(selectRow2Checkbox.checked).toEqual(false)4541 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [])4542 // Should allow multiple defaultSelected even when using single selection4543 rerender(<Reactable {...props} selection="single" defaultSelected={[1, 2]} />)4544 expect(selectRow1Checkbox.checked).toEqual(false)4545 expect(selectRow2Checkbox.checked).toEqual(true)4546 expect(selectRow3Checkbox.checked).toEqual(true)4547 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [2, 3])4548 })4549 it('selected state persists when data changes', () => {4550 // This really tests that selected state persists when data changes via groupBy.4551 // Selected state should be reset when the actual data changes through dataKey4552 // or updateReactable.4553 const props = {4554 data: { a: [1, 2, 3] },4555 columns: [{ name: 'a', accessor: 'a' }],4556 selection: 'multiple',4557 selectionId: 'selected'4558 }4559 const { container, rerender } = render(<Reactable {...props} />)4560 const selectRowCheckboxes = getSelectRowCheckboxes(container)4561 expect(selectRowCheckboxes).toHaveLength(4)4562 const selectAllCheckbox = selectRowCheckboxes[0]4563 const selectRow1Checkbox = selectRowCheckboxes[1]4564 const selectRow2Checkbox = selectRowCheckboxes[2]4565 const selectRow3Checkbox = selectRowCheckboxes[3]4566 fireEvent.click(selectRow1Checkbox)4567 fireEvent.click(selectRow3Checkbox)4568 expect(selectAllCheckbox.checked).toEqual(false)4569 expect(selectRow1Checkbox.checked).toEqual(true)4570 expect(selectRow2Checkbox.checked).toEqual(false)4571 expect(selectRow3Checkbox.checked).toEqual(true)4572 expect(window.Shiny.onInputChange).toHaveBeenCalledTimes(3)4573 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [1, 3])4574 rerender(<Reactable {...props} data={{ a: [2, 4, 6] }} />)4575 expect(selectAllCheckbox.checked).toEqual(false)4576 expect(selectRow1Checkbox.checked).toEqual(true)4577 expect(selectRow2Checkbox.checked).toEqual(false)4578 expect(selectRow3Checkbox.checked).toEqual(true)4579 expect(window.Shiny.onInputChange).toHaveBeenCalledTimes(4)4580 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [1, 3])4581 })4582 it('selected rows only update on row selection', () => {4583 const props = {4584 data: { a: ['aaa', 'bbb'] },4585 columns: [{ name: 'col-a', accessor: 'a' }],4586 selection: 'multiple',4587 selectionId: 'selected',4588 searchable: true4589 }4590 const { container, getAllByLabelText, getByLabelText, getByText } = render(4591 <Reactable {...props} />4592 )4593 expect(getSelectRowCheckboxes(container)).toHaveLength(3)4594 const selectAllCheckbox = getByLabelText('Select all rows')4595 const selectRowCheckboxes = getAllByLabelText('Select row')4596 const selectRow1Checkbox = selectRowCheckboxes[0]4597 const selectRow2Checkbox = selectRowCheckboxes[1]4598 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [])4599 fireEvent.click(selectAllCheckbox)4600 expect(selectAllCheckbox.checked).toEqual(true)4601 expect(selectRow1Checkbox.checked).toEqual(true)4602 expect(selectRow2Checkbox.checked).toEqual(true)4603 expect(window.Shiny.onInputChange).toHaveBeenLastCalledWith('selected', [1, 2])4604 expect(window.Shiny.onInputChange).toHaveBeenCalledTimes(2)4605 // Table state changes (e.g., sorting) should not update selected rows4606 fireEvent.click(getByText('col-a'))4607 expect(window.Shiny.onInputChange).toHaveBeenCalledTimes(2)4608 // Selected rows should not change when rows are filtered4609 const searchInput = getSearchInput(container)4610 fireEvent.change(searchInput, { target: { value: 'aaa' } })4611 expect(getDataRows(container)).toHaveLength(1)4612 expect(window.Shiny.onInputChange).toHaveBeenCalledTimes(2)4613 })4614 it('works without Shiny', () => {4615 delete window.Shiny4616 const props = {4617 data: { a: [1, 2] },4618 columns: [{ name: 'a', accessor: 'a' }],4619 selection: 'multiple',4620 selectionId: 'selected'4621 }4622 const { container, getAllByLabelText, getByLabelText } = render(<Reactable {...props} />)4623 expect(getSelectRowCheckboxes(container)).toHaveLength(3)4624 const selectAllCheckbox = getByLabelText('Select all rows')4625 const selectRowCheckboxes = getAllByLabelText('Select row')4626 const selectRow1Checkbox = selectRowCheckboxes[0]4627 const selectRow2Checkbox = selectRowCheckboxes[1]4628 fireEvent.click(selectAllCheckbox)4629 expect(selectAllCheckbox.checked).toEqual(true)4630 expect(selectRow1Checkbox.checked).toEqual(true)4631 expect(selectRow2Checkbox.checked).toEqual(true)4632 })4633 it('multiple selection cells are clickable', () => {4634 const props = {4635 data: { a: [1, 2] },4636 columns: [{ name: 'a', accessor: 'a' }],4637 selection: 'multiple'4638 }4639 const { container } = render(<Reactable {...props} />)4640 const selectRowCheckboxes = getSelectRowCheckboxes(container)4641 expect(selectRowCheckboxes).toHaveLength(3)4642 const selectAllCheckbox = selectRowCheckboxes[0]4643 const selectRow1Checkbox = selectRowCheckboxes[1]4644 const selectRow2Checkbox = selectRowCheckboxes[2]4645 const selectRowCells = getSelectRowCells(container)4646 expect(selectRowCells).toHaveLength(3)4647 const selectAllCell = selectRowCells[0]4648 const selectRow1Cell = selectRowCells[1]4649 const selectRow2Cell = selectRowCells[2]4650 fireEvent.click(selectAllCell)4651 expect(selectAllCheckbox.checked).toEqual(true)4652 expect(selectRow1Checkbox.checked).toEqual(true)4653 expect(selectRow2Checkbox.checked).toEqual(true)4654 fireEvent.click(selectRow1Cell)4655 expect(selectAllCheckbox.checked).toEqual(false)4656 expect(selectRow1Checkbox.checked).toEqual(false)4657 expect(selectRow2Checkbox.checked).toEqual(true)4658 fireEvent.click(selectRow2Cell)4659 expect(selectAllCheckbox.checked).toEqual(false)4660 expect(selectRow1Checkbox.checked).toEqual(false)4661 expect(selectRow2Checkbox.checked).toEqual(false)4662 })4663 it('multiple selection cells are clickable with sub rows', () => {4664 const props = {4665 data: { a: ['x', 'x', 'y', 'y'], b: [1, 1, 2, 41] },4666 columns: [4667 { name: 'a', accessor: 'a' },4668 { name: 'b', accessor: 'b' }4669 ],4670 selection: 'multiple',4671 pivotBy: ['a'],4672 defaultExpanded: true4673 }4674 const { container, getAllByLabelText } = render(<Reactable {...props} />)4675 const selectAllCheckboxes = getAllByLabelText('Select all rows in group')4676 expect(selectAllCheckboxes).toHaveLength(2)4677 const selectRowCheckboxes = getAllByLabelText('Select row')4678 const selectRow1Checkbox = selectRowCheckboxes[0]4679 const selectRow2Checkbox = selectRowCheckboxes[1]4680 const selectRowCells = getSelectRowCells(container)4681 expect(selectRowCells).toHaveLength(7)4682 const selectAllGroup1Cell = selectRowCells[1]4683 fireEvent.click(selectAllGroup1Cell)4684 expect(selectAllCheckboxes[0].checked).toEqual(true)4685 expect(selectRow1Checkbox.checked).toEqual(true)4686 expect(selectRow2Checkbox.checked).toEqual(true)4687 fireEvent.click(selectAllGroup1Cell)4688 expect(selectAllCheckboxes[0].checked).toEqual(false)4689 expect(selectRow1Checkbox.checked).toEqual(false)4690 expect(selectRow2Checkbox.checked).toEqual(false)4691 })4692 it('single selection cells are clickable', () => {4693 const props = {4694 data: { a: [1, 2] },4695 columns: [{ name: 'a', accessor: 'a' }],4696 selection: 'single'4697 }4698 const { container } = render(<Reactable {...props} />)4699 const selectRowRadios = getSelectRowRadios(container)4700 expect(selectRowRadios).toHaveLength(2)4701 const selectRow1Radio = selectRowRadios[0]4702 const selectRow2Radio = selectRowRadios[1]4703 const selectRowCells = getSelectRowCells(container)4704 expect(selectRowCells).toHaveLength(2)4705 const selectRow1Cell = selectRowCells[0]4706 const selectRow2Cell = selectRowCells[1]4707 fireEvent.click(selectRow2Cell)4708 expect(selectRow1Radio.checked).toEqual(false)4709 expect(selectRow2Radio.checked).toEqual(true)4710 fireEvent.click(selectRow1Cell)4711 expect(selectRow1Radio.checked).toEqual(true)4712 expect(selectRow2Radio.checked).toEqual(false)4713 fireEvent.click(selectRow1Cell)4714 expect(selectRow1Radio.checked).toEqual(false)4715 expect(selectRow2Radio.checked).toEqual(false)4716 })4717 it('selects on row click', () => {4718 const props = {4719 data: { a: ['aaa1', 'aaa2'] },4720 columns: [{ name: 'a', accessor: 'a' }],4721 onClick: 'select',4722 selection: 'single'4723 }4724 const { getAllByLabelText, getByText, rerender } = render(<Reactable {...props} />)4725 const selectRowRadios = getAllByLabelText('Select row')4726 expect(selectRowRadios).toHaveLength(2)4727 const selectRow1Radio = selectRowRadios[0]4728 const selectRow2Radio = selectRowRadios[1]4729 fireEvent.click(getByText('aaa1'))4730 expect(selectRow1Radio.checked).toEqual(true)4731 expect(selectRow2Radio.checked).toEqual(false)4732 fireEvent.click(getByText('aaa2'))4733 expect(selectRow1Radio.checked).toEqual(false)4734 expect(selectRow2Radio.checked).toEqual(true)4735 // Should work fine with select inputs4736 fireEvent.click(selectRow2Radio)4737 expect(selectRow1Radio.checked).toEqual(false)4738 expect(selectRow2Radio.checked).toEqual(false)4739 rerender(<Reactable {...props} selection="multiple" />)4740 fireEvent.click(getByText('aaa1'))4741 fireEvent.click(getByText('aaa2'))4742 expect(selectRow1Radio.checked).toEqual(true)4743 expect(selectRow2Radio.checked).toEqual(true)4744 })4745 it('selects all sub rows on row click', () => {4746 const props = {4747 data: { a: ['aaa1', 'aaa2'], b: ['bbb1', 'bbb1'] },4748 columns: [4749 { name: 'a', accessor: 'a', aggregate: () => 'a-aggregated' },4750 { name: 'b', accessor: 'b' }4751 ],4752 pivotBy: ['b'],4753 selection: 'multiple',4754 onClick: 'select'4755 }4756 const { getByLabelText, getAllByLabelText, getByText } = render(<Reactable {...props} />)4757 // Clicking on expandable cell should not toggle selection4758 fireEvent.click(getByText('bbb1 (2)'))4759 const selectAllCheckbox = getByLabelText('Select all rows in group')4760 const selectRowCheckboxes = getAllByLabelText('Select row')4761 const selectRow1Checkbox = selectRowCheckboxes[0]4762 const selectRow2Checkbox = selectRowCheckboxes[1]4763 expect(selectAllCheckbox.checked).toEqual(false)4764 expect(selectRow1Checkbox.checked).toEqual(false)4765 expect(selectRow2Checkbox.checked).toEqual(false)4766 fireEvent.click(getByText('a-aggregated'))4767 expect(selectAllCheckbox.checked).toEqual(true)4768 expect(selectRow1Checkbox.checked).toEqual(true)4769 expect(selectRow2Checkbox.checked).toEqual(true)4770 })4771 it('does not select sub rows on row click for single selection', () => {4772 const props = {4773 data: { a: ['aaa1', 'aaa2'], b: ['bbb1', 'bbb1'] },4774 columns: [4775 { name: 'a', accessor: 'a', aggregate: () => 'a-aggregated' },4776 { name: 'b', accessor: 'b' }4777 ],4778 pivotBy: ['b'],4779 selection: 'single',4780 onClick: 'select',4781 defaultExpanded: true4782 }4783 const { getAllByLabelText, getByText } = render(<Reactable {...props} />)4784 const selectRowCheckboxes = getAllByLabelText('Select row')4785 const selectRow1Checkbox = selectRowCheckboxes[0]4786 const selectRow2Checkbox = selectRowCheckboxes[1]4787 expect(selectRow1Checkbox.checked).toEqual(false)4788 expect(selectRow2Checkbox.checked).toEqual(false)4789 fireEvent.click(getByText('a-aggregated'))4790 expect(selectRow1Checkbox.checked).toEqual(false)4791 expect(selectRow2Checkbox.checked).toEqual(false)4792 })4793 it('ignores pad rows on row click', () => {4794 const props = {4795 data: { a: ['aaa1', 'aaa2'] },4796 columns: [{ name: 'a', accessor: 'a' }],4797 selection: 'single',4798 onClick: 'select',4799 minRows: 54800 }4801 const { container, getAllByLabelText } = render(<Reactable {...props} />)4802 const padRows = getPadRows(container)4803 fireEvent.click(getCells(padRows[0])[0])4804 const selectRowRadios = getAllByLabelText('Select row')4805 expect(selectRowRadios).toHaveLength(2)4806 const selectRow1Radio = selectRowRadios[0]4807 const selectRow2Radio = selectRowRadios[1]4808 expect(selectRow1Radio.checked).toEqual(false)4809 expect(selectRow2Radio.checked).toEqual(false)4810 })4811 it('selection cells can still be clicked with other click actions', () => {4812 const props = {4813 data: { a: ['aaa1', 'aaa2'] },4814 columns: [{ name: 'a', accessor: 'a' }],4815 onClick: () => {4816 throw new Error('should not be called')4817 },4818 selection: 'multiple'4819 }4820 const { container } = render(<Reactable {...props} />)4821 const selectRowCells = getSelectRowCells(container)4822 expect(selectRowCells).toHaveLength(3)4823 const selectRow1Cell = selectRowCells[1]4824 const selectRow2Cell = selectRowCells[2]4825 fireEvent.click(selectRow1Cell)4826 fireEvent.click(selectRow2Cell)4827 const selectRowCheckboxes = getSelectRowCheckboxes(container)4828 expect(selectRowCheckboxes).toHaveLength(3)4829 selectRowCheckboxes.forEach(checkbox => expect(checkbox.checked).toEqual(true))4830 })4831 it('selected state should be available in cellInfo, rowInfo, and state', () => {4832 const props = {4833 data: { a: [1, 2, 3], b: ['a', 'b', 'c'] },4834 columns: [4835 {4836 name: 'a',4837 accessor: 'a',4838 cell: (cellInfo, state) => {4839 return `${cellInfo.value} selected? ${4840 cellInfo.selected ? 'yes' : 'no'4841 }. selected: ${JSON.stringify(state.selected)}`4842 },4843 details: (rowInfo, state) => {4844 return `row ${rowInfo.index} selected? ${4845 rowInfo.selected ? 'yes' : 'no'4846 }. selected: ${JSON.stringify(state.selected)}`4847 },4848 className: 'col-a'4849 },4850 { name: 'b', accessor: 'b' }4851 ],4852 selection: 'multiple',4853 rowClassName: (rowInfo, state) => {4854 if (rowInfo.selected && state.selected.includes(rowInfo.index)) {4855 return 'i-am-selected'4856 }4857 },4858 rowStyle: (rowInfo, state) => {4859 if (rowInfo.selected && state.selected.includes(rowInfo.index)) {4860 return { backgroundColor: 'red' }4861 }4862 },4863 defaultExpanded: true4864 }4865 const { container, getAllByLabelText, getByText } = render(<Reactable {...props} />)4866 expect(getCellsText(container, '.col-a')).toEqual([4867 '\u200b1 selected? no. selected: []',4868 '\u200b2 selected? no. selected: []',4869 '\u200b3 selected? no. selected: []'4870 ])4871 const selectRow1Checkbox = getAllByLabelText('Select row')[0]4872 fireEvent.click(selectRow1Checkbox)4873 expect(getCellsText(container, '.col-a')).toEqual([4874 '\u200b1 selected? yes. selected: [0]',4875 '\u200b2 selected? no. selected: [0]',4876 '\u200b3 selected? no. selected: [0]'4877 ])4878 const rows = getRows(container)4879 expect(rows[0]).toHaveClass('i-am-selected')4880 expect(rows[1]).not.toHaveClass('i-am-selected')4881 expect(rows[0]).toHaveStyle('background-color: red')4882 expect(rows[1]).not.toHaveStyle('background-color: red')4883 expect(getByText('row 0 selected? yes. selected: [0]')).toBeVisible()4884 expect(getByText('row 1 selected? no. selected: [0]')).toBeVisible()4885 })4886 it('selection column can be customized', () => {4887 const props = {4888 data: { a: [1, 2] },4889 columns: [4890 { name: 'a', accessor: 'a' },4891 {4892 name: '',4893 accessor: '.selection',4894 selectable: true,4895 className: 'cell-cls',4896 headerClassName: 'header-cls',4897 style: { color: 'blue' },4898 headerStyle: { color: 'orange' },4899 width: 2224900 }4901 ],4902 selection: 'multiple'4903 }4904 const { container } = render(<Reactable {...props} />)4905 const selectRowCells = getSelectRowCells(container)4906 expect(selectRowCells).toHaveLength(3)4907 selectRowCells.forEach(cell => expect(cell).toHaveStyle('width: 222px'))4908 const selectAllCell = selectRowCells[0]4909 const selectRow1Cell = selectRowCells[1]4910 const selectRow2Cell = selectRowCells[2]4911 expect(selectAllCell).toHaveClass('header-cls')4912 expect(selectRow1Cell).toHaveClass('cell-cls')4913 expect(selectRow2Cell).toHaveClass('cell-cls')4914 expect(selectAllCell).toHaveStyle('color: orange')4915 expect(selectRow1Cell).toHaveStyle('color: blue')4916 expect(selectRow2Cell).toHaveStyle('color: blue')4917 // Selection columns should always be first4918 const headers = getHeaders(container)4919 expect([...headers].map(header => header.textContent)).toEqual(['\u200b', 'a'])4920 })4921 it('row selection columns should not be sortable, filterable, or searchable', () => {4922 const props = {4923 data: { a: [1, 2], '.selection': ['aaa', 'bbb'] },4924 columns: [4925 { name: 'col-a', accessor: 'a' },4926 {4927 name: '',4928 accessor: '.selection',4929 selectable: true,4930 filterable: true,4931 searchable: true,4932 sortable: true4933 }4934 ],4935 selection: 'multiple',4936 searchable: true4937 }4938 const { container } = render(<Reactable {...props} />)4939 const sortableHeaders = getSortableHeaders(container)4940 expect(sortableHeaders).toHaveLength(1)4941 const filters = getFilters(container)4942 expect(filters).toHaveLength(0)4943 const searchInput = getSearchInput(container)4944 fireEvent.change(searchInput, { target: { value: 'aaa' } })4945 expect(getDataRows(container)).toHaveLength(0)4946 })4947 it('row selection column works with column groups', () => {4948 const props = {4949 data: { a: [1, 2], b: [3, 4], c: ['a', 'b'] },4950 columns: [4951 { name: 'col-a', accessor: 'a' },4952 { name: 'col-b', accessor: 'b' },4953 { name: 'col-c', accessor: 'c' },4954 {4955 name: '',4956 accessor: '.selection',4957 selectable: true4958 }4959 ],4960 columnGroups: [4961 { columns: ['b'], name: 'group-b' },4962 { columns: ['a', '.selection'], name: 'group-sel' }4963 ],4964 selection: 'multiple'4965 }4966 const { container } = render(<Reactable {...props} />)4967 const columnHeaders = getColumnHeaders(container)4968 expect([...columnHeaders].map(header => header.textContent)).toEqual([4969 '\u200b', // Selection column should still be first4970 'col-a',4971 'col-b',4972 'col-c'4973 ])4974 const groupHeaders = getGroupHeaders(container)4975 expect(groupHeaders).toHaveLength(2)4976 expect(groupHeaders[0]).toHaveAttribute('aria-colspan', '2')4977 expect(groupHeaders[1]).toHaveAttribute('aria-colspan', '1')4978 expect([...groupHeaders].map(header => header.textContent)).toEqual(['group-sel', 'group-b'])4979 expect(getUngroupedHeaders(container)).toHaveLength(1)4980 expect(getSelectRowCells(container)).toHaveLength(3)4981 })4982 it('row selection column works with split column groups', () => {4983 const props = {4984 data: { a: [1, 2], b: [3, 4], c: ['a', 'b'] },4985 columns: [4986 { name: 'col-a', accessor: 'a' },4987 { name: 'col-b', accessor: 'b' },4988 { name: 'col-c', accessor: 'c' },4989 {4990 name: '',4991 accessor: '.selection',4992 selectable: true4993 }4994 ],4995 columnGroups: [4996 { columns: ['a'], name: 'group-a' },4997 { columns: ['b', '.selection'], name: 'group-sel' }4998 ],4999 selection: 'multiple'5000 }5001 const { container } = render(<Reactable {...props} />)5002 const columnHeaders = getColumnHeaders(container)5003 expect([...columnHeaders].map(header => header.textContent)).toEqual([5004 '\u200b', // Selection column should still be first5005 'col-a',5006 'col-b',5007 'col-c'5008 ])5009 const groupHeaders = getGroupHeaders(container)5010 expect(groupHeaders).toHaveLength(3)5011 expect(groupHeaders[0]).toHaveAttribute('aria-colspan', '1')5012 expect(groupHeaders[1]).toHaveAttribute('aria-colspan', '1')5013 expect(groupHeaders[2]).toHaveAttribute('aria-colspan', '1')5014 expect([...groupHeaders].map(header => header.textContent)).toEqual([5015 'group-sel',5016 'group-a',5017 'group-sel'5018 ])5019 expect(getUngroupedHeaders(container)).toHaveLength(1)5020 expect(getSelectRowCells(container)).toHaveLength(3)5021 })5022})5023describe('expandable row details', () => {5024 it('renders row details', () => {5025 const props = {5026 data: { a: [1, 2], b: ['a', 'b'] },5027 columns: [5028 { name: '', accessor: '.details', details: rowInfo => `row details: ${rowInfo.index}` },5029 { name: 'a', accessor: 'a' },5030 { name: 'b', accessor: 'b' }5031 ]5032 }5033 const { container } = render(<Reactable {...props} />)5034 let rowGroups = getRowGroups(container)5035 rowGroups.forEach(rowGroup => expect(rowGroup.children).toHaveLength(1))5036 let rowDetails = getRowDetails(container)5037 expect(rowDetails).toHaveLength(0)5038 const expanders = getExpanders(container)5039 expect(expanders).toHaveLength(2)5040 expanders.forEach(expander => {5041 expect(expander).toHaveAttribute('aria-label', 'Toggle details')5042 expect(expander).toHaveAttribute('aria-expanded', 'false')5043 })5044 const expanderIcons = getExpanderIcons(container)5045 expanderIcons.forEach(icon => expect(icon).not.toHaveClass('rt-expander-open'))5046 const expandableCells = getExpandableCells(container)5047 expect(expandableCells).toHaveLength(2)5048 fireEvent.click(expanders[0])5049 expect(expanderIcons[0]).toHaveClass('rt-expander-open')5050 rowDetails = getRowDetails(container)5051 expect(rowDetails).toHaveLength(1)5052 expect(rowDetails[0].textContent).toEqual('row details: 0')5053 // Row details should be in row groups5054 rowGroups = getRowGroups(container)5055 expect(rowGroups[0].children[1]).toEqual(rowDetails[0])5056 fireEvent.click(expanders[1])5057 rowDetails = getRowDetails(container)5058 expect(rowDetails).toHaveLength(2)5059 expect(rowDetails[1].textContent).toEqual('row details: 1')5060 rowGroups = getRowGroups(container)5061 expect(rowGroups[1].children[1]).toEqual(rowDetails[1])5062 expanders.forEach(expander => {5063 expect(expander).toHaveAttribute('aria-label', 'Toggle details')5064 expect(expander).toHaveAttribute('aria-expanded', 'true')5065 })5066 expanderIcons.forEach(icon => expect(icon).toHaveClass('rt-expander-open'))5067 // Row details should collapse5068 fireEvent.click(expanders[0])5069 expect(expanders[0]).toHaveAttribute('aria-label', 'Toggle details')5070 expect(expanders[0]).toHaveAttribute('aria-expanded', 'false')5071 expect(expanderIcons[0]).not.toHaveClass('rt-expander-open')5072 expect(getRowDetails(container)).toHaveLength(1)5073 // Expandable cells should be clickable5074 fireEvent.click(expandableCells[0])5075 expect(getRowDetails(container)).toHaveLength(2)5076 fireEvent.click(expandableCells[0])5077 expect(getRowDetails(container)).toHaveLength(1)5078 })5079 it('row details render function (JS)', () => {5080 const assertProps = (rowInfo, state) => {5081 expect(rowInfo.index >= 0).toEqual(true)5082 expect(rowInfo.viewIndex >= 0).toEqual(true)5083 expect(rowInfo.level).toEqual(0)5084 expect(rowInfo.aggregated).toBeFalsy()5085 expect(rowInfo.expanded).toEqual(true)5086 expect(rowInfo.selected).toEqual(false)5087 expect(rowInfo.subRows).toEqual([])5088 expect(rowInfo.values).toEqual(5089 [5090 { a: 1, b: 'a' },5091 { a: 2, b: 'b' }5092 ][rowInfo.index]5093 )5094 expect(rowInfo.row).toEqual(5095 [5096 { a: 1, b: 'a' },5097 { a: 2, b: 'b' }5098 ][rowInfo.index]5099 )5100 expect(state.page).toEqual(0)5101 expect(state.pageSize).toEqual(10)5102 expect(state.pages).toEqual(1)5103 expect(state.sorted).toEqual([])5104 expect(state.groupBy).toEqual([])5105 expect(state.filters).toEqual([])5106 expect(state.searchValue).toEqual(undefined)5107 expect(state.selected).toEqual([])5108 expect(state.pageRows).toEqual([5109 { a: 1, b: 'a' },5110 { a: 2, b: 'b' }5111 ])5112 expect(state.sortedData).toEqual([5113 { a: 1, b: 'a' },5114 { a: 2, b: 'b' }5115 ])5116 expect(state.data).toEqual([5117 { a: 1, b: 'a' },5118 { a: 2, b: 'b' }5119 ])5120 }5121 const props = {5122 data: { a: [1, 2], b: ['a', 'b'] },5123 columns: [5124 { name: 'a', accessor: 'a' },5125 {5126 name: 'b',5127 accessor: 'b',5128 details: (rowInfo, state) => {5129 assertProps(rowInfo, state)5130 return `row details: ${rowInfo.values.a}`5131 }5132 }5133 ]5134 }5135 const { container } = render(<Reactable {...props} />)5136 const expanders = getExpanders(container)5137 fireEvent.click(expanders[0])5138 fireEvent.click(expanders[1])5139 const rowDetails = getRowDetails(container)5140 expect(rowDetails[0].textContent).toEqual('row details: 1')5141 expect(rowDetails[1].textContent).toEqual('row details: 2')5142 })5143 it('row details render function (JS) as html', () => {5144 const props = {5145 data: { a: [1, 2], b: ['a', 'b'] },5146 columns: [5147 { name: 'a', accessor: 'a' },5148 {5149 name: 'b',5150 accessor: 'b',5151 html: true,5152 details: rowInfo => `<span>row details: ${rowInfo.values.a}</span>`5153 }5154 ]5155 }5156 const { container } = render(<Reactable {...props} />)5157 const expanders = getExpanders(container)5158 fireEvent.click(expanders[0])5159 fireEvent.click(expanders[1])5160 const rowDetails = getRowDetails(container)5161 expect(rowDetails).toHaveLength(2)5162 expect(rowDetails[0].innerHTML).toEqual('<span>row details: 1</span>')5163 expect(rowDetails[1].innerHTML).toEqual('<span>row details: 2</span>')5164 })5165 it('row details render function (R)', () => {5166 const props = {5167 data: { a: [1, 2], b: ['a', 'b'] },5168 columns: [5169 { name: 'a', accessor: 'a' },5170 {5171 name: 'b',5172 accessor: 'b',5173 html: true,5174 details: ['<span>row details: 1</span>', '<span>row details: 2</span>']5175 }5176 ]5177 }5178 const { container } = render(<Reactable {...props} />)5179 const expanders = getExpanders(container)5180 fireEvent.click(expanders[0])5181 fireEvent.click(expanders[1])5182 const rowDetails = getRowDetails(container)5183 expect(rowDetails).toHaveLength(2)5184 expect(rowDetails[0].innerHTML).toEqual('<span>row details: 1</span>')5185 expect(rowDetails[1].innerHTML).toEqual('<span>row details: 2</span>')5186 })5187 it('renders conditional row details', () => {5188 const props = {5189 data: { a: [1, 2], b: ['a', 'b'] },5190 columns: [5191 { name: 'a', accessor: 'a' },5192 { name: 'b', accessor: 'b', details: ['row details: 1', null] }5193 ]5194 }5195 const { container, getByText, queryByText } = render(<Reactable {...props} />)5196 const expanders = getExpanders(container)5197 expect(expanders).toHaveLength(1)5198 expect(queryByText('row details: 1')).toEqual(null)5199 fireEvent.click(expanders[0])5200 expect(getByText('row details: 1')).toBeVisible()5201 })5202 it('renders empty row details', () => {5203 const props = {5204 data: { a: [1, 2], b: ['a', 'b'] },5205 columns: [5206 { name: 'a', accessor: 'a' },5207 { name: 'b', accessor: 'b', details: ['', ''] }5208 ]5209 }5210 const { container } = render(<Reactable {...props} />)5211 const expanders = getExpanders(container)5212 expect(expanders).toHaveLength(2)5213 fireEvent.click(expanders[0])5214 fireEvent.click(expanders[1])5215 })5216 it('renders multiple row details', () => {5217 const props = {5218 data: { a: [1, 2], b: ['a', 'b'] },5219 columns: [5220 { name: 'a', accessor: 'a', details: ['detail-a1', 'detail-a2'] },5221 { name: 'b', accessor: 'b', details: ['detail-b1', 'detail-b2'] }5222 ]5223 }5224 const { container, getByText, queryByText } = render(<Reactable {...props} />)5225 const expanders = getExpanders(container)5226 expect(expanders).toHaveLength(4)5227 fireEvent.click(expanders[0])5228 expect(getByText('detail-a1')).toBeVisible()5229 expect(queryByText('detail-b1')).toEqual(null)5230 // Row 1, col b5231 fireEvent.click(expanders[1])5232 expect(getByText('detail-b1')).toBeVisible()5233 expect(queryByText('detail-a1')).toEqual(null)5234 // Row 2, col a5235 fireEvent.click(expanders[2])5236 expect(getByText('detail-a2')).toBeVisible()5237 expect(queryByText('detail-b2')).toEqual(null)5238 // Row 2, col b5239 fireEvent.click(expanders[3])5240 expect(getByText('detail-b2')).toBeVisible()5241 expect(queryByText('detail-a2')).toEqual(null)5242 })5243 it('expander-only columns have expected classes and styles', () => {5244 const props = {5245 data: { a: [1, 2], b: ['a', 'b'], c: ['c', 'd'] },5246 columns: [5247 {5248 name: '',5249 accessor: '.details',5250 className: 'expander-no-content',5251 style: { color: 'blue' },5252 details: ['detail-1', null]5253 },5254 {5255 name: '',5256 accessor: '.details2',5257 className: 'expander-with-content',5258 cell: () => 'content',5259 details: ['detail-2', null]5260 },5261 { name: 'c', accessor: 'c' }5262 ]5263 }5264 const { container, getByText } = render(<Reactable {...props} />)5265 expect(getExpanders(container)).toHaveLength(2)5266 let expanderCells = container.querySelectorAll('.expander-no-content')5267 expect(expanderCells).toHaveLength(2)5268 fireEvent.click(expanderCells[0])5269 expect(getByText('detail-1')).toBeVisible()5270 // Expander-only cells without content should have special styles5271 expect(expanderCells[0]).toHaveStyle('text-overflow: clip; user-select: none; color: blue')5272 expect(expanderCells[1]).not.toHaveStyle('text-overflow: clip; user-select: none')5273 // Expander-only cells with content should not have special styles5274 expanderCells = container.querySelectorAll('.expander-with-content')5275 expect(expanderCells).toHaveLength(2)5276 expect(expanderCells[0]).not.toHaveStyle('text-overflow: clip; user-select: none')5277 })5278 it('does not render row details for pad rows', () => {5279 const props = {5280 data: { a: [1, 2], b: ['a', 'b'] },5281 columns: [5282 { name: 'a', accessor: 'a' },5283 {5284 name: 'b',5285 accessor: 'b',5286 details: rowInfo => `row details: ${rowInfo.values.a}`5287 }5288 ],5289 minRows: 65290 }5291 const { container } = render(<Reactable {...props} />)5292 const expanders = getExpanders(container)5293 expect(expanders).toHaveLength(2)5294 fireEvent.click(expanders[0])5295 fireEvent.click(expanders[1])5296 const rowDetails = getRowDetails(container)5297 expect(rowDetails).toHaveLength(2)5298 expect(rowDetails[0].textContent).toEqual('row details: 1')5299 expect(rowDetails[1].textContent).toEqual('row details: 2')5300 })5301 it('row expanded state persists across sorting, filtering, and pagination changes', () => {5302 const props = {5303 data: { a: ['cell-1', 'cell-2'], b: ['b', 'a'] },5304 columns: [5305 {5306 name: 'col-a',5307 accessor: 'a',5308 filterable: true,5309 details: ['row-details-1', 'row-details-2']5310 },5311 { name: 'col-b', accessor: 'b' }5312 ],5313 defaultPageSize: 15314 }5315 const { container, getByText, queryByText } = render(<Reactable {...props} />)5316 let expanders = getExpanders(container)5317 expect(expanders).toHaveLength(1)5318 // Pagination5319 fireEvent.click(expanders[0])5320 expect(getByText('row-details-1')).toBeVisible()5321 fireEvent.click(getNextButton(container))5322 expect(queryByText('row-details-1')).toEqual(null)5323 expect(queryByText('row-details-2')).toEqual(null)5324 fireEvent.click(getPrevButton(container))5325 expect(getByText('row-details-1')).toBeVisible()5326 fireEvent.click(expanders[0])5327 expect(queryByText('row-details-1')).toEqual(null)5328 // Sorting5329 fireEvent.click(expanders[0])5330 expect(getByText('row-details-1')).toBeVisible()5331 fireEvent.click(getByText('col-b'))5332 expect(queryByText('row-details-1')).toEqual(null)5333 expect(queryByText('row-details-2')).toEqual(null)5334 fireEvent.click(getByText('col-b'))5335 expect(getByText('row-details-1')).toBeVisible()5336 fireEvent.click(expanders[0])5337 expect(queryByText('row-details-1')).toEqual(null)5338 // Filtering5339 fireEvent.click(expanders[0])5340 expect(getByText('row-details-1')).toBeVisible()5341 const filter = getFilters(container)[0]5342 fireEvent.change(filter, { target: { value: 'cell-1' } })5343 expect(getByText('row-details-1')).toBeVisible()5344 fireEvent.change(filter, { target: { value: 'cell-2' } })5345 expect(queryByText('row-details-1')).toEqual(null)5346 expect(queryByText('row-details-2')).toEqual(null)5347 fireEvent.change(filter, { target: { value: '' } })5348 expect(getByText('row-details-1')).toBeVisible()5349 fireEvent.click(expanders[0])5350 expect(queryByText('row-details-1')).toEqual(null)5351 })5352 it('row expanded state persists when data changes', () => {5353 // This really tests that expanded state persists when data changes via groupBy.5354 // Expanded state should be reset when the actual data changes through dataKey5355 // or updateReactable.5356 const props = {5357 data: { a: [1, 2, 3], b: ['x', 'x', 'z'] },5358 columns: [5359 {5360 name: 'a',5361 accessor: 'a',5362 details: rowInfo => `row details: ${rowInfo.index}-${rowInfo.values.a}-a`5363 },5364 { name: 'b', accessor: 'b' }5365 ]5366 }5367 const { container, getByText, rerender } = render(<Reactable {...props} />)5368 const expanders = getExpanders(container)5369 expect(expanders).toHaveLength(3)5370 fireEvent.click(expanders[0])5371 fireEvent.click(expanders[1])5372 expect(getRowDetails(container)).toHaveLength(2)5373 expect(getByText('row details: 0-1-a')).toBeVisible()5374 rerender(<Reactable {...props} data={{ a: [22, 44, 66], b: ['x', 'y', 'z'] }} />)5375 expect(getRowDetails(container)).toHaveLength(2)5376 expect(getByText('row details: 0-22-a')).toBeVisible()5377 })5378 it('handles Shiny elements in row details content', () => {5379 window.Shiny = { bindAll: jest.fn(), unbindAll: jest.fn(), addCustomMessageHandler: jest.fn() }5380 const props = {5381 data: { a: [1, 2, 3], b: ['a', 'b', 'c'] },5382 columns: [5383 { name: 'a', accessor: 'a', details: ['details-a-1', null, 'details-a-3'] },5384 { name: 'b', accessor: 'b', details: ['details-b-1', null, 'details-b-3'] }5385 ],5386 defaultPageSize: 25387 }5388 const { container, getByText } = render(<Reactable {...props} />)5389 const expanders = getExpanders(container)5390 expect(expanders).toHaveLength(2)5391 fireEvent.click(expanders[0])5392 expect(window.Shiny.bindAll).toHaveBeenCalledTimes(1)5393 expect(window.Shiny.unbindAll).toHaveBeenCalledTimes(0)5394 fireEvent.click(expanders[0])5395 expect(window.Shiny.bindAll).toHaveBeenCalledTimes(1)5396 expect(window.Shiny.unbindAll).toHaveBeenCalledTimes(1)5397 // Content should update properly when expanding another column while one5398 // column is already expanded.5399 fireEvent.click(expanders[0])5400 expect(window.Shiny.bindAll).toHaveBeenCalledTimes(2)5401 expect(window.Shiny.unbindAll).toHaveBeenCalledTimes(1)5402 fireEvent.click(expanders[1])5403 expect(window.Shiny.bindAll).toHaveBeenCalledTimes(3)5404 expect(window.Shiny.unbindAll).toHaveBeenCalledTimes(2)5405 fireEvent.click(expanders[0])5406 expect(window.Shiny.bindAll).toHaveBeenCalledTimes(4)5407 expect(window.Shiny.unbindAll).toHaveBeenCalledTimes(3)5408 // Row details content should be cleaned up properly when changing page5409 expect(getByText('details-a-1')).toBeVisible()5410 fireEvent.click(getNextButton(container))5411 expect(window.Shiny.unbindAll).toHaveBeenCalledTimes(4)5412 // Content should update properly when changing page5413 fireEvent.click(getExpanders(container)[0])5414 expect(getByText('details-a-3')).toBeVisible()5415 expect(window.Shiny.bindAll).toHaveBeenCalledTimes(5)5416 expect(window.Shiny.unbindAll).toHaveBeenCalledTimes(4)5417 fireEvent.click(getPrevButton(container))5418 expect(getByText('details-a-1')).toBeVisible()5419 expect(window.Shiny.bindAll).toHaveBeenCalledTimes(6)5420 expect(window.Shiny.unbindAll).toHaveBeenCalledTimes(5)5421 delete window.Shiny5422 })5423 it('row details work with column groups', () => {5424 const props = {5425 data: { a: [1, 2], b: ['a', 'b'] },5426 columns: [5427 { name: 'col-a', accessor: 'a', details: ['row-details-1', 'row-details-2'] },5428 { name: 'col-b', accessor: 'b' }5429 ],5430 columnGroups: [{ columns: ['a', 'b'] }]5431 }5432 const { container, getByText } = render(<Reactable {...props} />)5433 let expanders = getExpanders(container)5434 expect(expanders).toHaveLength(2)5435 fireEvent.click(expanders[0])5436 expect(getByText('row-details-1')).toBeVisible()5437 fireEvent.click(expanders[1])5438 expect(getByText('row-details-2')).toBeVisible()5439 })5440 it('row details do not use manual expanded key', () => {5441 // react-table uses an 'expanded' column to control expanded state by default5442 const props = {5443 data: { a: [1, 2], b: ['a', 'b'], expanded: [true, true] },5444 columns: [5445 { name: 'a', accessor: 'a' },5446 { name: 'b', accessor: 'b', details: () => 'details' },5447 { name: 'expanded', accessor: 'expanded' }5448 ]5449 }5450 const { container } = render(<Reactable {...props} />)5451 const rowDetails = getRowDetails(container)5452 expect(rowDetails).toHaveLength(0)5453 })5454 it('row details work with grouping', () => {5455 const props = {5456 data: { a: [1, 2], b: ['a', 'b'], c: ['x', 'y'] },5457 columns: [5458 { name: 'col-a', accessor: 'a', details: ['r-row-details', 'r-row-details'] },5459 { name: 'col-b', accessor: 'b', details: () => 'js-row-details' },5460 { name: 'col-c', accessor: 'c' }5461 ],5462 pivotBy: ['c']5463 }5464 const { container, getByText, queryByText } = render(<Reactable {...props} />)5465 let expanders = getExpanders(container)5466 expect(expanders).toHaveLength(2)5467 expect(getRows(container)).toHaveLength(2)5468 // Expand grouped cell5469 fireEvent.click(expanders[0])5470 expect(getRows(container)).toHaveLength(3)5471 // Row details should not be shown for grouped rows (not supported currently)5472 expect(queryByText('r-row-details')).toEqual(null)5473 expect(queryByText('js-row-details')).toEqual(null)5474 // Expand details5475 expanders = getExpanders(container)5476 expect(expanders).toHaveLength(4)5477 fireEvent.click(expanders[1]) // Expand col-a5478 expect(getByText('r-row-details')).toBeVisible()5479 fireEvent.click(expanders[2]) // Expand col-b5480 expect(getByText('js-row-details')).toBeVisible()5481 // Aggregated cells in columns with JS row details should not be clickable5482 const cells = getCells(container)5483 const aggregatedCell = cells[2]5484 expect(aggregatedCell.textContent).toEqual('')5485 fireEvent.click(aggregatedCell)5486 expect(getByText('js-row-details')).toBeVisible()5487 // Placeholder cells under grouped cells should not be clickable5488 const groupedChildCell = cells[3]5489 expect(groupedChildCell.textContent).toEqual('')5490 fireEvent.click(groupedChildCell)5491 expect(getByText('js-row-details')).toBeVisible()5492 })5493 it('expanders have aria labels', () => {5494 const props = {5495 data: { a: [1, 2], b: ['a', 'b'] },5496 columns: [5497 { name: 'col-a', accessor: 'a', details: rowInfo => `row details: ${rowInfo.values.a}` },5498 { name: 'col-b', accessor: 'b' }5499 ]5500 }5501 const { container } = render(<Reactable {...props} />)5502 const expanders = getExpanders(container)5503 expect(expanders).toHaveLength(2)5504 expect(expanders[0]).toHaveAttribute('aria-label', 'Toggle details')5505 expect(expanders[0]).toHaveAttribute('aria-expanded', 'false')5506 fireEvent.click(expanders[0])5507 expect(expanders[0]).toHaveAttribute('aria-expanded', 'true')5508 })5509 it('defaultExpanded works with row details', () => {5510 const props = {5511 data: { a: [1, 2, 3], b: ['cell-b-0', 'cell-b-1', 'cell-b-2'], c: [3, 4, 5] },5512 columns: [5513 { name: 'a', accessor: 'a', details: [null, 'details: 1-a', 'details: 2-a'] },5514 { name: 'b', accessor: 'b', details: rowInfo => `details: ${rowInfo.index}-b` },5515 { name: 'c', accessor: 'c', details: rowInfo => `details: ${rowInfo.index}-c` }5516 ],5517 defaultExpanded: true5518 }5519 const { container, getByText, queryByText, rerender } = render(<Reactable {...props} />)5520 expect(getByText('details: 1-a')).toBeVisible()5521 expect(getByText('details: 2-a')).toBeVisible()5522 // defaultExpanded should work with conditional row details5523 expect(getRowDetails(container)).toHaveLength(2)5524 // Only the first column should be expanded when there are multiple row details5525 expect(queryByText('details: 0-b')).toEqual(null)5526 expect(queryByText('details: 1-c')).toEqual(null)5527 // Should update when prop changes5528 rerender(<Reactable {...props} defaultExpanded={undefined} />)5529 expect(queryByText('details:')).toEqual(null)5530 expect(getRowDetails(container)).toHaveLength(0)5531 rerender(<Reactable {...props} defaultExpanded={true} />)5532 expect(getByText('details: 1-a')).toBeVisible()5533 expect(getByText('details: 2-a')).toBeVisible()5534 rerender(<Reactable {...props} defaultExpanded={false} />)5535 expect(getRowDetails(container)).toHaveLength(0)5536 })5537 it('defaultExpanded works with column groups', () => {5538 const props = {5539 data: { a: [1, 2], b: ['a', 'b'] },5540 columns: [5541 { name: 'col-a', accessor: 'a', details: ['row-details-1', 'row-details-2'] },5542 { name: 'col-b', accessor: 'b' }5543 ],5544 columnGroups: [{ columns: ['a', 'b'] }],5545 defaultExpanded: true5546 }5547 const { getByText } = render(<Reactable {...props} />)5548 expect(getByText('row-details-1')).toBeVisible()5549 expect(getByText('row-details-2')).toBeVisible()5550 })5551 it('defaultExpanded expands the first details column when there are multiple', () => {5552 const props = {5553 data: { a: [1, 2], b: ['a', 'b'] },5554 columns: [5555 { name: 'col-a', accessor: 'a', details: ['details-a-1', 'details-a-2'] },5556 { name: 'col-b', accessor: 'b', details: ['details-b-1', 'details-b-2'] }5557 ],5558 columnGroups: [{ columns: ['a', 'b'] }],5559 defaultExpanded: true5560 }5561 const { container, getByText, queryByText } = render(<Reactable {...props} />)5562 expect(getByText('details-a-1')).toBeVisible()5563 expect(getByText('details-a-2')).toBeVisible()5564 expect(queryByText('details-b-1')).toBeFalsy()5565 expect(queryByText('details-b-2')).toBeFalsy()5566 expect(getRowDetails(container)).toHaveLength(2)5567 })5568 it('defaultExpanded does not error when there are no expandable rows', () => {5569 const props = {5570 data: { a: [1, 2], b: ['a', 'b'] },5571 columns: [5572 { name: 'col-a', accessor: 'a' },5573 { name: 'col-b', accessor: 'b' }5574 ],5575 defaultExpanded: true5576 }5577 const { container } = render(<Reactable {...props} />)5578 expect(getRowDetails(container)).toHaveLength(0)5579 })5580 it('styles do not bleed through to nested tables', () => {5581 const props = {5582 data: { a: [1, 2] },5583 columns: [5584 {5585 name: 'a',5586 accessor: 'a',5587 details: [5588 null,5589 <Reactable5590 key="nested"5591 data={{ a: [1, 2, 3] }}5592 columns={[{ name: 'a', accessor: 'a' }]}5593 rowClassName="nested-row"5594 className="nested"5595 />5596 ]5597 }5598 ],5599 striped: true,5600 highlight: true,5601 defaultExpanded: true5602 }5603 const { container } = render(<Reactable {...props} />)5604 const rows = container.querySelectorAll('.nested-row')5605 expect(rows).toHaveLength(3)5606 rows.forEach(row => expect(row).not.toHaveClass('rt-tr-striped'))5607 rows.forEach(row => expect(row).not.toHaveClass('rt-tr-highlight'))5608 const headerRows = getHeaderRows(container.querySelector('.nested'))5609 expect(headerRows).toHaveLength(1)5610 expect(headerRows[0]).not.toHaveClass('rt-tr-striped')5611 expect(headerRows[0]).not.toHaveClass('rt-tr-highlight')5612 })5613 it('expanders language', () => {5614 const props = {5615 data: { a: [1, 2], b: ['a', 'b'] },5616 columns: [5617 { name: 'a', accessor: 'a', details: rowInfo => `row details: ${rowInfo.values.a}` }5618 ],5619 language: {5620 detailsExpandLabel: '_Toggle details'5621 }5622 }5623 const { container } = render(<Reactable {...props} />)5624 const expanders = getExpanders(container)5625 expect(expanders[0]).toHaveAttribute('aria-label', '_Toggle details')5626 fireEvent.click(expanders[0])5627 expect(expanders[0]).toHaveAttribute('aria-label', '_Toggle details')5628 })5629})5630describe('grouping and aggregation', () => {5631 it('renders grouped rows', () => {5632 const props = {5633 data: { a: [1, 2, 1], b: ['a', 'b', 'c'], c: ['x', 'y', 'z'] },5634 columns: [5635 { name: 'col-a', accessor: 'a' },5636 { name: 'col-b', accessor: 'b' },5637 { name: 'col-c', accessor: 'c' }5638 ],5639 pivotBy: ['a']5640 }5641 const { container } = render(<Reactable {...props} />)5642 const expanders = getExpanders(container)5643 expect(expanders).toHaveLength(2)5644 expect(getRows(container)).toHaveLength(2)5645 expect(expanders[0]).toHaveAttribute('aria-label', 'Toggle group')5646 expect(expanders[1]).toHaveAttribute('aria-label', 'Toggle group')5647 expect(expanders[0]).toHaveAttribute('aria-expanded', 'false')5648 expect(expanders[1]).toHaveAttribute('aria-expanded', 'false')5649 const expandableCells = getExpandableCells(container)5650 expect(expandableCells).toHaveLength(2)5651 expect(expandableCells[0].textContent).toEqual('\u200b1 (2)')5652 expect(expandableCells[1].textContent).toEqual('\u200b2 (1)')5653 // Expand grouped cell5654 fireEvent.click(expanders[0])5655 expect(getRows(container)).toHaveLength(4)5656 fireEvent.click(getExpanders(container)[1])5657 expect(getRows(container)).toHaveLength(5)5658 expect(expanders[0]).toHaveAttribute('aria-label', 'Toggle group')5659 expect(expanders[0]).toHaveAttribute('aria-expanded', 'true')5660 expect(getExpanders(container)[1]).toHaveAttribute('aria-label', 'Toggle group')5661 expect(getExpanders(container)[1]).toHaveAttribute('aria-expanded', 'true')5662 // Expandable cells should be clickable5663 fireEvent.click(expandableCells[0])5664 expect(getRows(container)).toHaveLength(3)5665 fireEvent.click(expandableCells[0])5666 expect(getRows(container)).toHaveLength(5)5667 expect(getCellsText(container)).toEqual([5668 '\u200b1 (2)',5669 '',5670 '',5671 '',5672 'a',5673 'x',5674 '',5675 'c',5676 'z',5677 '\u200b2 (1)',5678 '',5679 '',5680 '',5681 'b',5682 'y'5683 ])5684 })5685 it('renders grouped rows with multiple groupBy', () => {5686 const props = {5687 data: { a: [1, 2, 3], b: ['a', 'b', 'c'], c: ['x', 'x', 'z'] },5688 columns: [5689 { name: 'col-a', accessor: 'a' },5690 { name: 'col-b', accessor: 'b' },5691 { name: 'col-c', accessor: 'c' }5692 ],5693 pivotBy: ['c', 'a']5694 }5695 const { container, getByText } = render(<Reactable {...props} />)5696 expect(getRows(container)).toHaveLength(2)5697 expect(getExpanders(container)).toHaveLength(2)5698 expect(getExpandableCells(container)).toHaveLength(4)5699 const headers = getHeaders(container)5700 expect([...headers].map(header => header.textContent)).toEqual(['col-c', 'col-a', 'col-b'])5701 fireEvent.click(getByText('x (2)'))5702 fireEvent.click(getByText('z (1)'))5703 expect(getRows(container)).toHaveLength(5)5704 expect(getExpanders(container)).toHaveLength(5)5705 expect(getExpandableCells(container)).toHaveLength(10)5706 fireEvent.click(getExpanders(container)[1]) // 1 (1)5707 fireEvent.click(getExpanders(container)[2]) // 2 (1)5708 fireEvent.click(getExpanders(container)[4]) // 3 (1)5709 expect(getRows(container)).toHaveLength(8)5710 expect(getCellsText(container)).toEqual([5711 '\u200bx (2)', // 15712 '',5713 '',5714 '', // 25715 '\u200b1 (1)',5716 '',5717 '', // 35718 '',5719 'a',5720 '', // 45721 '\u200b2 (1)',5722 '',5723 '', // 55724 '',5725 'b',5726 '\u200bz (1)', // 65727 '',5728 '',5729 '', // 75730 '\u200b3 (1)',5731 '',5732 '', // 85733 '',5734 'c'5735 ])5736 })5737 it('grouped cells use JS cell render functions', () => {5738 const props = {5739 data: { a: [1, 2, 1], b: ['a', 'b', 'c'], c: ['x', 'y', 'z'] },5740 columns: [5741 {5742 name: 'col-a',5743 accessor: 'a',5744 className: 'col-grouped',5745 cell: (cellInfo, state) => {5746 return (5747 `${cellInfo.value}: aggregated=${cellInfo.aggregated}, ` +5748 `isGrouped=${cellInfo.isGrouped}, row=${cellInfo.index}, ` +5749 `page=${state.page}`5750 )5751 }5752 },5753 { name: 'col-b', accessor: 'b' },5754 { name: 'col-c', accessor: 'c' }5755 ],5756 pivotBy: ['a']5757 }5758 const { container } = render(<Reactable {...props} />)5759 expect(getRows(container)).toHaveLength(2)5760 expect(getCellsText(container, '.col-grouped')).toEqual([5761 // cellInfo.aggregated should be the same as row.aggregated,5762 // NOT cell.isAggregated, which is false for grouped columns.5763 '\u200b1: aggregated=true, isGrouped=true, row=0, page=0 (2)',5764 '\u200b2: aggregated=true, isGrouped=true, row=1, page=0 (1)'5765 ])5766 })5767 it('grouped cells do not use R cell render functions', () => {5768 // Grouped cells could support R cell render functions, but they currently5769 // do not because the row index of a grouped cell does not correspond to5770 // any specific row in the data.5771 const props = {5772 data: { a: [1, 2, 1], b: ['a', 'b', 'c'] },5773 columns: [5774 {5775 name: 'col-a',5776 accessor: 'a',5777 className: 'col-grouped',5778 cell: ['not-shown', 'not-shown', 'not-shown']5779 },5780 { name: 'col-b', accessor: 'b' }5781 ],5782 pivotBy: ['a']5783 }5784 const { container } = render(<Reactable {...props} />)5785 expect(getRows(container)).toHaveLength(2)5786 expect(getCellsText(container, '.col-grouped')).toEqual(['\u200b1 (2)', '\u200b2 (1)'])5787 })5788 it('grouped cell render function', () => {5789 const props = {5790 data: { a: ['a', '', null], b: [1, 2, 3] },5791 columns: [5792 {5793 name: 'col-a',5794 accessor: 'a',5795 na: 'missing',5796 cell: () => 'overridden',5797 grouped: (cellInfo, state) => {5798 const rows = [5799 { a: 'a', b: null, _subRows: [{ a: 'a', b: 1 }] },5800 { a: '', b: null, _subRows: [{ a: '', b: 2 }] },5801 { a: null, b: null, _subRows: [{ a: null, b: 3 }] }5802 ]5803 expect(cellInfo.column.id).toEqual('a')5804 expect(cellInfo.column.name).toEqual('col-a')5805 expect(cellInfo.index >= 0).toEqual(true)5806 expect(cellInfo.viewIndex >= 0).toEqual(true)5807 expect(cellInfo.page).toEqual(0)5808 expect(cellInfo.value).toEqual(['a', '', 'missing'][cellInfo.index])5809 expect(cellInfo.aggregated).toEqual(true)5810 expect(cellInfo.filterValue).toEqual(undefined)5811 expect(cellInfo.level).toEqual(0)5812 expect(cellInfo.expanded).toBeFalsy()5813 expect(cellInfo.selected).toEqual(false)5814 expect(cellInfo.row).toEqual(5815 [5816 { a: 'a', b: null },5817 { a: '', b: null },5818 { a: null, b: null }5819 ][cellInfo.index]5820 )5821 expect(cellInfo.subRows).toEqual(rows[cellInfo.index]._subRows)5822 expect(state.page).toEqual(0)5823 expect(state.pageSize).toEqual(10)5824 expect(state.pages).toEqual(1)5825 expect(state.sorted).toEqual([])5826 expect(state.groupBy).toEqual(['a'])5827 expect(state.filters).toEqual([])5828 expect(state.searchValue).toEqual(undefined)5829 expect(state.selected).toEqual([])5830 expect(state.pageRows).toEqual(rows)5831 expect(state.sortedData).toEqual(rows)5832 expect(state.data).toEqual([5833 { a: 'a', b: 1 },5834 { a: '', b: 2 },5835 { a: null, b: 3 }5836 ])5837 return cellInfo.value5838 },5839 className: 'col-a'5840 },5841 { name: 'col-b', accessor: 'b' }5842 ],5843 pivotBy: ['a']5844 }5845 const { container } = render(<Reactable {...props} />)5846 expect(getCellsText(container, '.col-a')).toEqual(['\u200ba', '\u200b\u200b', '\u200bmissing'])5847 })5848 it('aggregates values', () => {5849 let aggregateCount = 05850 const props = {5851 data: { a: [1, 2, 1], b: ['a', 'b', 'c'], c: ['x', 'x', 'z'] },5852 columns: [5853 { name: 'col-a', accessor: 'a', type: 'numeric', aggregate: 'sum' },5854 {5855 name: 'col-b',5856 accessor: 'b',5857 aggregate: (values, rows, aggregatedRows) => {5858 if (aggregateCount === 0) {5859 expect(values).toEqual(['a', 'b'])5860 expect(rows).toEqual([5861 { a: 1, b: 'a', c: 'x' },5862 { a: 2, b: 'b', c: 'x' }5863 ])5864 expect(aggregatedRows).toEqual([5865 { a: 1, b: 'a', c: 'x' },5866 { a: 2, b: 'b', c: 'x' }5867 ])5868 } else {5869 expect(values).toEqual(['c'])5870 expect(rows).toEqual([{ a: 1, b: 'c', c: 'z' }])5871 expect(aggregatedRows).toEqual([{ a: 1, b: 'c', c: 'z' }])5872 }5873 aggregateCount++5874 return values.join(', ')5875 }5876 },5877 { name: 'col-c', accessor: 'c' }5878 ],5879 pivotBy: ['c']5880 }5881 const { container } = render(<Reactable {...props} />)5882 expect(aggregateCount).toEqual(2)5883 expect(getCellsText(container)).toEqual(['\u200bx (2)', '3', 'a, b', '\u200bz (1)', '1', 'c'])5884 })5885 it('aggregates values with multiple groupBy columns', () => {5886 let aggregateCount = 05887 const props = {5888 data: { a: ['a', 'a', 'b'], b: [1, 2, 3], c: ['x', 'x', 'x'] },5889 columns: [5890 {5891 name: 'col-a',5892 accessor: 'a',5893 // Aggregate functions should work for columns in groupBy, as long as5894 // they aren't the first groupBy column.5895 aggregate: values => {5896 return values.join(', ')5897 }5898 },5899 {5900 name: 'col-b',5901 accessor: 'b',5902 aggregate: (values, rows, aggregatedRows) => {5903 if (aggregateCount === 0) {5904 // Sub-group a (2)5905 expect(values).toEqual([1, 2])5906 expect(rows).toEqual([5907 { a: 'a', b: 1, c: 'x' },5908 { a: 'a', b: 2, c: 'x' }5909 ])5910 expect(aggregatedRows).toEqual([5911 { a: 'a', b: 1, c: 'x' },5912 { a: 'a', b: 2, c: 'x' }5913 ])5914 } else if (aggregateCount === 1) {5915 // Sub-group b (1)5916 expect(values).toEqual([3])5917 expect(rows).toEqual([{ a: 'b', b: 3, c: 'x' }])5918 expect(aggregatedRows).toEqual([{ a: 'b', b: 3, c: 'x' }])5919 } else {5920 // Group x (2): should have all leaf rows5921 expect(values).toEqual([1, 2, 3])5922 expect(rows).toEqual([5923 { a: 'a', b: 1, c: 'x' },5924 { a: 'a', b: 2, c: 'x' },5925 { a: 'b', b: 3, c: 'x' }5926 ])5927 expect(aggregatedRows).toEqual([5928 { a: 'a', b: 3, c: 'x' },5929 { a: 'b', b: 3, c: 'x' }5930 ])5931 }5932 aggregateCount++5933 return values.reduce((a, b) => a + b, 0)5934 }5935 },5936 {5937 name: 'col-c',5938 accessor: 'c',5939 // Aggregate function should not be called for non-aggregated groupBy columns5940 // (the first groupBy column).5941 aggregate: () => {5942 throw new Error('should not be called')5943 }5944 }5945 ],5946 pivotBy: ['c', 'a']5947 }5948 const { container } = render(<Reactable {...props} />)5949 expect(aggregateCount).toEqual(3)5950 fireEvent.click(getExpanders(container)[0])5951 expect(getCellsText(container)).toEqual([5952 '\u200bx (2)',5953 'a, a, b',5954 '6',5955 '',5956 '\u200ba (2)',5957 '3',5958 '',5959 '\u200bb (1)',5960 '3'5961 ])5962 })5963 it('aggregated values update when filtering', () => {5964 const props = {5965 data: { a: ['a', 'a', 'b'], b: [1, 2, 3], c: ['x', 'x', 'x'] },5966 columns: [5967 { name: 'col-a', accessor: 'a' },5968 {5969 name: 'col-b',5970 accessor: 'b',5971 aggregate: (values, rows, aggregatedRows) => {5972 return `${values} ${rows.map(row => row.b)} ${aggregatedRows.map(row => row.b)}`5973 }5974 },5975 { name: 'col-c', accessor: 'c' }5976 ],5977 pivotBy: ['c'],5978 searchable: true5979 }5980 const { container } = render(<Reactable {...props} />)5981 fireEvent.change(getSearchInput(container), { target: { value: 'a' } })5982 expect(getCellsText(container)).toEqual(['\u200bx (2)', '', '1,2 1,2 1,2'])5983 })5984 it('renders aggregated cells', () => {5985 const props = {5986 data: {5987 groupA: ['a', 'a'],5988 groupB: [3, 4],5989 a: [1, 2],5990 b: ['x', 'x'],5991 c: [1, 2],5992 d: ['g', 'h'],5993 e: [1, 2],5994 f: [1, 2]5995 },5996 columns: [5997 { name: 'col-groupA', accessor: 'groupA' },5998 {5999 // Aggregated cell renderers should work for aggregated cells in groupBy6000 // columns, as long as they aren't the first groupBy column.6001 name: 'col-groupB',6002 accessor: 'groupB',6003 type: 'numeric',6004 aggregate: 'sum'6005 },6006 { name: 'col-a', accessor: 'a', aggregate: 'unique' },6007 { name: 'col-b', accessor: 'b', aggregated: () => 123 },6008 { name: 'col-c', accessor: 'c', aggregated: () => true },6009 {6010 // HTML rendering6011 name: 'col-d',6012 accessor: 'd',6013 aggregated: () => '<div>col-d</div>',6014 html: true6015 },6016 {6017 // React elements and HTML rendering should not clash6018 name: 'col-e',6019 accessor: 'e',6020 aggregated: function Aggregated() {6021 return <div>col-e</div>6022 },6023 html: true6024 },6025 {6026 // Formatters should not apply to empty aggregate values6027 name: 'col-f',6028 accessor: 'f',6029 format: { aggregated: { prefix: '!!', date: true } }6030 }6031 ],6032 pivotBy: ['groupA', 'groupB']6033 }6034 const { container } = render(<Reactable {...props} />)6035 expect(getCellsText(container)).toEqual([6036 '\u200ba (2)',6037 '7',6038 '1, 2',6039 '123',6040 'true',6041 'col-d',6042 'col-e',6043 ''6044 ])6045 })6046 it('aggregated cell render function', () => {6047 let isExpanded = false6048 let aggregatedCount = 06049 const props = {6050 data: { a: ['a', 'a', 'b'], b: [1, 2, 3], c: ['x', 'x', 'x'], d: [1, 2, 3] },6051 columns: [6052 {6053 name: 'col-a',6054 accessor: 'a',6055 // Aggregated cell renderers should work for aggregated cells in groupBy6056 // columns, as long as they aren't the first groupBy column.6057 aggregated: cellInfo => {6058 expect(cellInfo.value).toEqual(null)6059 return 'agg-a'6060 }6061 },6062 {6063 name: 'col-b',6064 accessor: 'b',6065 type: 'numeric',6066 aggregate: 'mean',6067 aggregated: (cellInfo, state) => {6068 const rows = [6069 {6070 c: 'x',6071 a: null,6072 b: 2,6073 d: null,6074 _subRows: [6075 {6076 a: 'a',6077 b: 1.5,6078 c: 'x',6079 d: null,6080 _subRows: [6081 { a: 'a', b: 1, c: 'x', d: 1 },6082 { a: 'a', b: 2, c: 'x', d: 2 }6083 ]6084 },6085 { a: 'b', b: 3, c: 'x', d: null, _subRows: [{ a: 'b', b: 3, c: 'x', d: 3 }] }6086 ]6087 }6088 ]6089 // First row, unexpanded6090 if (!isExpanded) {6091 expect(cellInfo.column.id).toEqual('b')6092 expect(cellInfo.column.name).toEqual('col-b')6093 expect(cellInfo.index).toEqual(0)6094 expect(cellInfo.viewIndex).toEqual(0)6095 expect(cellInfo.page).toEqual(0)6096 expect(cellInfo.value).toEqual(2)6097 expect(cellInfo.aggregated).toEqual(true)6098 expect(cellInfo.filterValue).toEqual(undefined)6099 expect(cellInfo.level).toEqual(0)6100 expect(cellInfo.expanded).toBeFalsy()6101 expect(cellInfo.selected).toEqual(false)6102 expect(cellInfo.row).toEqual({ c: 'x', a: null, b: 2, d: null })6103 expect(cellInfo.subRows).toEqual(rows[0]._subRows)6104 expect(state.page).toEqual(0)6105 expect(state.pageSize).toEqual(10)6106 expect(state.pages).toEqual(1)6107 expect(state.sorted).toEqual([])6108 expect(state.groupBy).toEqual(['c', 'a'])6109 expect(state.filters).toEqual([])6110 expect(state.searchValue).toEqual(undefined)6111 expect(state.selected).toEqual([])6112 expect(state.pageRows).toEqual(rows)6113 expect(state.sortedData).toEqual(rows)6114 expect(state.data).toEqual([6115 { a: 'a', b: 1, c: 'x', d: 1 },6116 { a: 'a', b: 2, c: 'x', d: 2 },6117 { a: 'b', b: 3, c: 'x', d: 3 }6118 ])6119 }6120 // First row, expanded6121 if (isExpanded && cellInfo.level === 0) {6122 expect(cellInfo.expanded).toEqual(true)6123 }6124 // Two child rows when expanded6125 if (cellInfo.level > 0) {6126 expect(cellInfo.index === 0 || cellInfo.index === 1).toEqual(true)6127 expect(cellInfo.value).toEqual([1.5, 3][cellInfo.index])6128 expect(cellInfo.aggregated).toEqual(true)6129 expect(cellInfo.level).toEqual(1)6130 expect(cellInfo.expanded).toBeFalsy()6131 expect(cellInfo.selected).toEqual(false)6132 expect(cellInfo.row).toEqual(6133 [6134 { a: 'a', b: 1.5, c: 'x', d: null },6135 { a: 'b', b: 3, c: 'x', d: null }6136 ][cellInfo.index]6137 )6138 expect(cellInfo.subRows.length).toEqual([2, 1][cellInfo.index])6139 expect(state.page).toEqual(0)6140 expect(state.pageSize).toEqual(10)6141 expect(state.pages).toEqual(1)6142 expect(state.sorted).toEqual([])6143 expect(state.groupBy).toEqual(['c', 'a'])6144 expect(state.filters).toEqual([])6145 expect(state.searchValue).toEqual(undefined)6146 expect(state.selected).toEqual([])6147 expect(state.pageRows).toEqual([rows[0], rows[0]._subRows[0], rows[0]._subRows[1]])6148 expect(state.sortedData).toEqual([rows[0], rows[0]._subRows[0], rows[0]._subRows[1]])6149 expect(state.data).toEqual([6150 { a: 'a', b: 1, c: 'x', d: 1 },6151 { a: 'a', b: 2, c: 'x', d: 2 },6152 { a: 'b', b: 3, c: 'x', d: 3 }6153 ])6154 aggregatedCount++6155 }6156 return `mean: ${cellInfo.value}`6157 }6158 },6159 {6160 name: 'col-c',6161 accessor: 'c',6162 // Aggregated cell renderer should not be called for non-aggregated groupBy columns6163 // (the first groupBy column).6164 aggregated: () => {6165 throw new Error('should not be called')6166 }6167 },6168 {6169 name: 'col-d',6170 accessor: 'd',6171 aggregated: cellInfo => {6172 expect(cellInfo.value).toEqual(null)6173 return 'agg-d'6174 },6175 html: true6176 }6177 ],6178 pivotBy: ['c', 'a'],6179 paginateSubRows: true6180 }6181 const { container } = render(<Reactable {...props} />)6182 isExpanded = true6183 fireEvent.click(getExpanders(container)[0])6184 expect(aggregatedCount).toEqual(2)6185 expect(getCellsText(container)).toEqual([6186 '\u200bx (2)',6187 'agg-a',6188 'mean: 2',6189 'agg-d',6190 '',6191 '\u200ba (2)',6192 'mean: 1.5',6193 'agg-d',6194 '',6195 '\u200bb (1)',6196 'mean: 3',6197 'agg-d'6198 ])6199 })6200 it('leaf rows should have a nesting depth > 0', () => {6201 const props = {6202 data: { a: ['a', 'a', 'b'], b: [1, 2, 3], c: ['x', 'x', 'x'], d: [1, 2, 3] },6203 columns: [6204 { name: 'col-a', accessor: 'a' },6205 {6206 name: 'col-b',6207 accessor: 'b',6208 cell: cellInfo => cellInfo.depth,6209 aggregated: cellInfo => cellInfo.depth,6210 className: 'col-b'6211 },6212 { name: 'col-c', accessor: 'c' },6213 { name: 'col-d', accessor: 'd' }6214 ],6215 pivotBy: ['c', 'a'],6216 defaultExpanded: true6217 }6218 const { container } = render(<Reactable {...props} />)6219 expect(getCellsText(container, '.col-b')).toEqual(['0', '1', '2', '2', '1', '2'])6220 })6221 it('aggregated cell formatting', () => {6222 const props = {6223 data: { a: ['a', 'a', 'b'], b: [1, 2, 3], c: ['x', 'x', 'x'], d: [1, 2, 3] },6224 columns: [6225 { name: 'col-a', accessor: 'a', format: { cell: { suffix: '__cell_a' } } },6226 {6227 name: 'col-b',6228 accessor: 'b',6229 format: { cell: { suffix: '__cell' }, aggregated: { prefix: 'agg__' } },6230 aggregate: () => '',6231 // Formatting should be applied before aggregated cell renderers6232 aggregated: cellInfo => `${cellInfo.value}b-${cellInfo.level}-${cellInfo.index}`,6233 className: 'col-b'6234 },6235 { name: 'col-c', accessor: 'c' },6236 { name: 'col-d', accessor: 'd' }6237 ],6238 pivotBy: ['c', 'a'],6239 defaultExpanded: true6240 }6241 const { container, getByText } = render(<Reactable {...props} />)6242 expect(getByText('a__cell_a (2)')).toBeVisible()6243 expect(getByText('b__cell_a (1)')).toBeVisible()6244 expect(getCellsText(container, '.col-b')).toEqual([6245 'agg__b-0-0',6246 'agg__b-1-0',6247 '1__cell',6248 '2__cell',6249 'agg__b-1-1',6250 '3__cell'6251 ])6252 })6253 it('applies classes and styles to aggregated cells', () => {6254 let isExpanded = false6255 const assertProps = (rowInfo, colInfo, state) => {6256 // Check props for initial state only (one row)6257 if (isExpanded) {6258 return6259 }6260 expect(colInfo.id).toEqual('b')6261 expect(colInfo.name).toEqual('col-b')6262 expect(rowInfo.index).toEqual(0)6263 expect(rowInfo.viewIndex).toEqual(0)6264 expect(rowInfo.aggregated).toEqual(true)6265 expect(rowInfo.level).toEqual(0)6266 expect(rowInfo.expanded).toBeFalsy()6267 expect(rowInfo.selected).toEqual(false)6268 expect(rowInfo.values).toEqual({ c: 'x', a: null, b: 2, d: null })6269 expect(rowInfo.row).toEqual({ c: 'x', a: null, b: 2, d: null })6270 expect(rowInfo.subRows).toEqual([6271 {6272 a: 'a',6273 b: 1.5,6274 c: 'x',6275 d: null,6276 _subRows: [6277 { a: 'a', b: 1, c: 'x', d: 1 },6278 { a: 'a', b: 2, c: 'x', d: 2 }6279 ]6280 },6281 { a: 'b', b: 3, c: 'x', d: null, _subRows: [{ a: 'b', b: 3, c: 'x', d: 3 }] }6282 ])6283 expect(state.page).toEqual(0)6284 expect(state.pageSize).toEqual(10)6285 expect(state.pages).toEqual(1)6286 expect(state.sorted).toEqual([])6287 expect(state.groupBy).toEqual(['c', 'a'])6288 expect(state.filters).toEqual([])6289 expect(state.searchValue).toEqual(undefined)6290 expect(state.selected).toEqual([])6291 expect(state.pageRows).toEqual([6292 {6293 c: 'x',6294 a: null,6295 b: 2,6296 d: null,6297 _subRows: [6298 {6299 a: 'a',6300 b: 1.5,6301 c: 'x',6302 d: null,6303 _subRows: [6304 { b: 1, d: 1, a: 'a', c: 'x' },6305 { b: 2, d: 2, a: 'a', c: 'x' }6306 ]6307 },6308 { a: 'b', b: 3, c: 'x', d: null, _subRows: [{ b: 3, d: 3, a: 'b', c: 'x' }] }6309 ]6310 }6311 ])6312 expect(state.sortedData).toEqual(state.pageRows)6313 }6314 const props = {6315 data: { a: ['a', 'a', 'b'], b: [1, 2, 3], c: ['x', 'x', 'x'], d: [1, 2, 3] },6316 columns: [6317 {6318 name: 'col-a',6319 accessor: 'a',6320 className: rowInfo => {6321 return rowInfo.aggregated ? 'grouped-a' : 'ungrouped-a'6322 },6323 style: { color: '#aaa' }6324 },6325 {6326 name: 'col-b',6327 accessor: 'b',6328 type: 'numeric',6329 aggregate: 'mean',6330 className: (rowInfo, colInfo, state) => {6331 assertProps(rowInfo, colInfo, state)6332 return rowInfo.aggregated ? 'grouped-b' : 'ungrouped-b'6333 },6334 style: (rowInfo, colInfo, state) => {6335 assertProps(rowInfo, colInfo, state)6336 return { color: '#bbb' }6337 }6338 },6339 { name: 'col-c', accessor: 'c' },6340 { name: 'col-d', accessor: 'd' }6341 ],6342 pivotBy: ['c', 'a']6343 }6344 const { container, getByText } = render(<Reactable {...props} />)6345 // Expand group x (2)6346 isExpanded = true6347 fireEvent.click(getByText('x (2)'))6348 // Grouped cells in groupBy columns should be styled6349 const groupedCellsA = getCells(container, '.grouped-a')6350 expect(groupedCellsA).toHaveLength(3)6351 groupedCellsA.forEach(cell => expect(cell).toHaveStyle('color: #aaa'))6352 // Grouped cells in regular columns should be styled6353 const groupedCellsB = getCells(container, '.grouped-b')6354 expect(groupedCellsB).toHaveLength(3)6355 groupedCellsB.forEach(cell => expect(cell).toHaveStyle('color: #bbb'))6356 // Expand the second group6357 fireEvent.click(getByText('a (2)'))6358 // Ungrouped cells should be styled6359 const ungroupedCellsA = getCells(container, '.ungrouped-a')6360 expect(ungroupedCellsA).toHaveLength(2)6361 ungroupedCellsA.forEach(cell => expect(cell).toHaveStyle('color: #aaa'))6362 })6363 it('applies row classes and styles to aggregated rows', () => {6364 let isExpanded = false6365 const assertProps = (rowInfo, state) => {6366 // Check props for initial state only (one row)6367 if (isExpanded) {6368 return6369 }6370 expect(rowInfo.index).toEqual(0)6371 expect(rowInfo.viewIndex).toEqual(0)6372 expect(rowInfo.aggregated).toEqual(true)6373 expect(rowInfo.level).toEqual(0)6374 expect(rowInfo.expanded).toBeFalsy()6375 expect(rowInfo.selected).toEqual(false)6376 expect(rowInfo.values).toEqual({ c: 'x', a: null, b: 2, d: null })6377 expect(rowInfo.row).toEqual({ c: 'x', a: null, b: 2, d: null })6378 expect(rowInfo.subRows).toEqual([6379 {6380 a: 'a',6381 b: 1.5,6382 c: 'x',6383 d: null,6384 _subRows: [6385 { a: 'a', b: 1, c: 'x', d: 1 },6386 { a: 'a', b: 2, c: 'x', d: 2 }6387 ]6388 },6389 { a: 'b', b: 3, c: 'x', d: null, _subRows: [{ a: 'b', b: 3, c: 'x', d: 3 }] }6390 ])6391 expect(state.page).toEqual(0)6392 expect(state.pageSize).toEqual(10)6393 expect(state.pages).toEqual(1)6394 expect(state.sorted).toEqual([])6395 expect(state.groupBy).toEqual(['c', 'a'])6396 expect(state.filters).toEqual([])6397 expect(state.searchValue).toEqual(undefined)6398 expect(state.selected).toEqual([])6399 expect(state.pageRows).toEqual([6400 {6401 c: 'x',6402 a: null,6403 b: 2,6404 d: null,6405 _subRows: [6406 {6407 a: 'a',6408 b: 1.5,6409 c: 'x',6410 d: null,6411 _subRows: [6412 { b: 1, d: 1, a: 'a', c: 'x' },6413 { b: 2, d: 2, a: 'a', c: 'x' }6414 ]6415 },6416 { a: 'b', b: 3, c: 'x', d: null, _subRows: [{ b: 3, d: 3, a: 'b', c: 'x' }] }6417 ]6418 }6419 ])6420 expect(state.sortedData).toEqual(state.pageRows)6421 expect(state.data).toEqual([6422 { a: 'a', b: 1, c: 'x', d: 1 },6423 { a: 'a', b: 2, c: 'x', d: 2 },6424 { a: 'b', b: 3, c: 'x', d: 3 }6425 ])6426 }6427 const props = {6428 data: { a: ['a', 'a', 'b'], b: [1, 2, 3], c: ['x', 'x', 'x'], d: [1, 2, 3] },6429 columns: [6430 { name: 'col-a', accessor: 'a' },6431 { name: 'col-b', accessor: 'b', type: 'numeric', aggregate: 'mean' },6432 { name: 'col-c', accessor: 'c' },6433 { name: 'col-d', accessor: 'd' }6434 ],6435 pivotBy: ['c', 'a'],6436 rowClassName: (rowInfo, colInfo, state) => {6437 assertProps(rowInfo, colInfo, state)6438 return rowInfo.aggregated ? 'grouped' : 'ungrouped'6439 },6440 rowStyle: (rowInfo, colInfo, state) => {6441 assertProps(rowInfo, colInfo, state)6442 return { color: '#bbb' }6443 }6444 }6445 const { container, getByText } = render(<Reactable {...props} />)6446 // Expand first group6447 isExpanded = true6448 fireEvent.click(getByText('x (2)'))6449 // Grouped cells in groupBy columns should be styled6450 const groupedRows = getRows(container, '.grouped')6451 expect(groupedRows).toHaveLength(3)6452 groupedRows.forEach(row => expect(row).toHaveStyle('color: #bbb'))6453 // Expand second group6454 fireEvent.click(getByText('a (2)'))6455 // Ungrouped rows should be styled6456 const ungroupedRows = getRows(container, '.ungrouped')6457 expect(ungroupedRows).toHaveLength(2)6458 ungroupedRows.forEach(row => expect(row).toHaveStyle('color: #bbb'))6459 })6460 it('header render functions and footer render functions can access sub rows', () => {6461 const assertProps = (colInfo, state) => {6462 const { column, data } = colInfo6463 expect(column.id).toEqual('a')6464 expect(column.name).toEqual('col-a')6465 expect(column.filterValue).toEqual(undefined)6466 const expectedRows = [6467 {6468 c: 'x',6469 a: null,6470 b: null,6471 _subRows: [6472 {6473 a: 'a',6474 b: null,6475 c: 'x',6476 _subRows: [6477 { b: 1, a: 'a', c: 'x' },6478 { b: 2, a: 'a', c: 'x' }6479 ]6480 },6481 { a: 'b', b: null, c: 'x', _subRows: [{ b: 3, a: 'b', c: 'x' }] }6482 ]6483 }6484 ]6485 expect(data).toEqual(expectedRows)6486 expect(state.page).toEqual(0)6487 expect(state.pageSize).toEqual(10)6488 expect(state.pages).toEqual(1)6489 expect(state.sorted).toEqual([])6490 expect(state.groupBy).toEqual(['c', 'a'])6491 expect(state.filters).toEqual([])6492 expect(state.searchValue).toEqual(undefined)6493 expect(state.pageRows).toEqual(expectedRows)6494 expect(state.sortedData).toEqual(expectedRows)6495 expect(state.data).toEqual([6496 { a: 'a', b: 1, c: 'x' },6497 { a: 'a', b: 2, c: 'x' },6498 { a: 'b', b: 3, c: 'x' }6499 ])6500 }6501 const props = {6502 data: { a: ['a', 'a', 'b'], b: [1, 2, 3], c: ['x', 'x', 'x'] },6503 columns: [6504 {6505 name: 'col-a',6506 accessor: 'a',6507 header: (colInfo, state) => {6508 assertProps(colInfo, state)6509 return `header_${colInfo.data.length}_${colInfo.data[0]._subRows.length}`6510 },6511 footer: (colInfo, state) => {6512 assertProps(colInfo, state)6513 return `footer_${colInfo.data.length}_${colInfo.data[0]._subRows.length}`6514 },6515 headerClassName: 'header-a',6516 footerClassName: 'footer-a'6517 },6518 { name: 'col-b', accessor: 'b' },6519 { name: 'col-c', accessor: 'c' }6520 ],6521 pivotBy: ['c', 'a']6522 }6523 const { container } = render(<Reactable {...props} />)6524 expect(container.querySelector('.header-a').textContent).toEqual('header_1_2')6525 expect(container.querySelector('.footer-a').textContent).toEqual('footer_1_2')6526 })6527 it('non-expander grouped cells should expand grouped rows when clicked', () => {6528 const getNonExpanderCells = container => {6529 const expandableCells = [...getExpandableCells(container)]6530 return expandableCells.filter(cell => cell.textContent === '')6531 }6532 const props = {6533 data: { a: [1, 2, 3], b: ['b--a', 'b--b', 'b--c'], c: ['x', 'x', 'z'] },6534 columns: [6535 { name: 'col-a', accessor: 'a' },6536 { name: 'col-b', accessor: 'b' },6537 { name: 'col-c', accessor: 'c' }6538 ],6539 pivotBy: ['c', 'a']6540 }6541 const { container, getByText } = render(<Reactable {...props} />)6542 expect(getRows(container)).toHaveLength(2)6543 expect(getExpanders(container)).toHaveLength(2)6544 expect(getExpandableCells(container)).toHaveLength(4)6545 expect(getNonExpanderCells(container)).toHaveLength(2)6546 expect(getNonExpanderCells(container)[0]).toEqual(getExpandableCells(container)[1])6547 expect(getNonExpanderCells(container)[1]).toEqual(getExpandableCells(container)[3])6548 fireEvent.click(getNonExpanderCells(container)[0]) // Aggregated cell for column a, group x (2)6549 expect(getRows(container)).toHaveLength(4)6550 expect(getExpandableCells(container)).toHaveLength(8)6551 expect(getNonExpanderCells(container)).toHaveLength(4)6552 expect(getByText('1 (1)')).toBeVisible()6553 expect(getByText('2 (1)')).toBeVisible()6554 fireEvent.click(getNonExpanderCells(container)[3]) // Aggregated cell for column a, group z (1)6555 expect(getRows(container)).toHaveLength(5)6556 expect(getExpandableCells(container)).toHaveLength(10)6557 expect(getNonExpanderCells(container)).toHaveLength(5)6558 expect(getByText('3 (1)')).toBeVisible()6559 fireEvent.click(getNonExpanderCells(container)[1]) // Placeholder for column c, group 1 (1)6560 expect(getByText('b--a')).toBeVisible()6561 fireEvent.click(getNonExpanderCells(container)[2]) // Placeholder for column c, group 2 (1)6562 expect(getByText('b--b')).toBeVisible()6563 fireEvent.click(getNonExpanderCells(container)[4]) // Placeholder for column c, group 3 (1)6564 expect(getByText('b--c')).toBeVisible()6565 // No other grouped cells (for leaf rows) should be expandable6566 expect(getExpandableCells(container)).toHaveLength(10)6567 expect(getNonExpanderCells(container)).toHaveLength(5)6568 // Non-expander cells should collapse rows6569 fireEvent.click(getNonExpanderCells(container)[0]) // Aggregated cell for column a, group x (2)6570 fireEvent.click(getNonExpanderCells(container)[1]) // Aggregated cell for column a, group z (1)6571 expect(getRows(container)).toHaveLength(2)6572 expect(getNonExpanderCells(container)).toHaveLength(2)6573 })6574 it('table updates when groupBy changes', () => {6575 const props = {6576 data: { a: [1, 2], b: ['a', 'b'] },6577 columns: [6578 { name: 'col-a', accessor: 'a' },6579 { name: 'col-b', accessor: 'b' }6580 ]6581 }6582 const { container, rerender } = render(<Reactable {...props} />)6583 expect(getExpanders(container)).toHaveLength(0)6584 expect(getRows(container)).toHaveLength(2)6585 rerender(<Reactable {...props} pivotBy={['b']} />)6586 expect(getExpanders(container)).toHaveLength(2)6587 })6588 it('row expanded state persists when groupBy changes', () => {6589 const props = {6590 data: { a: [1, 2], b: ['a', 'b'], c: ['x', 'y'] },6591 columns: [6592 { name: 'col-a', accessor: 'a' },6593 { name: 'col-b', accessor: 'b' },6594 { name: 'col-c', accessor: 'c' }6595 ],6596 pivotBy: ['c']6597 }6598 const { container, rerender } = render(<Reactable {...props} />)6599 const expanders = getExpanders(container)6600 expect(expanders).toHaveLength(2)6601 expect(getRows(container)).toHaveLength(2)6602 fireEvent.click(expanders[0])6603 expect(getRows(container)).toHaveLength(3)6604 // Adding groupBy columns6605 rerender(<Reactable {...props} pivotBy={['c', 'b']} />)6606 expect(getExpanders(container)).toHaveLength(3)6607 expect(getRows(container)).toHaveLength(3)6608 // Removing groupBy columns6609 rerender(<Reactable {...props} pivotBy={['c']} />)6610 expect(getExpanders(container)).toHaveLength(2)6611 expect(getRows(container)).toHaveLength(3)6612 })6613 it('row expanded state persists by row ID when groupBy changes', () => {6614 const props = {6615 data: { a: [1, 2], b: ['a', 'b'], c: ['x', 'y'] },6616 columns: [6617 { name: 'col-a', accessor: 'a', details: ['row-details-1', 'row-details-2'] },6618 { name: 'col-b', accessor: 'b' },6619 { name: 'col-c', accessor: 'c' }6620 ]6621 }6622 const { container, getByText, queryByText, rerender } = render(<Reactable {...props} />)6623 let expanders = getExpanders(container)6624 expect(expanders).toHaveLength(2)6625 expect(getRows(container)).toHaveLength(2)6626 // Expanded state should not persist by relative row index (like in v6).6627 // Details row expansion should not transfer to grouped rows.6628 fireEvent.click(expanders[0])6629 expect(queryByText('row-details-1')).toBeVisible()6630 expect(getRows(container)).toHaveLength(2)6631 rerender(<Reactable {...props} pivotBy={['c']} />)6632 expect(queryByText('row-details-1')).toBeFalsy()6633 expect(getRows(container)).toHaveLength(2)6634 // Details row should still be expanded6635 expanders = getExpanders(container)6636 expect(expanders).toHaveLength(2)6637 fireEvent.click(expanders[0])6638 expect(getRows(container)).toHaveLength(3)6639 expect(getByText('row-details-1')).toBeVisible()6640 })6641 it('table updates when defaultExpanded changes', () => {6642 const props = {6643 data: { a: [1, 2, 3], b: ['a', 'b', 'c'], c: ['x', 'x', 'z'] },6644 columns: [6645 { name: 'a', accessor: 'a', details: rowInfo => `row details: ${rowInfo.index}-a` },6646 { name: 'b', accessor: 'b' },6647 { name: 'c', accessor: 'c' }6648 ],6649 pivotBy: ['c'],6650 defaultExpanded: true6651 }6652 const { container, rerender } = render(<Reactable {...props} />)6653 expect(getExpanders(container)).toHaveLength(5)6654 expect(getRows(container)).toHaveLength(5)6655 rerender(<Reactable {...props} defaultExpanded={false} />)6656 expect(getExpanders(container)).toHaveLength(2)6657 expect(getRows(container)).toHaveLength(2)6658 rerender(<Reactable {...props} defaultExpanded={true} />)6659 expect(getExpanders(container)).toHaveLength(5)6660 expect(getRows(container)).toHaveLength(5)6661 })6662 it('defaultExpanded works with grouped rows and row details', () => {6663 const props = {6664 data: { a: [1, 2, 3], b: ['a', 'b', 'c'], c: ['x', 'x', 'z'] },6665 columns: [6666 { name: 'a', accessor: 'a', details: rowInfo => `row details: ${rowInfo.index}-a` },6667 { name: 'b', accessor: 'b' },6668 { name: 'c', accessor: 'c' }6669 ],6670 pivotBy: ['c'],6671 defaultExpanded: true6672 }6673 const { container, getByText, rerender } = render(<Reactable {...props} />)6674 expect(getExpanders(container)).toHaveLength(5)6675 expect(getRows(container)).toHaveLength(5)6676 expect(getByText('row details: 0-a')).toBeVisible()6677 expect(getByText('row details: 1-a')).toBeVisible()6678 expect(getByText('row details: 2-a')).toBeVisible()6679 // When adding new groupBy columns, previous expanded state should persist.6680 // New groupBy columns should also be expanded, but this does not currently work.6681 rerender(<Reactable {...props} pivotBy={['c', 'b']} />)6682 expect(getRows(container)).toHaveLength(5)6683 expect(getRowDetails(container)).toHaveLength(0)6684 rerender(<Reactable {...props} pivotBy={['c', 'b']} defaultExpanded={false} />)6685 expect(getRows(container)).toHaveLength(2)6686 rerender(<Reactable {...props} pivotBy={['c', 'b']} defaultExpanded={true} />)6687 expect(getRows(container)).toHaveLength(8)6688 })6689 it('grouped state persists when data changes', () => {6690 const props = {6691 data: { a: [1, 2], b: ['a', 'b'] },6692 columns: [6693 { name: 'col-a', accessor: 'a' },6694 { name: 'col-b', accessor: 'b' }6695 ],6696 pivotBy: ['b']6697 }6698 const { container, rerender } = render(<Reactable {...props} />)6699 expect(getExpanders(container)).toHaveLength(2)6700 rerender(<Reactable {...props} data={{ a: [1, 2, 3], b: ['a', 'b', 'c'] }} />)6701 expect(getExpanders(container)).toHaveLength(3)6702 })6703 it('groupBy columns work with column groups', () => {6704 const props = {6705 data: { a: [1, 2, 3], b: ['a', 'b', 'c'], c: ['x', 'x', 'z'] },6706 columns: [6707 { name: 'a', accessor: 'a' },6708 { name: 'b', accessor: 'b' },6709 { name: 'c', accessor: 'c' }6710 ],6711 columnGroups: [{ columns: ['a', 'c'], name: 'group' }],6712 pivotBy: ['c']6713 }6714 const { container } = render(<Reactable {...props} />)6715 const columnHeaders = getColumnHeaders(container)6716 expect([...columnHeaders].map(header => header.textContent)).toEqual(['c', 'a', 'b'])6717 const groupHeaders = getGroupHeaders(container)6718 expect(groupHeaders).toHaveLength(1)6719 expect(groupHeaders[0]).toHaveAttribute('aria-colspan', '2')6720 expect(getUngroupedHeaders(container)).toHaveLength(1)6721 })6722 it('groupBy columns work with split column groups', () => {6723 const props = {6724 data: { a: [1, 2, 3], b: ['a', 'b', 'c'], c: ['x', 'x', 'z'] },6725 columns: [6726 { name: 'a', accessor: 'a' },6727 { name: 'b', accessor: 'b' },6728 { name: 'c', accessor: 'c' }6729 ],6730 columnGroups: [{ columns: ['c', 'b'], name: 'group' }],6731 pivotBy: ['c']6732 }6733 const { container } = render(<Reactable {...props} />)6734 const columnHeaders = getColumnHeaders(container)6735 // groupBy columns should still be first6736 expect([...columnHeaders].map(header => header.textContent)).toEqual(['c', 'a', 'b'])6737 const groupHeaders = getGroupHeaders(container)6738 expect(groupHeaders).toHaveLength(2)6739 expect(groupHeaders[0]).toHaveAttribute('aria-colspan', '1')6740 expect(groupHeaders[1]).toHaveAttribute('aria-colspan', '1')6741 expect([...groupHeaders].map(header => header.textContent)).toEqual(['group', 'group'])6742 expect(getUngroupedHeaders(container)).toHaveLength(1)6743 })6744 it('expanders language', () => {6745 const props = {6746 data: { a: [1, 2], b: ['a', 'b'] },6747 columns: [6748 { name: 'a', accessor: 'a' },6749 { name: 'b', accessor: 'b' }6750 ],6751 language: {6752 groupExpandLabel: '_Toggle group'6753 },6754 pivotBy: ['a']6755 }6756 const { container } = render(<Reactable {...props} />)6757 const expanders = getExpanders(container)6758 expect(expanders[0]).toHaveAttribute('aria-label', '_Toggle group')6759 fireEvent.click(expanders[0])6760 expect(expanders[0]).toHaveAttribute('aria-label', '_Toggle group')6761 })6762})6763describe('sub rows', () => {6764 it('subRows is a valid column ID', () => {6765 const props = {6766 data: { a: [1, 2], b: [3, 4], subRows: ['a', 'b'] },6767 columns: [6768 { name: 'colA', accessor: 'a' },6769 { name: 'colB', accessor: 'b' },6770 { name: 'colSubRows', accessor: 'subRows' }6771 ]6772 }6773 const { container, getByText } = render(<Reactable {...props} />)6774 const headers = getHeaders(container)6775 expect(headers).toHaveLength(3)6776 expect(getByText('colSubRows')).toBeVisible()6777 })6778 it('handles data with sub rows', () => {6779 const props = {6780 data: { a: [1, 2], b: [3, 4], '.subRows': [{ a: [5, 6], b: [7, 8] }, null] },6781 columns: [6782 { name: 'colA', accessor: 'a' },6783 { name: 'colB', accessor: 'b' }6784 ]6785 }6786 const { container } = render(<Reactable {...props} />)6787 const headers = getHeaders(container)6788 expect(headers).toHaveLength(2)6789 })6790})6791describe('cell click actions', () => {6792 it('expands row details on click', () => {6793 const props = {6794 data: { a: ['aaa1', 'aaa2'], b: ['bbb1', 'bbb2'], c: ['ccc1', 'ccc2'] },6795 columns: [6796 { name: 'a', accessor: 'a' },6797 { name: 'b', accessor: 'b', details: ['detail-b', null] },6798 { name: 'c', accessor: 'c', details: ['detail-c', 'detail-c'] }6799 ],6800 onClick: 'expand'6801 }6802 const { container, getByText, queryByText } = render(<Reactable {...props} />)6803 const expanders = getExpanders(container)6804 expect(expanders).toHaveLength(3)6805 fireEvent.click(getByText('aaa1'))6806 // Should expand first details column6807 expect(getByText('detail-b')).toBeVisible()6808 expect(queryByText('detail-c')).toEqual(null)6809 // Should work fine with expander buttons6810 fireEvent.click(expanders[0])6811 expect(queryByText('detail-b')).toEqual(null)6812 fireEvent.click(expanders[0])6813 expect(getByText('detail-b')).toBeVisible()6814 // Collapse row6815 fireEvent.click(getByText('aaa1'))6816 expect(queryByText('detail-b')).toEqual(null)6817 })6818 it('expands row details on click with column groups', () => {6819 const props = {6820 data: { a: ['aaa1', 'aaa2'], b: ['bbb1', 'bbb2'] },6821 columns: [6822 { name: 'col-a', accessor: 'a', details: ['row-details-1', 'row-details-2'] },6823 { name: 'col-b', accessor: 'b' }6824 ],6825 columnGroups: [{ columns: ['a', 'b'] }],6826 onClick: 'expand'6827 }6828 const { getByText } = render(<Reactable {...props} />)6829 fireEvent.click(getByText('bbb1'))6830 expect(getByText('row-details-1')).toBeVisible()6831 fireEvent.click(getByText('bbb2'))6832 expect(getByText('row-details-2')).toBeVisible()6833 })6834 it('expands grouped rows on click', () => {6835 const props = {6836 data: { a: [1, 1, 2], b: ['b-a', 'b-b', 'b-c'], c: ['x', 'x', 'x'], d: [1, 2, 3] },6837 columns: [6838 { name: 'col-a', accessor: 'a' },6839 {6840 name: 'col-b',6841 accessor: 'b',6842 aggregated: cellInfo => `b-agg-${cellInfo.level}-${cellInfo.index}`,6843 details: () => 'details-b'6844 },6845 { name: 'col-c', accessor: 'c' },6846 {6847 name: 'col-d',6848 accessor: 'd',6849 aggregated: cellInfo => `d-agg-${cellInfo.level}-${cellInfo.index}`6850 }6851 ],6852 pivotBy: ['c', 'a'],6853 onClick: 'expand'6854 }6855 const { container, getByText } = render(<Reactable {...props} />)6856 expect(getRows(container)).toHaveLength(1)6857 // Non-expandable cell should expand and collapse6858 fireEvent.click(getByText('b-agg-0-0'))6859 expect(getRows(container)).toHaveLength(3)6860 fireEvent.click(getByText('b-agg-0-0'))6861 expect(getRows(container)).toHaveLength(1)6862 // Expandable cells should still work6863 const expandableCells = getExpandableCells(container)6864 fireEvent.click(expandableCells[1])6865 expect(getRows(container)).toHaveLength(3)6866 // Expanders should still work6867 const expanders = getExpanders(container)6868 fireEvent.click(expanders[0])6869 expect(getRows(container)).toHaveLength(1)6870 fireEvent.click(expanders[0])6871 expect(getRows(container)).toHaveLength(3)6872 fireEvent.click(getByText('d-agg-1-0'))6873 expect(getRows(container)).toHaveLength(5)6874 // Clicking should still expand row details6875 fireEvent.click(getByText('b-b'))6876 expect(getByText('details-b')).toBeVisible()6877 })6878 it('ignores pad rows on click', () => {6879 const props = {6880 data: { a: ['aaa1', 'aaa2'] },6881 columns: [{ name: 'a', accessor: 'a', details: ['detail-a', 'detail-a', 'detail-a'] }],6882 onClick: 'expand',6883 minRows: 56884 }6885 const { container, queryByText } = render(<Reactable {...props} />)6886 const padRows = getPadRows(container)6887 fireEvent.click(getCells(padRows[0])[0])6888 expect(queryByText('detail-a')).toEqual(null)6889 })6890 it('custom onClick actions', () => {6891 let clickCount = 06892 const props = {6893 data: { a: ['aaa1', 'aaa2'], b: ['bbb1', 'bbb2'], c: ['ccc1', 'ccc2'] },6894 columns: [6895 { name: 'col-a', accessor: 'a' },6896 { name: 'col-b', accessor: 'b' },6897 { name: 'col-c', accessor: 'c' }6898 ],6899 onClick: (rowInfo, colInfo, state) => {6900 if (clickCount < 2) {6901 expect(colInfo.id).toEqual('b')6902 expect(colInfo.name).toEqual('col-b')6903 expect(rowInfo.index).toEqual(1)6904 expect(rowInfo.viewIndex).toEqual(1)6905 expect(rowInfo.aggregated).toBeFalsy()6906 expect(rowInfo.level).toEqual(0)6907 expect(rowInfo.expanded).toBeFalsy()6908 expect(rowInfo.selected).toEqual(false)6909 expect(rowInfo.values).toEqual({ a: 'aaa2', b: 'bbb2', c: 'ccc2' })6910 expect(rowInfo.row).toEqual({ a: 'aaa2', b: 'bbb2', c: 'ccc2' })6911 expect(rowInfo.subRows).toEqual([])6912 expect(state.page).toEqual(0)6913 expect(state.pageSize).toEqual(10)6914 expect(state.pages).toEqual(1)6915 expect(state.sorted).toEqual([])6916 expect(state.groupBy).toEqual([])6917 expect(state.filters).toEqual([])6918 expect(state.searchValue).toEqual(undefined)6919 expect(state.selected).toEqual([])6920 expect(state.pageRows).toEqual([6921 { a: 'aaa1', b: 'bbb1', c: 'ccc1' },6922 { a: 'aaa2', b: 'bbb2', c: 'ccc2' }6923 ])6924 expect(state.sortedData).toEqual([6925 { a: 'aaa1', b: 'bbb1', c: 'ccc1' },6926 { a: 'aaa2', b: 'bbb2', c: 'ccc2' }6927 ])6928 expect(state.data).toEqual([6929 { a: 'aaa1', b: 'bbb1', c: 'ccc1' },6930 { a: 'aaa2', b: 'bbb2', c: 'ccc2' }6931 ])6932 } else {6933 expect(colInfo.id).toEqual('c')6934 expect(rowInfo.index).toEqual(0)6935 expect(rowInfo.viewIndex).toEqual(0)6936 }6937 clickCount++6938 },6939 minRows: 56940 }6941 const { container, getByText } = render(<Reactable {...props} />)6942 fireEvent.click(getByText('bbb2'))6943 expect(clickCount).toEqual(1)6944 fireEvent.click(getByText('bbb2'))6945 expect(clickCount).toEqual(2)6946 fireEvent.click(getByText('ccc1'))6947 expect(clickCount).toEqual(3)6948 // Pad rows should not be clickable6949 const padRows = getPadRows(container)6950 fireEvent.click(getCells(padRows[0])[0])6951 expect(clickCount).toEqual(3)6952 })6953})6954describe('pagination', () => {6955 it('defaultPageSize', () => {6956 const props = {6957 data: { a: [1, 2, 3, 4, 5, 6, 7] },6958 columns: [{ name: 'a', accessor: 'a' }],6959 defaultPageSize: 26960 }6961 const { container } = render(<Reactable {...props} />)6962 expect(getRows(container)).toHaveLength(2)6963 })6964 it('table updates when defaultPageSize changes', () => {6965 const props = {6966 data: { a: [1, 2, 3, 4, 5, 6, 7] },6967 columns: [{ name: 'a', accessor: 'a' }],6968 defaultPageSize: 26969 }6970 const { container, rerender } = render(<Reactable {...props} />)6971 expect(getRows(container)).toHaveLength(2)6972 rerender(<Reactable {...props} defaultPageSize={3} />)6973 expect(getRows(container)).toHaveLength(3)6974 rerender(<Reactable {...props} defaultPageSize={7} />)6975 expect(getRows(container)).toHaveLength(7)6976 })6977 it('shows or hides pagination', () => {6978 const props = {6979 data: { a: [1, 2], b: ['a', 'b'] },6980 columns: [6981 { name: 'a', accessor: 'a' },6982 { name: 'b', accessor: 'b' }6983 ]6984 }6985 // Auto-hidden if table always fits on one page6986 const { container, rerender } = render(<Reactable {...props} defaultPageSize={2} />)6987 expect(getPagination(container)).toEqual(null)6988 // Auto-shown if default page size causes paging6989 rerender(6990 <Reactable {...props} defaultPageSize={1} showPageSizeOptions pageSizeOptions={[10, 20]} />6991 )6992 expect(getPagination(container)).toBeVisible()6993 // Auto-shown if page size option causes paging6994 rerender(6995 <Reactable {...props} defaultPageSize={20} showPageSizeOptions pageSizeOptions={[1, 20]} />6996 )6997 expect(getPagination(container)).toBeVisible()6998 // Force show pagination6999 rerender(7000 <Reactable7001 {...props}7002 showPagination7003 defaultPageSize={2}7004 showPageSizeOptions7005 pageSizeOptions={[2]}7006 />7007 )7008 expect(getPagination(container)).toBeVisible()7009 // Force hide pagination7010 rerender(7011 <Reactable7012 {...props}7013 showPagination={false}7014 defaultPageSize={1}7015 showPageSizeOptions7016 pageSizeOptions={[10, 20]}7017 />7018 )7019 expect(getPagination(container)).toEqual(null)7020 })7021 it('auto-shown pagination persists after filtering', () => {7022 const props = {7023 data: { a: [111, 222, 333], b: ['aaa', 'aaa', 'ccc'] },7024 columns: [7025 { name: 'a', accessor: 'a' },7026 { name: 'b', accessor: 'b', filterable: true }7027 ],7028 defaultPageSize: 2,7029 searchable: true7030 }7031 const { container } = render(<Reactable {...props} />)7032 expect(getPagination(container)).toBeVisible()7033 const filter = getFilters(container)[0]7034 fireEvent.change(filter, { target: { value: 'aaa' } })7035 expect(getRows(container)).toHaveLength(2)7036 expect(getPagination(container)).toBeVisible()7037 const searchInput = getSearchInput(container)7038 fireEvent.change(searchInput, { target: { value: '222' } })7039 expect(getRows(container)).toHaveLength(1)7040 expect(getPagination(container)).toBeVisible()7041 })7042 it('auto-shows pagination when expanded rows would cause table to span multiple pages', () => {7043 const props = {7044 data: {7045 group: ['a', 'a', 'a', 'a'],7046 a: [111, 111, 222, 33]7047 },7048 columns: [7049 { name: 'group', accessor: 'group' },7050 { name: 'col-a', accessor: 'a' }7051 ],7052 defaultPageSize: 4,7053 pivotBy: ['group'],7054 paginateSubRows: true,7055 searchable: true7056 }7057 const { container } = render(<Reactable {...props} />)7058 expect(getRows(container)).toHaveLength(1)7059 expect(getPagination(container)).toBeVisible()7060 expect(getPageInfo(container).textContent).toEqual('1–1 of 1 rows')7061 fireEvent.click(getExpanders(container)[0])7062 expect(getRows(container)).toHaveLength(4)7063 expect(getPagination(container)).toBeVisible()7064 expect(getPageInfo(container).textContent).toEqual('1–4 of 5 rows')7065 // Pagination should persist after filtering7066 const searchInput = getSearchInput(container)7067 fireEvent.change(searchInput, { target: { value: '222' } })7068 expect(getRows(container)).toHaveLength(2)7069 expect(getPagination(container)).toBeVisible()7070 expect(getPageInfo(container).textContent).toEqual('1–2 of 2 rows')7071 })7072 it('auto-shown pagination works when data changes', () => {7073 const props = {7074 data: { a: [1, 2, 3, 4, 5] },7075 columns: [{ name: 'col-a', accessor: 'a' }],7076 defaultPageSize: 47077 }7078 const { container, rerender } = render(<Reactable {...props} />)7079 expect(getPagination(container)).toBeVisible()7080 expect(getPageInfo(container).textContent).toEqual('1–4 of 5 rows')7081 rerender(<Reactable {...props} data={{ a: [1, 2, 3, 4] }} />)7082 expect(getPagination(container)).toEqual(null)7083 })7084 it('page info', () => {7085 const props = {7086 data: { a: [1, 2, 3, 4, 5], b: ['a', 'b', 'c', 'd', 'e'] },7087 columns: [7088 { name: 'a', accessor: 'a' },7089 { name: 'b', accessor: 'b' }7090 ],7091 defaultPageSize: 27092 }7093 let { container, rerender } = render(<Reactable {...props} />)7094 let pageInfo = getPageInfo(container)7095 expect(pageInfo.textContent).toEqual('1–2 of 5 rows')7096 expect(pageInfo).toHaveAttribute('aria-live', 'polite')7097 const nextButton = getNextButton(container)7098 fireEvent.click(nextButton)7099 expect(pageInfo.textContent).toEqual('3–4 of 5 rows')7100 fireEvent.click(nextButton)7101 expect(pageInfo.textContent).toEqual('5–5 of 5 rows')7102 // Updates on filtering7103 rerender(<Reactable {...props} filterable />)7104 const filter = getFilters(container)[0]7105 fireEvent.change(filter, { target: { value: '11' } })7106 expect(pageInfo.textContent).toEqual('0–0 of 0 rows')7107 fireEvent.change(filter, { target: { value: '' } })7108 // Hide page info7109 rerender(<Reactable {...props} showPageInfo={false} />)7110 pageInfo = getPageInfo(container)7111 expect(pageInfo).toEqual(null)7112 // Language7113 rerender(7114 <Reactable7115 {...props}7116 showPageInfo7117 language={{ pageInfo: '_{rowStart} to {rowEnd} of {rows}' }}7118 />7119 )7120 pageInfo = getPageInfo(container)7121 expect(pageInfo.textContent).toEqual('_1 to 2 of 5')7122 })7123 it('page size options', () => {7124 const props = {7125 data: { a: [1, 2, 3, 4, 5], b: ['_a1', '_b2', '_c3', '_d4', '_e5'] },7126 columns: [7127 { name: 'a', accessor: 'a' },7128 { name: 'b', accessor: 'b' }7129 ],7130 defaultPageSize: 2,7131 showPageSizeOptions: true,7132 pageSizeOptions: [2, 4, 6]7133 }7134 const { container, rerender } = render(<Reactable {...props} />)7135 let pageSizeOptions = getPageSizeOptions(container)7136 let pageSizeSelect = getPageSizeSelect(container)7137 expect(pageSizeOptions.textContent).toEqual('Show 246')7138 expect(pageSizeSelect).toHaveAttribute('aria-label', 'Rows per page')7139 // Options7140 const options = pageSizeSelect.querySelectorAll('option')7141 expect(options).toHaveLength(3)7142 options.forEach((option, i) =>7143 expect(option.textContent).toEqual(`${props.pageSizeOptions[i]}`)7144 )7145 // Change page size7146 fireEvent.change(pageSizeSelect, { target: { value: 4 } })7147 expect(getRows(container)).toHaveLength(4)7148 expect(getPageInfo(container).textContent).toEqual('1–4 of 5 rows')7149 // Hide page size options7150 rerender(<Reactable {...props} showPageSizeOptions={false} />)7151 expect(getPageSizeOptions(container)).toEqual(null)7152 // No page info shown7153 rerender(<Reactable {...props} showPageInfo={false} />)7154 expect(getPageSizeOptions(container).textContent).toEqual('Show 246')7155 // Language7156 rerender(7157 <Reactable7158 {...props}7159 language={{ pageSizeOptions: '_Show {rows}', pageSizeOptionsLabel: '_Rows per page' }}7160 />7161 )7162 pageSizeOptions = getPageSizeOptions(container)7163 pageSizeSelect = getPageSizeSelect(container)7164 expect(pageSizeOptions.textContent).toEqual('_Show 246')7165 expect(pageSizeSelect).toHaveAttribute('aria-label', '_Rows per page')7166 })7167 it('simple page navigation', () => {7168 const props = {7169 data: { a: [1, 2, 3, 4, 5], b: ['_a1', '_b2', '_c3', '_d4', '_e5'] },7170 columns: [7171 { name: 'a', accessor: 'a' },7172 { name: 'b', accessor: 'b' }7173 ],7174 defaultPageSize: 2,7175 paginationType: 'simple'7176 }7177 const { container, queryByText, rerender } = render(<Reactable {...props} />)7178 const pageNumbers = getPageNumbers(container)7179 const prevButton = getPrevButton(container)7180 const nextButton = getNextButton(container)7181 expect(pageNumbers.textContent).toEqual('1 of 3')7182 expect(queryByText('_e5')).toEqual(null)7183 // First page: previous button should be disabled7184 expect(prevButton).toHaveAttribute('disabled')7185 expect(prevButton).toHaveAttribute('aria-disabled', 'true')7186 fireEvent.click(prevButton)7187 expect(pageNumbers.textContent).toEqual('1 of 3')7188 fireEvent.click(nextButton)7189 expect(pageNumbers.textContent).toEqual('2 of 3')7190 expect(prevButton).not.toHaveAttribute('disabled')7191 expect(prevButton).not.toHaveAttribute('aria-disabled')7192 expect(nextButton).not.toHaveAttribute('aria-disabled')7193 fireEvent.click(nextButton)7194 expect(pageNumbers.textContent).toEqual('3 of 3')7195 expect(queryByText('_e5')).toBeVisible()7196 // Last page: next button should be disabled7197 fireEvent.click(nextButton)7198 expect(pageNumbers.textContent).toEqual('3 of 3')7199 expect(nextButton).toHaveAttribute('disabled')7200 expect(nextButton).toHaveAttribute('aria-disabled', 'true')7201 fireEvent.click(prevButton)7202 expect(pageNumbers.textContent).toEqual('2 of 3')7203 // Language7204 let language = {7205 pageNext: '_Next',7206 pagePrevious: '_Previous',7207 pageNumbers: '_{page} of {pages}',7208 pageNextLabel: '_Next page',7209 pagePreviousLabel: '_Previous page'7210 }7211 rerender(<Reactable {...props} language={language} />)7212 expect(prevButton.textContent).toEqual('_Previous')7213 expect(nextButton.textContent).toEqual('_Next')7214 expect(prevButton).toHaveAttribute('aria-label', '_Previous page')7215 expect(nextButton).toHaveAttribute('aria-label', '_Next page')7216 expect(pageNumbers.textContent).toEqual('_1 of 3')7217 language = {7218 pageNext: '',7219 pagePrevious: null,7220 pageNextLabel: '',7221 pagePreviousLabel: null7222 }7223 rerender(<Reactable {...props} language={language} />)7224 expect(prevButton).not.toHaveTextContent()7225 expect(nextButton).not.toHaveTextContent()7226 expect(prevButton).not.toHaveAttribute('aria-label')7227 expect(nextButton).not.toHaveAttribute('aria-label')7228 })7229 it('page number buttons', () => {7230 const props = {7231 data: { a: [1, 2, 3, 4, 5], b: ['_a1', '_b2', '_c3', '_d4', '_e5'] },7232 columns: [7233 { name: 'a', accessor: 'a' },7234 { name: 'b', accessor: 'b' }7235 ],7236 defaultPageSize: 1,7237 paginationType: 'numbers'7238 }7239 const { container, queryAllByText, rerender } = render(<Reactable {...props} />)7240 let pageButtons = [...getPageButtons(container)]7241 let pageNumberBtns = pageButtons.slice(1, pageButtons.length - 1)7242 expect(pageNumberBtns).toHaveLength(5)7243 pageNumberBtns.forEach((btn, i) => {7244 const page = i + 17245 expect(btn.textContent).toEqual(`${page}`)7246 if (page === 1) {7247 expect(btn).toHaveAttribute('aria-current', 'page')7248 expect(btn).toHaveAttribute('aria-label', `Page ${page} `)7249 } else {7250 expect(btn).toHaveAttribute('aria-label', `Page ${page}`)7251 }7252 })7253 fireEvent.click(pageNumberBtns[1])7254 const pageInfo = getPageInfo(container)7255 expect(pageInfo.textContent).toEqual('2–2 of 5 rows')7256 expect(pageNumberBtns[0]).not.toHaveClass('rt-page-button-current')7257 expect(pageNumberBtns[1]).toHaveClass('rt-page-button-current')7258 expect(pageNumberBtns[1]).toHaveAttribute('aria-current', 'page')7259 // Changing to the same page should be a no-op7260 fireEvent.click(pageNumberBtns[1])7261 expect(pageInfo.textContent).toEqual('2–2 of 5 rows')7262 expect(pageNumberBtns[1]).toHaveClass('rt-page-button-current')7263 fireEvent.click(pageNumberBtns[4])7264 expect(pageInfo.textContent).toEqual('5–5 of 5 rows')7265 // Should update on external page changes7266 const prevButton = getPrevButton(container)7267 const nextButton = getNextButton(container)7268 fireEvent.click(prevButton)7269 expect(pageNumberBtns[3]).toHaveClass('rt-page-button-current')7270 fireEvent.click(nextButton)7271 expect(pageNumberBtns[4]).toHaveClass('rt-page-button-current')7272 // Pages with ellipses7273 const data = { a: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] }7274 rerender(<Reactable {...props} data={data} />)7275 fireEvent.click(pageNumberBtns[0]) // page 17276 let ellipses = queryAllByText('...')7277 expect(ellipses).toHaveLength(1)7278 pageButtons = [...getPageButtons(container)]7279 fireEvent.click(pageNumberBtns[4]) // page 57280 ellipses = queryAllByText('...')7281 expect(ellipses).toHaveLength(2)7282 // Language7283 rerender(<Reactable {...props} language={{ pageNumberLabel: '_Page {page}' }} />)7284 pageButtons = [...getPageButtons(container)]7285 pageNumberBtns = pageButtons.slice(1, pageButtons.length - 1)7286 pageNumberBtns.forEach((btn, i) => {7287 const page = i + 17288 if (btn.hasAttribute('aria-current')) {7289 expect(btn).toHaveAttribute('aria-label', `_Page ${page} `)7290 } else {7291 expect(btn).toHaveAttribute('aria-label', `_Page ${page}`)7292 }7293 })7294 })7295 it('page jump', () => {7296 const props = {7297 data: { a: [1, 2, 3, 4, 5], b: ['_a1', '_b2', '_c3', '_d4', '_e5'] },7298 columns: [7299 { name: 'a', accessor: 'a' },7300 { name: 'b', accessor: 'b' }7301 ],7302 defaultPageSize: 2,7303 paginationType: 'jump'7304 }7305 const { container, rerender } = render(<Reactable {...props} />)7306 const pageJump = getPageJump(container)7307 const pageNumbers = getPageNumbers(container)7308 expect(pageJump).toHaveAttribute('value', '1')7309 expect(pageJump).toHaveAttribute('aria-label', 'Go to page')7310 expect(pageNumbers.textContent).toEqual(' of 3')7311 const pageInfo = getPageInfo(container)7312 fireEvent.change(pageJump, { target: { value: 2 } })7313 // Shouldn't change page yet7314 expect(pageInfo.textContent).toEqual('1–2 of 5 rows')7315 // Should change page on unfocus7316 fireEvent.blur(pageJump)7317 expect(pageInfo.textContent).toEqual('3–4 of 5 rows')7318 fireEvent.change(pageJump, { target: { value: 1 } })7319 // Should change page on enter keypress7320 fireEvent.keyPress(pageJump, { key: 'Enter', code: 13, charCode: 13 })7321 expect(pageInfo.textContent).toEqual('1–2 of 5 rows')7322 // Should update on external page changes7323 const nextButton = getNextButton(container)7324 fireEvent.click(nextButton)7325 expect(pageJump).toHaveAttribute('value', '2')7326 // Values out of range should be reset to nearest valid value7327 fireEvent.change(pageJump, { target: { value: '9' } })7328 fireEvent.blur(pageJump)7329 expect(pageJump).toHaveAttribute('value', '3')7330 fireEvent.change(pageJump, { target: { value: '0' } })7331 fireEvent.blur(pageJump)7332 expect(pageJump).toHaveAttribute('value', '1')7333 // Invalid and blank values should be reset to last value7334 fireEvent.change(pageJump, { target: { value: '2' } })7335 fireEvent.blur(pageJump)7336 fireEvent.change(pageJump, { target: { value: '' } })7337 fireEvent.blur(pageJump)7338 expect(pageJump).toHaveAttribute('value', '2')7339 fireEvent.change(pageJump, { target: { value: 'asdf' } })7340 fireEvent.blur(pageJump)7341 expect(pageJump).toHaveAttribute('value', '2')7342 // Language7343 rerender(7344 <Reactable7345 {...props}7346 language={{ pageNumbers: '_{page} of {pages}', pageJumpLabel: '_Go to page' }}7347 />7348 )7349 expect(pageJump).toHaveAttribute('aria-label', '_Go to page')7350 expect(pageNumbers.textContent).toEqual('_ of 3')7351 })7352 it('paginates sub rows', () => {7353 const props = {7354 data: {7355 group: ['a', 'a', 'a', 'a'],7356 a: [111, 111, 222, 33]7357 },7358 columns: [7359 { name: 'group', accessor: 'group' },7360 { name: 'col-a', accessor: 'a' }7361 ],7362 defaultPageSize: 2,7363 pivotBy: ['group'],7364 paginateSubRows: true7365 }7366 const { container, rerender } = render(<Reactable {...props} />)7367 expect(getRows(container)).toHaveLength(1)7368 expect(getPagination(container)).toBeVisible()7369 fireEvent.click(getExpanders(container)[0])7370 expect(getRows(container)).toHaveLength(2)7371 expect(getPageInfo(container).textContent).toEqual('1–2 of 5 rows')7372 // Auto-shown pagination should update when paginateSubRows changes7373 rerender(<Reactable {...props} paginateSubRows={false} />)7374 expect(getRows(container)).toHaveLength(5)7375 expect(getPagination(container)).toEqual(null)7376 })7377 it('does not paginate sub rows by default', () => {7378 const props = {7379 data: {7380 group: ['a', 'a', 'a', 'a'],7381 a: [111, 111, 222, 33]7382 },7383 columns: [7384 { name: 'group', accessor: 'group' },7385 { name: 'col-a', accessor: 'a' }7386 ],7387 defaultPageSize: 2,7388 pivotBy: ['group']7389 }7390 const { container, rerender } = render(<Reactable {...props} />)7391 expect(getRows(container)).toHaveLength(1)7392 expect(getPagination(container)).toEqual(null)7393 fireEvent.click(getExpanders(container)[0])7394 rerender(<Reactable {...props} />)7395 expect(getRows(container)).toHaveLength(5)7396 expect(getPagination(container)).toEqual(null)7397 })7398 it('disabling pagination works', () => {7399 const props = {7400 data: { a: [1, 2, 3], b: ['a', 'b', 'c'] },7401 columns: [7402 {7403 name: 'a',7404 accessor: 'a',7405 cell: (cellInfo, state) => {7406 return `page: ${state.page}, pageSize: ${state.pageSize}, pages: ${state.pages}`7407 }7408 },7409 { name: 'b', accessor: 'b' }7410 ],7411 pagination: false,7412 defaultPageSize: 27413 }7414 const { container } = render(<Reactable {...props} />)7415 expect(getRows(container)).toHaveLength(3)7416 expect(getPagination(container)).toEqual(null)7417 // Pagination properties should still be present in the API7418 expect(getCellsText(container)[0]).toEqual('page: 0, pageSize: 2, pages: 1')7419 })7420 it('disabling pagination works with visible pagination bar (showPagination)', () => {7421 const props = {7422 data: { a: [1, 2, 3], b: ['a', 'b', 'c'] },7423 columns: [7424 {7425 name: 'a',7426 accessor: 'a',7427 cell: (cellInfo, state) => {7428 return `page: ${state.page}, pageSize: ${state.pageSize}, pages: ${state.pages}`7429 }7430 },7431 { name: 'b', accessor: 'b' }7432 ],7433 pagination: false,7434 defaultPageSize: 2,7435 showPagination: true,7436 showPageSizeOptions: true,7437 pageSizeOptions: [2, 1]7438 }7439 const { container } = render(<Reactable {...props} />)7440 expect(getRows(container)).toHaveLength(3)7441 expect(getPagination(container)).toBeVisible()7442 expect(getPageInfo(container).textContent).toEqual('1–3 of 3 rows')7443 expect(getCellsText(container)[0]).toEqual('page: 0, pageSize: 2, pages: 1')7444 // Page size should be changeable, but still be ignored7445 const pageSizeSelect = getPageSizeSelect(container)7446 fireEvent.change(pageSizeSelect, { target: { value: 1 } })7447 expect(pageSizeSelect.value).toEqual('1')7448 expect(getRows(container)).toHaveLength(3)7449 expect(getPageInfo(container).textContent).toEqual('1–3 of 3 rows')7450 expect(getCellsText(container)[0]).toEqual('page: 0, pageSize: 1, pages: 1')7451 })7452 it('disabling pagination works when data changes', () => {7453 const props = {7454 data: { a: [1, 2, 3, 4] },7455 columns: [{ name: 'a', accessor: 'a' }],7456 pagination: false,7457 defaultPageSize: 27458 }7459 const { container, rerender } = render(<Reactable {...props} />)7460 expect(getRows(container)).toHaveLength(4)7461 rerender(<Reactable {...props} data={{ a: [1, 2, 3, 4, 5, 6, 7] }} />)7462 expect(getRows(container)).toHaveLength(7)7463 })7464 it('disabling pagination works with sub rows (paginateSubRows disabled)', () => {7465 const props = {7466 data: {7467 group: ['a', 'a', 'a', 'a'],7468 a: [111, 111, 222, 33]7469 },7470 columns: [7471 { name: 'group', accessor: 'group' },7472 { name: 'col-a', accessor: 'a' }7473 ],7474 pagination: false,7475 defaultPageSize: 4,7476 pivotBy: ['group'],7477 paginateSubRows: false,7478 showPagination: true,7479 defaultExpanded: true7480 }7481 const { container } = render(<Reactable {...props} />)7482 expect(getRows(container)).toHaveLength(5)7483 expect(getPageInfo(container).textContent).toEqual('1–1 of 1 rows')7484 })7485 it('disabling pagination works with sub rows (paginateSubRows enabled)', () => {7486 const props = {7487 data: {7488 group: ['a', 'a', 'a', 'a'],7489 a: [111, 111, 222, 33]7490 },7491 columns: [7492 { name: 'group', accessor: 'group' },7493 { name: 'col-a', accessor: 'a' }7494 ],7495 pagination: false,7496 defaultPageSize: 4,7497 pivotBy: ['group'],7498 paginateSubRows: true,7499 showPagination: true,7500 defaultExpanded: true7501 }7502 const { container } = render(<Reactable {...props} />)7503 expect(getRows(container)).toHaveLength(5)7504 // Should ignore default page size when pagination is disabled7505 expect(getPageInfo(container).textContent).toEqual('1–5 of 5 rows')7506 })7507 it('current page state resets when data changes (also on sorting, filtering, searching)', () => {7508 const props = {7509 data: { a: ['aa', 'aa', 'bb', 'cc', 'cc', 'cc', 'cc'] },7510 columns: [{ name: 'col-a', accessor: 'a' }],7511 defaultPageSize: 2,7512 filterable: true,7513 searchable: true7514 }7515 const { container, getByText, rerender } = render(<Reactable {...props} />)7516 const pageInfo = getPageInfo(container)7517 const nextButton = getNextButton(container)7518 expect(pageInfo.textContent).toEqual('1–2 of 7 rows')7519 fireEvent.click(nextButton)7520 expect(pageInfo.textContent).toEqual('3–4 of 7 rows')7521 // Data changes7522 rerender(<Reactable {...props} data={{ a: ['aa', 'aa', 'aa', 'bb'] }} />)7523 expect(pageInfo.textContent).toEqual('1–2 of 4 rows')7524 // Sorting changes7525 fireEvent.click(nextButton)7526 expect(pageInfo.textContent).toEqual('3–4 of 4 rows')7527 fireEvent.click(getByText('col-a'))7528 expect(pageInfo.textContent).toEqual('1–2 of 4 rows')7529 // Filter changes7530 const filter = getFilters(container)[0]7531 fireEvent.click(nextButton)7532 expect(pageInfo.textContent).toEqual('3–4 of 4 rows')7533 fireEvent.change(filter, { target: { value: 'aa' } })7534 expect(pageInfo.textContent).toEqual('1–2 of 3 rows')7535 fireEvent.change(filter, { target: { value: '' } })7536 // Search changes7537 const searchInput = getSearchInput(container)7538 fireEvent.click(nextButton)7539 expect(pageInfo.textContent).toEqual('3–4 of 4 rows')7540 fireEvent.change(searchInput, { target: { value: 'aa' } })7541 expect(pageInfo.textContent).toEqual('1–2 of 3 rows')7542 fireEvent.change(searchInput, { target: { value: '' } })7543 })7544})7545describe('themes', () => {7546 it('applies theme styles to the table', () => {7547 const props = {7548 data: { a: [1, 2], b: ['aa', 'bb'] },7549 columns: [7550 {7551 name: 'colA',7552 accessor: 'a',7553 footer: 'footer-a',7554 className: 'cell-a',7555 headerClassName: 'header-a',7556 footerClassName: 'footer-a',7557 details: () => 'details'7558 },7559 { name: 'colB', accessor: 'b', footer: 'footer-b' }7560 ],7561 columnGroups: [{ columns: ['a'], name: 'group-a' }],7562 minRows: 4,7563 filterable: true,7564 searchable: true,7565 className: 'my-root',7566 rowClassName: 'my-row',7567 theme: {7568 style: { color: 'red' },7569 tableStyle: { border: '1px solid black' },7570 tableBodyStyle: { content: '"tableBody"' },7571 rowGroupStyle: { content: '"rowGroup"' },7572 rowStyle: { content: '"row"' },7573 headerStyle: { content: '"header"' },7574 groupHeaderStyle: { content: '"groupHeader"' },7575 cellStyle: { content: '"cell"' },7576 footerStyle: { content: '"footer"' },7577 inputStyle: { content: '"input"' }7578 }7579 }7580 const { container } = render(<Reactable {...props} />)7581 const rootContainer = getRoot(container)7582 expect(rootContainer).toHaveStyleRule('color', 'red')7583 // Should work with custom classes7584 expect(container.querySelector('.my-root')).toBeVisible()7585 const table = getTable(container)7586 expect(table).toHaveStyleRule('border', '1px solid black')7587 const tbody = getTbody(container)7588 expect(tbody).toHaveStyleRule('content', '"tableBody"')7589 const rowGroups = getRowGroups(container)7590 rowGroups.forEach(rowGroup => expect(rowGroup).toHaveStyleRule('content', '"rowGroup"'))7591 const rows = getDataRows(container)7592 rows.forEach(row => expect(row).toHaveStyleRule('content', '"row"'))7593 // rowStyle should be applied to pad rows as well7594 const padRows = getPadRows(container)7595 padRows.forEach(row => expect(row).toHaveStyleRule('content', '"row"'))7596 const filterRow = getFilterRow(container)7597 expect(filterRow).toHaveStyleRule('content', '"row"')7598 const headerRows = getHeaderRows(container)7599 headerRows.forEach(row => expect(row).not.toHaveStyleRule('content', '"row"'))7600 const footerRow = getFooterRow(container)7601 expect(footerRow).not.toHaveStyleRule('content', '"row"')7602 // Should work with custom classes7603 expect(container.querySelectorAll('.my-row')).toHaveLength(4)7604 const headers = getColumnHeaders(container)7605 headers.forEach(header => expect(header).toHaveStyleRule('content', '"header"'))7606 const groupHeaders = getGroupHeaders(container)7607 groupHeaders.forEach(header => expect(header).toHaveStyleRule('content', '"groupHeader"'))7608 const ungroupedHeaders = getUngroupedHeaders(container)7609 ungroupedHeaders.forEach(header => expect(header).toHaveStyleRule('content', '"groupHeader"'))7610 const filterCells = getFilterCells(container)7611 filterCells.forEach(cell => expect(cell).toHaveStyleRule('content', '"cell"'))7612 // cellStyle should be applied to pad cells as well7613 const cells = getCells(container)7614 cells.forEach(cell => expect(cell).toHaveStyleRule('content', '"cell"'))7615 const footers = getFooters(container)7616 footers.forEach(footer => expect(footer).toHaveStyleRule('content', '"footer"'))7617 // Should work with custom classes7618 expect(container.querySelectorAll('.cell-a')).toHaveLength(2)7619 expect(container.querySelectorAll('.header-a')).toHaveLength(2) // Includes filter7620 expect(container.querySelectorAll('.footer-a')).toHaveLength(1)7621 const expanderIcons = getExpanderIcons(container)7622 expanderIcons.forEach(expander => {7623 expect(expander).toHaveStyleRule('border-top-color', 'red', { target: '::after' })7624 })7625 const filters = getFilters(container)7626 filters.forEach(input => expect(input).toHaveStyleRule('content', '"input"'))7627 const searchInput = getSearchInput(container)7628 expect(searchInput).toHaveStyleRule('content', '"input"')7629 })7630 it('applies theme styles to pagination', () => {7631 const props = {7632 data: { a: [1, 2], b: ['aa', 'bb'] },7633 columns: [7634 { name: 'colA', accessor: 'a' },7635 { name: 'colB', accessor: 'b' }7636 ],7637 defaultPageSize: 1,7638 showPageSizeOptions: true,7639 paginationType: 'jump',7640 theme: {7641 borderColor: 'red',7642 borderWidth: 999,7643 inputStyle: { content: '"input"' },7644 selectStyle: { content: '"select"' },7645 paginationStyle: { content: '"pagination"' },7646 pageButtonStyle: { content: '"pageButton"' },7647 pageButtonCurrentStyle: { color: 'pageButtonCurrent' }7648 }7649 }7650 const { container } = render(<Reactable {...props} />)7651 const pagination = getPagination(container)7652 expect(pagination).toHaveStyleRule('content', '"pagination"')7653 expect(pagination).toHaveStyleRule('border-top-color', 'red')7654 expect(pagination).toHaveStyleRule('border-top-width', '999px')7655 expect(pagination).toHaveStyleRule('content', '"select"', { target: '.rt-page-size-select' })7656 expect(pagination).toHaveStyleRule('content', '"input"', { target: '.rt-page-jump' })7657 expect(pagination).toHaveStyleRule('content', '"pageButton"', { target: '.rt-page-button' })7658 expect(pagination).toHaveStyleRule('color', 'pageButtonCurrent', {7659 target: '.rt-page-button-current'7660 })7661 })7662 it('applies cell padding styles correctly', () => {7663 const props = {7664 data: { a: [1, 2], b: ['aa', 'bb'] },7665 columns: [7666 { name: 'colA', accessor: 'a', footer: 'footer-a' },7667 { name: 'colB', accessor: 'b', footer: 'footer-b' }7668 ],7669 columnGroups: [{ columns: ['a'], name: 'group-a' }],7670 minRows: 4,7671 filterable: true,7672 theme: {7673 cellPadding: '99px'7674 }7675 }7676 const { container } = render(<Reactable {...props} />)7677 const assertHeader = el => {7678 const innerEl = el.querySelector('.rt-th-inner')7679 expect(innerEl).toBeVisible()7680 expect(el).not.toHaveStyleRule('padding', '99px')7681 expect(innerEl).toHaveStyleRule('padding', '99px')7682 }7683 const assertCell = el => {7684 const innerEl = el.querySelector('.rt-td-inner')7685 expect(innerEl).toBeVisible()7686 expect(el).not.toHaveStyleRule('padding', '99px')7687 expect(innerEl).toHaveStyleRule('padding', '99px')7688 }7689 const headers = getColumnHeaders(container)7690 headers.forEach(assertHeader)7691 const groupHeaders = getGroupHeaders(container)7692 groupHeaders.forEach(assertHeader)7693 const ungroupedHeaders = getUngroupedHeaders(container)7694 ungroupedHeaders.forEach(assertHeader)7695 const filterCells = getFilterCells(container)7696 filterCells.forEach(assertCell)7697 const cells = getCells(container) // Includes pad cells7698 cells.forEach(assertCell)7699 const footers = getFooters(container)7700 footers.forEach(assertCell)7701 })7702 it('theme styles are scoped to their tables', () => {7703 const props = {7704 data: { a: [] },7705 columns: [{ name: 'a', accessor: 'a' }]7706 }7707 const { container } = render(7708 <div>7709 <Reactable {...props} className="tbl-a" theme={{ style: { background: 'blue' } }} />7710 <Reactable7711 {...props}7712 className="tbl-b"7713 theme={{ style: { background: 'red', color: 'red' } }}7714 />7715 </div>7716 )7717 const tableA = container.querySelector('.tbl-a')7718 const tableB = container.querySelector('.tbl-b')7719 expect(tableA).toHaveStyleRule('background', 'blue')7720 expect(tableB).toHaveStyleRule('background', 'red')7721 expect(tableA).not.toHaveStyleRule('color', 'red')7722 expect(tableB).toHaveStyleRule('color', 'red')7723 })7724})7725describe('updateReactable updates table state from Shiny', () => {7726 beforeEach(() => {7727 window.Shiny = {7728 onInputChange: jest.fn(),7729 addCustomMessageHandler: jest.fn(),7730 bindAll: jest.fn(),7731 unbindAll: jest.fn()7732 }7733 })7734 afterEach(() => {7735 delete window.Shiny7736 })7737 it('updates selected rows', () => {7738 const props = {7739 data: { a: [1, 2] },7740 columns: [{ name: 'a', accessor: 'a' }],7741 selection: 'multiple',7742 selectionId: 'selected'7743 }7744 const { getAllByLabelText, getByLabelText } = render(7745 <div data-reactable-output="shiny-output-container">7746 <Reactable {...props} />7747 </div>7748 )7749 const [outputId, updateState] = window.Shiny.addCustomMessageHandler.mock.calls[0]7750 expect(outputId).toEqual('__reactable__shiny-output-container')7751 act(() => updateState({ selected: [1, 0] }))7752 expect(window.Shiny.onInputChange).toHaveBeenCalledWith('selected', [1, 2])7753 expect(window.Shiny.onInputChange).toHaveBeenCalledWith(7754 'shiny-output-container__reactable__selected',7755 [1, 2]7756 )7757 let selectAllCheckbox = getByLabelText('Select all rows')7758 let selectRowCheckboxes = getAllByLabelText('Select row')7759 let selectRow1Checkbox = selectRowCheckboxes[0]7760 let selectRow2Checkbox = selectRowCheckboxes[1]7761 expect(selectAllCheckbox.checked).toEqual(true)7762 expect(selectRow1Checkbox.checked).toEqual(true)7763 expect(selectRow2Checkbox.checked).toEqual(true)7764 window.Shiny.onInputChange.mockReset()7765 act(() => updateState({ selected: [] }))7766 expect(window.Shiny.onInputChange).toHaveBeenCalledWith('selected', [])7767 expect(window.Shiny.onInputChange).toHaveBeenCalledWith(7768 'shiny-output-container__reactable__selected',7769 []7770 )7771 expect(selectAllCheckbox.checked).toEqual(false)7772 expect(selectRow1Checkbox.checked).toEqual(false)7773 expect(selectRow2Checkbox.checked).toEqual(false)7774 })7775 it('handles invalid selected rows', () => {7776 const props = {7777 data: { a: [1, 2] },7778 columns: [{ name: 'a', accessor: 'a' }],7779 selection: 'multiple'7780 }7781 const { container } = render(7782 <div data-reactable-output="shiny-output-container">7783 <Reactable {...props} />7784 </div>7785 )7786 const [outputId, updateState] = window.Shiny.addCustomMessageHandler.mock.calls[0]7787 expect(outputId).toEqual('__reactable__shiny-output-container')7788 act(() => updateState({ selected: [4] }))7789 expect(window.Shiny.onInputChange).toHaveBeenCalledWith(7790 'shiny-output-container__reactable__selected',7791 []7792 )7793 const selectRowCheckboxes = getSelectRowCheckboxes(container)7794 selectRowCheckboxes.forEach(checkbox => expect(checkbox.checked).toEqual(false))7795 })7796 it('updates expanded rows', () => {7797 const props = {7798 data: { a: [1, 2] },7799 columns: [{ name: 'a', accessor: 'a', details: ['detail-1', 'detail-2'] }]7800 }7801 const { getByText, queryByText } = render(7802 <div data-reactable-output="shiny-output-container">7803 <Reactable {...props} />7804 </div>7805 )7806 const [outputId, updateState] = window.Shiny.addCustomMessageHandler.mock.calls[0]7807 expect(outputId).toEqual('__reactable__shiny-output-container')7808 act(() => updateState({ expanded: true }))7809 expect(getByText('detail-1')).toBeVisible()7810 expect(getByText('detail-2')).toBeVisible()7811 act(() => updateState({ expanded: false }))7812 expect(queryByText('detail-1')).toEqual(null)7813 expect(queryByText('detail-2')).toEqual(null)7814 })7815 it('updates current page', () => {7816 const props = {7817 data: { a: [1, 2, 3] },7818 columns: [{ name: 'a', accessor: 'a' }],7819 defaultPageSize: 17820 }7821 const { getByText } = render(7822 <div data-reactable-output="shiny-output-container">7823 <Reactable {...props} />7824 </div>7825 )7826 const [outputId, updateState] = window.Shiny.addCustomMessageHandler.mock.calls[0]7827 expect(outputId).toEqual('__reactable__shiny-output-container')7828 expect(getByText('1–1 of 3 rows')).toBeVisible()7829 act(() => updateState({ page: 2 }))7830 expect(getByText('3–3 of 3 rows')).toBeVisible()7831 expect(window.Shiny.onInputChange).toHaveBeenCalledWith(7832 'shiny-output-container__reactable__page',7833 37834 )7835 act(() => updateState({ page: 0 }))7836 expect(getByText('1–1 of 3 rows')).toBeVisible()7837 // Should round out-of-bounds page indexes to nearest valid page7838 act(() => updateState({ page: 999 }))7839 expect(getByText('3–3 of 3 rows')).toBeVisible()7840 act(() => updateState({ page: -5 }))7841 expect(getByText('1–1 of 3 rows')).toBeVisible()7842 })7843 it('updates data', () => {7844 const props = {7845 data: { a: ['c1', 'c2', 'c3', 'c4'] },7846 columns: [{ name: 'a', accessor: 'a' }],7847 defaultPageSize: 37848 }7849 const { getByText, queryByText, rerender } = render(7850 <div data-reactable-output="shiny-output-container">7851 <Reactable {...props} />7852 </div>7853 )7854 const [outputId, updateState] = window.Shiny.addCustomMessageHandler.mock.calls[0]7855 expect(outputId).toEqual('__reactable__shiny-output-container')7856 expect(getByText('c1')).toBeVisible()7857 act(() => updateState({ data: { a: ['newc1', 'newc2', 'newc3'] } }))7858 expect(getByText('newc1')).toBeVisible()7859 expect(getByText('newc2')).toBeVisible()7860 expect(getByText('newc3')).toBeVisible()7861 // After updating data, rerendering with new data should work7862 rerender(<Reactable {...props} data={{ a: ['b1', 'b2', 'b'] }} />)7863 expect(getByText('b1')).toBeVisible()7864 expect(queryByText('newc1')).toBeFalsy()7865 })7866 it('updates data, selected, expanded, and current page state', () => {7867 const props = {7868 data: { a: ['a1', 'a2', 'a3'] },7869 columns: [{ name: 'a', accessor: 'a', details: ['detail-1', 'detail-2', 'detail-3'] }],7870 defaultPageSize: 1,7871 selection: 'multiple'7872 }7873 const { getByLabelText, getByText } = render(7874 <div data-reactable-output="shiny-output-container">7875 <Reactable {...props} />7876 </div>7877 )7878 const [outputId, updateState] = window.Shiny.addCustomMessageHandler.mock.calls[0]7879 expect(outputId).toEqual('__reactable__shiny-output-container')7880 // Known issue: when updating data and current page at the same time in act(), the7881 // current page does not update correctly. Suppress the console error about act()7882 // for now.7883 const originalError = console.error7884 console.error = jest.fn()7885 updateState({ data: { a: ['c1', 'c2', 'c3'] }, selected: [2], expanded: true, page: 2 })7886 console.error = originalError7887 expect(getByText('c3')).toBeVisible()7888 expect(getByLabelText('Select row')).toBeChecked()7889 expect(getByText('detail-3')).toBeVisible()7890 expect(getByText('3–3 of 3 rows')).toBeVisible()7891 })7892 it('does not enable updateState for tables that are not Shiny outputs', () => {7893 const props = {7894 data: { a: [1, 2] },7895 columns: [{ name: 'a', accessor: 'a' }]7896 }7897 // Static rendered tables in Shiny have no parent element with a data-reactable-output ID7898 render(7899 <div>7900 <Reactable {...props} />7901 </div>7902 )7903 expect(window.Shiny.addCustomMessageHandler).not.toHaveBeenCalled()7904 })7905 it('does not enable updateState for nested tables, which are not Shiny bound', () => {7906 const props = {7907 data: { a: [1, 2] },7908 columns: [{ name: 'a', accessor: 'a' }],7909 nested: true7910 }7911 render(7912 <div data-reactable-output="not-a-shiny-output-container">7913 <Reactable {...props} />7914 </div>7915 )7916 expect(window.Shiny.addCustomMessageHandler).not.toHaveBeenCalled()7917 })7918 it('does not enable updateState when Shiny is not initialized', () => {7919 window.Shiny = undefined7920 const props = {7921 data: { a: [1, 2] },7922 columns: [{ name: 'a', accessor: 'a' }]7923 }7924 render(7925 <div data-reactable-output="not-a-shiny-output-container">7926 <Reactable {...props} />7927 </div>7928 )7929 // Should not call Shiny.addCustomMessageHandler7930 })7931})7932describe('getReactableState gets table state from Shiny', () => {7933 beforeEach(() => {7934 window.Shiny = {7935 onInputChange: jest.fn(),7936 addCustomMessageHandler: jest.fn(),7937 bindAll: jest.fn(),7938 unbindAll: jest.fn()7939 }7940 })7941 afterEach(() => {7942 delete window.Shiny7943 })7944 it('calls Shiny.onInputChange when table state changes', () => {7945 const props = {7946 data: { a: [1, 2, 3, 4] },7947 columns: [{ name: 'a', accessor: 'a' }],7948 selection: 'multiple',7949 defaultPageSize: 2,7950 showPageSizeOptions: true,7951 pageSizeOptions: [2, 4]7952 }7953 const { container, getAllByLabelText } = render(7954 <div data-reactable-output="tbl">7955 <Reactable {...props} />7956 </div>7957 )7958 // Initial state7959 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(1, 'tbl__reactable__page', 1)7960 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(2, 'tbl__reactable__pageSize', 2)7961 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(3, 'tbl__reactable__pages', 2)7962 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(4, 'tbl__reactable__selected', [])7963 window.Shiny.onInputChange.mockReset()7964 // Selected rows7965 const selectRow2Checkbox = getAllByLabelText('Select row')[1]7966 fireEvent.click(selectRow2Checkbox)7967 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(1, 'tbl__reactable__page', 1)7968 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(2, 'tbl__reactable__pageSize', 2)7969 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(3, 'tbl__reactable__pages', 2)7970 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(4, 'tbl__reactable__selected', [2])7971 window.Shiny.onInputChange.mockReset()7972 // Current page7973 fireEvent.click(getNextButton(container))7974 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(1, 'tbl__reactable__page', 2)7975 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(2, 'tbl__reactable__pageSize', 2)7976 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(3, 'tbl__reactable__pages', 2)7977 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(4, 'tbl__reactable__selected', [2])7978 window.Shiny.onInputChange.mockReset()7979 // Pages, page size7980 const pageSizeSelect = getPageSizeSelect(container)7981 fireEvent.change(pageSizeSelect, { target: { value: 4 } })7982 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(1, 'tbl__reactable__page', 1)7983 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(2, 'tbl__reactable__pageSize', 4)7984 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(3, 'tbl__reactable__pages', 1)7985 expect(window.Shiny.onInputChange).toHaveBeenNthCalledWith(4, 'tbl__reactable__selected', [2])7986 window.Shiny.onInputChange.mockReset()7987 })7988 it('does not send state when table is not a Shiny output', () => {7989 const props = {7990 data: { a: [1, 2] },7991 columns: [{ name: 'a', accessor: 'a' }]7992 }7993 // Static rendered tables in Shiny have no parent element with a data-reactable-output ID7994 render(7995 <div>7996 <Reactable {...props} />7997 </div>7998 )7999 expect(window.Shiny.onInputChange).not.toHaveBeenCalled()8000 })8001 it('does not send state for nested tables, which are not Shiny bound', () => {8002 const props = {8003 data: { a: [1, 2] },8004 columns: [{ name: 'a', accessor: 'a' }],8005 nested: true8006 }8007 render(8008 <div data-reactable-output="not-a-shiny-output-container">8009 <Reactable {...props} />8010 </div>8011 )8012 expect(window.Shiny.onInputChange).not.toHaveBeenCalled()8013 })8014 it('does not send state when Shiny is not fully initialized', () => {8015 // When static widgets are rendered in Shiny apps, Shiny may be defined8016 // but not fully initialized.8017 window.Shiny.onInputChange = undefined8018 const props = {8019 data: { a: [1, 2] },8020 columns: [{ name: 'a', accessor: 'a' }]8021 }8022 render(8023 <div data-reactable-output="not-a-shiny-output-container">8024 <Reactable {...props} />8025 </div>8026 )8027 // Should not call Shiny.onInputChange8028 window.Shiny = undefined8029 render(8030 <div data-reactable-output="not-a-shiny-output-container">8031 <Reactable {...props} />8032 </div>8033 )8034 })8035})8036describe('Crosstalk', () => {8037 let mockSelection, mockFilter8038 beforeEach(() => {8039 mockSelection = { on: jest.fn(), close: jest.fn(), set: jest.fn() }8040 mockFilter = { on: jest.fn(), close: jest.fn() }8041 window.crosstalk = {8042 SelectionHandle: jest.fn().mockReturnValueOnce(mockSelection),8043 FilterHandle: jest.fn().mockReturnValueOnce(mockFilter)8044 }8045 })8046 afterEach(() => {8047 mockSelection = null8048 mockFilter = null8049 delete window.crosstalk8050 })8051 it('handles selection changes', () => {8052 const props = {8053 data: { a: [111, 222, 333], b: ['aaa', 'bbb', 'ccc'] },8054 columns: [8055 { name: 'a', accessor: 'a' },8056 { name: 'b', accessor: 'b' }8057 ],8058 crosstalkKey: ['key1', 'key2', 'key3'],8059 crosstalkGroup: 'group'8060 }8061 const { container, getByText, unmount } = render(<Reactable {...props} />)8062 expect(window.crosstalk.SelectionHandle).toHaveBeenCalledTimes(1)8063 expect(window.crosstalk.SelectionHandle).toHaveBeenCalledWith('group')8064 expect(mockSelection.on).toHaveBeenCalledTimes(1)8065 const [selectionType, onSelection] = mockSelection.on.mock.calls[0]8066 expect(selectionType).toEqual('change')8067 // Select one value8068 act(() => onSelection({ sender: 'some other widget', value: ['key2'] }))8069 expect(getRows(container)).toHaveLength(1)8070 expect(getByText('bbb')).toBeVisible()8071 // Clear selection8072 act(() => onSelection({ sender: 'some other widget', value: [] }))8073 expect(getRows(container)).toHaveLength(3)8074 // Select multiple values8075 act(() => onSelection({ sender: 'some other widget', value: ['key3', 'key1', 'key2'] }))8076 expect(getRows(container)).toHaveLength(3)8077 // Clear selection8078 act(() => onSelection({ sender: 'some other widget', value: null }))8079 expect(getRows(container)).toHaveLength(3)8080 // Should ignore selections from same sender8081 act(() => onSelection({ sender: mockSelection, value: ['key2'] }))8082 expect(getRows(container)).toHaveLength(3)8083 // Should cleanup8084 unmount()8085 expect(mockSelection.close).toHaveBeenCalledTimes(1)8086 })8087 it('handles initial selection value', () => {8088 const props = {8089 data: { a: [111, 222, 333], b: ['aaa', 'bbb', 'ccc'] },8090 columns: [8091 { name: 'a', accessor: 'a' },8092 { name: 'b', accessor: 'b' }8093 ],8094 crosstalkKey: ['key1', 'key2', 'key3'],8095 crosstalkGroup: 'group'8096 }8097 mockSelection.value = ['key2']8098 const { container, getByText } = render(<Reactable {...props} />)8099 expect(getRows(container)).toHaveLength(1)8100 expect(getByText('bbb')).toBeVisible()8101 const onSelection = mockSelection.on.mock.calls[0][1]8102 act(() => onSelection({ sender: 'some other widget', value: [] }))8103 expect(getRows(container)).toHaveLength(3)8104 })8105 it('handles filter changes', () => {8106 const props = {8107 data: { a: [111, 222, 333], b: ['aaa', 'bbb', 'ccc'] },8108 columns: [8109 { name: 'a', accessor: 'a' },8110 { name: 'b', accessor: 'b' }8111 ],8112 crosstalkKey: ['key1', 'key2', 'key3'],8113 crosstalkGroup: 'group'8114 }8115 const { container, getByText, unmount } = render(<Reactable {...props} />)8116 expect(window.crosstalk.FilterHandle).toHaveBeenCalledTimes(1)8117 expect(window.crosstalk.FilterHandle).toHaveBeenCalledWith('group')8118 expect(mockFilter.on).toHaveBeenCalledTimes(1)8119 const [filterType, onFilter] = mockFilter.on.mock.calls[0]8120 expect(filterType).toEqual('change')8121 // Filter one value8122 act(() => onFilter({ sender: 'some other widget', value: ['key2'] }))8123 expect(getRows(container)).toHaveLength(1)8124 expect(getByText('bbb')).toBeVisible()8125 // Filter multiple values8126 act(() => onFilter({ sender: 'some other widget', value: ['key3', 'key1'] }))8127 expect(getRows(container)).toHaveLength(2)8128 expect(getByText('ccc')).toBeVisible()8129 expect(getByText('aaa')).toBeVisible()8130 // Clear filter8131 act(() => onFilter({ sender: 'some other widget', value: null }))8132 expect(getRows(container)).toHaveLength(3)8133 // Should ignore selections from same sender8134 act(() => onFilter({ sender: mockFilter, value: ['key2'] }))8135 expect(getRows(container)).toHaveLength(3)8136 // Should cleanup8137 unmount()8138 expect(mockFilter.close).toHaveBeenCalledTimes(1)8139 })8140 it('handles initial filter value', () => {8141 const props = {8142 data: { a: [111, 222, 333], b: ['aaa', 'bbb', 'ccc'] },8143 columns: [8144 { name: 'a', accessor: 'a' },8145 { name: 'b', accessor: 'b' }8146 ],8147 crosstalkKey: ['key1', 'key2', 'key3'],8148 crosstalkGroup: 'group'8149 }8150 mockFilter.filteredKeys = ['key2']8151 const { container, getByText } = render(<Reactable {...props} />)8152 expect(getRows(container)).toHaveLength(1)8153 expect(getByText('bbb')).toBeVisible()8154 const onFilter = mockFilter.on.mock.calls[0][1]8155 act(() => onFilter({ sender: 'some other widget', value: null }))8156 expect(getRows(container)).toHaveLength(3)8157 })8158 it('handles both selection and filter changes', () => {8159 const props = {8160 data: { a: [111, 222, 333], b: ['aaa', 'bbb', 'ccc'] },8161 columns: [8162 { name: 'a', accessor: 'a' },8163 { name: 'b', accessor: 'b' }8164 ],8165 crosstalkKey: ['key1', 'key2', 'key3'],8166 crosstalkGroup: 'group'8167 }8168 const { container, getByText } = render(<Reactable {...props} />)8169 const onSelection = mockSelection.on.mock.calls[0][1]8170 const onFilter = mockFilter.on.mock.calls[0][1]8171 // Filter with existing selection8172 act(() => onSelection({ sender: 'some other widget', value: ['key2'] }))8173 act(() => onFilter({ sender: 'some other widget', value: ['key2', 'key3'] }))8174 expect(getRows(container)).toHaveLength(1)8175 expect(getByText('bbb')).toBeVisible()8176 // Selection with existing filter8177 act(() => onFilter({ sender: 'some other widget', value: ['key1', 'key3'] }))8178 act(() => onSelection({ sender: 'some other widget', value: ['key3'] }))8179 expect(getRows(container)).toHaveLength(1)8180 expect(getByText('ccc')).toBeVisible()8181 // Clear selection and filter8182 act(() => onSelection({ sender: 'some other widget', value: [] }))8183 expect(getRows(container)).toHaveLength(2)8184 act(() => onFilter({ sender: 'some other widget', value: null }))8185 expect(getRows(container)).toHaveLength(3)8186 })8187 it('sends selection changes', () => {8188 const props = {8189 data: { a: [111, 222, 333], b: ['aaa', 'bbb', 'ccc'] },8190 columns: [8191 { name: 'a', accessor: 'a' },8192 { name: 'b', accessor: 'b' }8193 ],8194 selection: 'multiple',8195 crosstalkKey: ['key1', 'key2', 'key3'],8196 crosstalkGroup: 'group'8197 }8198 const { container } = render(<Reactable {...props} />)8199 const selectRowCheckboxes = getSelectRowCheckboxes(container)8200 const selectAllCheckbox = selectRowCheckboxes[0]8201 const selectRow1Checkbox = selectRowCheckboxes[1]8202 const selectRow2Checkbox = selectRowCheckboxes[2]8203 // Should not set initial selection if there are no default selected rows8204 expect(mockSelection.set).toHaveBeenCalledTimes(0)8205 fireEvent.click(selectRow2Checkbox)8206 expect(mockSelection.set).toHaveBeenLastCalledWith(['key2'])8207 fireEvent.click(selectRow1Checkbox)8208 expect(mockSelection.set).toHaveBeenLastCalledWith(['key1', 'key2'])8209 fireEvent.click(selectAllCheckbox)8210 expect(mockSelection.set).toHaveBeenLastCalledWith(['key1', 'key2', 'key3'])8211 fireEvent.click(selectAllCheckbox)8212 expect(mockSelection.set).toHaveBeenLastCalledWith([])8213 expect(mockSelection.set).toHaveBeenCalledTimes(4)8214 })8215 it('sends selection changes for defaultSelected rows', () => {8216 const props = {8217 data: { a: [111, 222, 333], b: ['aaa', 'bbb', 'ccc'] },8218 columns: [8219 { name: 'a', accessor: 'a' },8220 { name: 'b', accessor: 'b' }8221 ],8222 selection: 'multiple',8223 defaultSelected: [2, 0],8224 crosstalkKey: ['key1', 'key2', 'key3'],8225 crosstalkGroup: 'group'8226 }8227 const { rerender } = render(<Reactable {...props} />)8228 expect(mockSelection.set).toHaveBeenLastCalledWith(['key1', 'key3'])8229 expect(mockSelection.set).toHaveBeenCalledTimes(1)8230 rerender(<Reactable {...props} defaultSelected={[1]} />)8231 expect(mockSelection.set).toHaveBeenLastCalledWith(['key2'])8232 expect(mockSelection.set).toHaveBeenCalledTimes(3)8233 })8234 it('clears selection filter on selection from table', () => {8235 const props = {8236 data: { a: [111, 222, 333], b: ['aaa', 'bbb', 'ccc'] },8237 columns: [8238 { name: 'a', accessor: 'a' },8239 { name: 'b', accessor: 'b' }8240 ],8241 selection: 'multiple',8242 crosstalkKey: ['key1', 'key2', 'key3'],8243 crosstalkGroup: 'group'8244 }8245 const { container, getByText } = render(<Reactable {...props} />)8246 const onSelection = mockSelection.on.mock.calls[0][1]8247 const onFilter = mockFilter.on.mock.calls[0][1]8248 act(() => onFilter({ sender: 'some other widget', value: ['key2', 'key3'] }))8249 act(() => onSelection({ sender: 'some other widget', value: ['key2'] }))8250 expect(getRows(container)).toHaveLength(1)8251 expect(getByText('bbb')).toBeVisible()8252 const selectRowCheckboxes = getSelectRowCheckboxes(container)8253 const selectRow2Checkbox = selectRowCheckboxes[1]8254 fireEvent.click(selectRow2Checkbox)8255 act(() => onSelection({ sender: mockSelection, value: ['key2'] }))8256 expect(mockSelection.set).toHaveBeenLastCalledWith(['key2'])8257 expect(getRows(container)).toHaveLength(2)8258 expect(getByText('bbb')).toBeVisible()8259 expect(getByText('ccc')).toBeVisible()8260 })8261 it('clears selected state on selection changes from other widgets', () => {8262 const props = {8263 data: { a: [111, 222, 333], b: ['aaa', 'bbb', 'ccc'] },8264 columns: [8265 { name: 'a', accessor: 'a' },8266 { name: 'b', accessor: 'b' }8267 ],8268 selection: 'multiple',8269 searchable: true,8270 crosstalkKey: ['key1', 'key2', 'key3'],8271 crosstalkGroup: 'group'8272 }8273 const { container } = render(<Reactable {...props} />)8274 const onSelection = mockSelection.on.mock.calls[0][1]8275 const selectRowCheckboxes = getSelectRowCheckboxes(container)8276 const selectRow1Checkbox = selectRowCheckboxes[1]8277 const selectRow2Checkbox = selectRowCheckboxes[2]8278 fireEvent.click(selectRow1Checkbox)8279 fireEvent.click(selectRow2Checkbox)8280 expect(selectRow1Checkbox.checked).toEqual(true)8281 expect(selectRow2Checkbox.checked).toEqual(true)8282 act(() => onSelection({ sender: 'some other widget', value: null }))8283 expect(selectRow1Checkbox.checked).toEqual(false)8284 expect(selectRow2Checkbox.checked).toEqual(false)8285 // Should clear selection on rows that are not visible and have been filtered out8286 fireEvent.click(selectRow1Checkbox)8287 expect(selectRow1Checkbox.checked).toEqual(true)8288 fireEvent.change(getSearchInput(container), { target: { value: 'bbb' } })8289 expect(getRows(container)).toHaveLength(1)8290 getSelectRowCheckboxes(container).forEach(checkbox => expect(checkbox.checked).toEqual(false))8291 act(() => onSelection({ sender: 'some widget far, far away', value: ['key1', 'key2'] }))8292 fireEvent.change(getSearchInput(container), { target: { value: '' } })8293 expect(getRows(container)).toHaveLength(2)8294 getSelectRowCheckboxes(container).forEach(checkbox => expect(checkbox.checked).toEqual(false))8295 })8296 it('handles missing keys', () => {8297 // crosstalkKey can be null when there are no rows in the table8298 const props = {8299 data: { a: [], b: [] },8300 columns: [8301 { name: 'a', accessor: 'a' },8302 { name: 'b', accessor: 'b' }8303 ],8304 crosstalkKey: null,8305 crosstalkGroup: 'group'8306 }8307 render(<Reactable {...props} />)8308 expect(window.crosstalk.SelectionHandle).toHaveBeenCalledTimes(1)8309 expect(window.crosstalk.SelectionHandle).toHaveBeenCalledWith('group')8310 expect(mockSelection.on).toHaveBeenCalledTimes(1)8311 })8312 it('does not create filter/selection handles when Crosstalk is not used', () => {8313 const props = {8314 data: { a: [1, 2] },8315 columns: [{ name: 'a', accessor: 'a' }]8316 }8317 render(<Reactable {...props} />)8318 expect(window.crosstalk.FilterHandle).not.toHaveBeenCalled()8319 expect(window.crosstalk.SelectionHandle).not.toHaveBeenCalled()8320 window.crosstalk = undefined8321 render(<Reactable {...props} crosstalkGroup="group" />)8322 })8323 it('Crosstalk filtering works with column groups', () => {8324 const props = {8325 data: { a: [111, 222, 333], b: ['aaa', 'bbb', 'ccc'] },8326 columns: [8327 { name: 'a', accessor: 'a' },8328 { name: 'b', accessor: 'b' }8329 ],8330 columnGroups: [{ name: 'group-a', columns: ['a', 'b'] }],8331 crosstalkKey: ['key1', 'key2', 'key3'],8332 crosstalkGroup: 'group'8333 }8334 const { container, getByText } = render(<Reactable {...props} />)8335 expect(window.crosstalk.FilterHandle).toHaveBeenCalledTimes(1)8336 expect(window.crosstalk.FilterHandle).toHaveBeenCalledWith('group')8337 expect(mockFilter.on).toHaveBeenCalledTimes(1)8338 const [filterType, onFilter] = mockFilter.on.mock.calls[0]8339 expect(filterType).toEqual('change')8340 // Filter one value8341 act(() => onFilter({ sender: 'some other widget', value: ['key2'] }))8342 expect(getRows(container)).toHaveLength(1)8343 expect(getByText('bbb')).toBeVisible()8344 })8345 it('Crosstalk filtering works with grouping and sub-rows', () => {8346 const props = {8347 data: {8348 group: ['group-x', 'group-x', 'group-x', 'group-y'],8349 a: [1, 1, 2, 41],8350 b: ['aaa', 'bbb', 'aaa', 'bbb']8351 },8352 columns: [8353 { name: 'group', accessor: 'group' },8354 { name: 'col-a', accessor: 'a', type: 'numeric', aggregate: 'sum', className: 'col-a' },8355 { name: 'col-b', accessor: 'b', aggregate: () => 'ccc' }8356 ],8357 pivotBy: ['group'],8358 crosstalkKey: ['key1', 'key2', 'key3', 'key4'],8359 crosstalkGroup: 'group'8360 }8361 const { container } = render(<Reactable {...props} />)8362 expect(window.crosstalk.FilterHandle).toHaveBeenCalledTimes(1)8363 expect(window.crosstalk.FilterHandle).toHaveBeenCalledWith('group')8364 expect(mockFilter.on).toHaveBeenCalledTimes(1)8365 const [filterType, onFilter] = mockFilter.on.mock.calls[0]8366 expect(filterType).toEqual('change')8367 act(() => onFilter({ sender: 'some other widget', value: ['key1', 'key2'] }))8368 expect(getRows(container)).toHaveLength(1)8369 expect(getCellsText(container)).toEqual([8370 '\u200bgroup-x (2)',8371 '2', // Aggregate functions should work on filtered data8372 'ccc'8373 ])8374 })8375 it('does not show hidden Crosstalk column used for filtering', () => {8376 const props = {8377 data: { a: [111, 222, 333], b: ['aaa', 'bbb', 'ccc'], c: [5, 6, 7] },8378 columns: [8379 { name: 'col-a', accessor: 'a' },8380 { name: 'col-b', accessor: 'b' },8381 { name: 'col-c', accessor: 'c', show: false }8382 ],8383 columnGroups: [{ name: 'group-a', columns: ['a', 'b'] }],...

Full Screen

Full Screen

jqgrid-util.js

Source:jqgrid-util.js Github

copy

Full Screen

...310 for (var k = 0; k < cellNameArray.length; k++) {311 var rowSpanTaxCount = 1;312 for (var i = 0; i < length; i += rowSpanTaxCount) {313 // 从当前行开始比对下面的信息314 var before = $this.getCellsText(ids[i], cellNameArray[k]);315316 rowSpanTaxCount = 1;317 for (j = i + 1; j < length; j++) {318 // 和上边的信息对比,如果值一样就合并行数+1,然后设置rowspan让当前单元格隐藏319 var end = $this.getCellsText(ids[j], cellNameArray[k]);320321 if (before == end && before != "") {322 rowSpanTaxCount++;323 $("#" + $this.gridId).jqGrid("setCell", ids[j],324 cellNameArray[k], '', {325 display : 'none'326 });327 } else {328 break;329 }330 }331 $("#" + cellNameIdArray[k] + ids[i]).attr("rowspan",332 rowSpanTaxCount);333 } ...

Full Screen

Full Screen

partnerConfig.js

Source:partnerConfig.js Github

copy

Full Screen

...66function deleteConfig() {67 var rowId = $("#gridTable").jqGrid("getGridParam", "selrow");6869 if (rowId != "") {70 var partner_name = gridCommon.getCellsText(rowId, "partner_name");71 messageBox.setPropertyCaption("警告");72 messageBox.setPropertyContent("确定要删除" + partner_name + "的信息吗?");7374 messageBox.show();75 messageBox.setOnClickListener(function() {76 $.post("manage/deleteOperatorConfig.json", {77 id : rowId78 }, function(data) {79 if (data.status == "0") {80 gridCommon.reload();81 } else {82 alert(data.error)83 }84 }); ...

Full Screen

Full Screen

table-to-csv.js

Source:table-to-csv.js Github

copy

Full Screen

1const $tableRows = document.querySelectorAll(".anime-table__row");2const $convertBtn = document.querySelector('[data-js="convert-btn"]');3const getCellsText = ({ textContent }) => textContent;4const getStringSeparatedWithComma = ({ cells }) => Array.from(cells)5 .map(getCellsText)6 .join(",");7const getCSVString = () => Array.from($tableRows)8 .map(getStringSeparatedWithComma)9 .join("\n");10const setCSVDownload = csvString => {11 const CSVURI = 12 `data:text/csvcharset=utf-8,${encodeURIComponent(csvString)}`;13 14 $convertBtn.setAttribute('href', CSVURI);15 $convertBtn.setAttribute("download", "table.csv");16}17const exportTable = () => {18 const csvString = getCSVString();19 setCSVDownload(csvString);20}...

Full Screen

Full Screen

app.js

Source:app.js Github

copy

Full Screen

1const tableRows = document.querySelectorAll('tr')2const exportBtn = document.querySelector('[data-js="export-table-btn"]')3const getCellsText = ({ textContent }) => textContent4const getStringWithCommas = ({ cells }) => Array.from(cells)5 .map(getCellsText)6 .join(',')7const createCSVString = () => Array.from(tableRows)8 .map(getStringWithCommas)9 .join('\n')10const setCSVDownload = CSVString => {11 const CSVURI = `data:text/csvcharset=utf-8,${encodeURIComponent(CSVString)}`12 exportBtn.setAttribute('href', CSVURI)13 exportBtn.setAttribute('download', 'table.csv')14}15const exportTable = () => {16 const CSVString = createCSVString()17 setCSVDownload(CSVString)18}...

Full Screen

Full Screen

Using AI Code Generation

copy

Full Screen

1var gherkin = require('gherkin');2var fs = require('fs');3var parser = new gherkin.Parser();4var lexer = new gherkin.Lexer('en');5var featureSource = fs.readFileSync('features/test.feature', 'utf-8');6var feature = parser.parse(lexer.lex(featureSource));7var feature = parser.parse(lexer.lex(featureSource));8var scenario = feature.children[0];9var step = scenario.steps[0];10var cellsText = step.getCellsText();11console.log(cellsText);

Full Screen

Using AI Code Generation

copy

Full Screen

1var gherkin = require('gherkin');2var fs = require('fs');3var data = fs.readFileSync('test.feature', 'utf-8');4var parser = new gherkin.Parser();5var feature = parser.parse(data);6var table = feature.feature.children[0].steps[0].argument.rows;7var tableText = table.map(function(row) {8 return row.cells.map(function(cell) {9 return cell.value;10 });11});12console.log(tableText);13Your name to display (optional):14Your name to display (optional):

Full Screen

Using AI Code Generation

copy

Full Screen

1var Gherkin = require('cucumber-gherkin');2var fs = require('fs');3var gherkin = new Gherkin('path/to/feature/file.feature');4var cellsText = gherkin.getCellsText();5fs.writeFile('output.txt', cellsText, function (err) {6 if (err) return console.log(err);7 console.log('output.txt saved');8});9var Gherkin = require('cucumber-gherkin');10var gherkin = new Gherkin('path/to/feature/file.feature');11var feature = gherkin.getFeature();12var scenarios = gherkin.getScenarios();13var scenario = gherkin.getScenario('My Scenario');14var steps = gherkin.getSteps('My Scenario');15var step = gherkin.getStep('My Scenario', 'Given I have a step');16var cellsText = gherkin.getCellsText('My Scenario', 'Given I have a step');17var cellText = gherkin.getCellText('My Scenario', 'Given I have a step', 1, 1);18var tags = gherkin.getTags('My Scenario');19var tags = gherkin.getTags('My Scenario', 'Given I have a step');20var featureJSON = gherkin.getFeatureJSON();

Full Screen

Using AI Code Generation

copy

Full Screen

1var gherkin = require('gherkin');2var fs = require('fs');3var gherkinFile = fs.readFileSync('C:\\Users\\abc\\Desktop\\test.feature', 'utf-8');4var parser = new gherkin.Parser();5var feature = parser.parse(gherkinFile);6var table = feature.feature.children[0].steps[0].argument.rows;7var text = table.getCellsText();8console.log(text);

Full Screen

Using AI Code Generation

copy

Full Screen

1var gherkin = require('cucumber-gherkin');2var fs = require('fs');3var feature = fs.readFileSync('test.feature').toString();4var parser = new gherkin.Parser();5var ast = parser.parse(feature);6var cells = ast.feature.children[0].scenario.steps[0].table.rows[0].cells;7console.log(cells[0].value);8console.log(cells[1].value);9console.log(cells[2].value);

Full Screen

Using AI Code Generation

copy

Full Screen

1var Cucumber = require('cucumber-gherkin');2var fs = require('fs');3var feature = fs.readFileSync('test.feature', 'utf8');4var parser = new Cucumber.Parser();5var feature = parser.parse(feature);6var featureElements = feature.getFeatureElements();7for(var i = 0; i < featureElements.length; i++) {8 var scenario = featureElements[i];9 var steps = scenario.getSteps();10 for(var j = 0; j < steps.length; j++) {11 var step = steps[j];12 var table = step.getRows();13 if(table) {14 console.log("Table found for step " + step.getKeyword() + step.getName());15 var tableCells = table.getCellsText();16 for(var k = 0; k < tableCells.length; k++) {17 console.log(tableCells[k]);18 }19 }20 }21}

Full Screen

Cucumber Tutorial:

LambdaTest offers a detailed Cucumber testing tutorial, explaining its features, importance, best practices, and more to help you get started with running your automation testing scripts.

Cucumber Tutorial Chapters:

Here are the detailed Cucumber testing chapters to help you get started:

  • Importance of Cucumber - Learn why Cucumber is important in Selenium automation testing during the development phase to identify bugs and errors.
  • Setting Up Cucumber in Eclipse and IntelliJ - Learn how to set up Cucumber in Eclipse and IntelliJ.
  • Running First Cucumber.js Test Script - After successfully setting up your Cucumber in Eclipse or IntelliJ, this chapter will help you get started with Selenium Cucumber testing in no time.
  • Annotations in Cucumber - To handle multiple feature files and the multiple scenarios in each file, you need to use functionality to execute these scenarios. This chapter will help you learn about a handful of Cucumber annotations ranging from tags, Cucumber hooks, and more to ease the maintenance of the framework.
  • Automation Testing With Cucumber And Nightwatch JS - Learn how to build a robust BDD framework setup for performing Selenium automation testing by integrating Cucumber into the Nightwatch.js framework.
  • Automation Testing With Selenium, Cucumber & TestNG - Learn how to perform Selenium automation testing by integrating Cucumber with the TestNG framework.
  • Integrate Cucumber With Jenkins - By using Cucumber with Jenkins integration, you can schedule test case executions remotely and take advantage of the benefits of Jenkins. Learn how to integrate Cucumber with Jenkins with this detailed chapter.
  • Cucumber Best Practices For Selenium Automation - Take a deep dive into the advanced use cases, such as creating a feature file, separating feature files, and more for Cucumber testing.

Run Cucumber-gherkin automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful