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'