diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx index 8447f1fc456d..bbc3286e6b8a 100644 --- a/src/components/views/settings/devices/FilteredDeviceList.tsx +++ b/src/components/views/settings/devices/FilteredDeviceList.tsx @@ -112,7 +112,13 @@ const NoResults: React.FC = ({ filter, clearFilter }) => !!filter && <>   - { _t('Show all') } + + { _t('Show all') } + } ; @@ -157,11 +163,11 @@ const FilteredDeviceList: React.FC = ({ devices, filter, onFilterChange } - { options.map(({ id, label, description }) => + { options.map(({ id, label }) =>
{ label }
, ) }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4fc8250a9a9c..074e2126a43d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1724,7 +1724,7 @@ "Not ready for secure messaging": "Not ready for secure messaging", "Inactive": "Inactive", "Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer", - "Show": "Show", + "Filter devices": "Filter devices", "Security recommendations": "Security recommendations", "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", "View all": "View all", diff --git a/test/components/views/settings/devices/FilteredDeviceList-test.tsx b/test/components/views/settings/devices/FilteredDeviceList-test.tsx index 0d9fd66172c9..26a2cb8753f6 100644 --- a/test/components/views/settings/devices/FilteredDeviceList-test.tsx +++ b/test/components/views/settings/devices/FilteredDeviceList-test.tsx @@ -15,26 +15,39 @@ limitations under the License. */ import React from 'react'; -import { render } from '@testing-library/react'; +import { act, fireEvent, render } from '@testing-library/react'; import FilteredDeviceList from '../../../../../src/components/views/settings/devices/FilteredDeviceList'; +import { DeviceSecurityVariation } from '../../../../../src/components/views/settings/devices/filter'; +import { flushPromises, mockPlatformPeg } from '../../../../test-utils'; +mockPlatformPeg(); + +const MS_DAY = 86400000; describe('', () => { - const noMetaDevice = { device_id: 'no-meta-device', isVerified: true }; - const oldDevice = { device_id: 'old', last_seen_ts: new Date(1993, 7, 3, 4).getTime(), isVerified: true }; const newDevice = { device_id: 'new', - last_seen_ts: new Date().getTime() - 500, + last_seen_ts: Date.now() - 500, last_seen_ip: '123.456.789', display_name: 'My Device', isVerified: true, }; + const unverifiedNoMetadata = { device_id: 'unverified-no-metadata', isVerified: false }; + const verifiedNoMetadata = { device_id: 'verified-no-metadata', isVerified: true }; + const hundredDaysOld = { device_id: '100-days-old', isVerified: true, last_seen_ts: Date.now() - (MS_DAY * 100) }; + const hundredDaysOldUnverified = { + device_id: 'unverified-100-days-old', + isVerified: false, + last_seen_ts: Date.now() - (MS_DAY * 100), + }; const defaultProps = { onFilterChange: jest.fn(), devices: { - [noMetaDevice.device_id]: noMetaDevice, - [oldDevice.device_id]: oldDevice, + [unverifiedNoMetadata.device_id]: unverifiedNoMetadata, + [verifiedNoMetadata.device_id]: verifiedNoMetadata, [newDevice.device_id]: newDevice, + [hundredDaysOld.device_id]: hundredDaysOld, + [hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified, }, }; const getComponent = (props = {}) => @@ -44,14 +57,16 @@ describe('', () => { const { container } = render(getComponent()); const tiles = container.querySelectorAll('.mx_DeviceTile'); expect(tiles[0].getAttribute('data-testid')).toEqual(`device-tile-${newDevice.device_id}`); - expect(tiles[1].getAttribute('data-testid')).toEqual(`device-tile-${oldDevice.device_id}`); - expect(tiles[2].getAttribute('data-testid')).toEqual(`device-tile-${noMetaDevice.device_id}`); + expect(tiles[1].getAttribute('data-testid')).toEqual(`device-tile-${hundredDaysOld.device_id}`); + expect(tiles[2].getAttribute('data-testid')).toEqual(`device-tile-${hundredDaysOldUnverified.device_id}`); + expect(tiles[3].getAttribute('data-testid')).toEqual(`device-tile-${unverifiedNoMetadata.device_id}`); + expect(tiles[4].getAttribute('data-testid')).toEqual(`device-tile-${verifiedNoMetadata.device_id}`); }); it('updates list order when devices change', () => { - const updatedOldDevice = { ...oldDevice, last_seen_ts: new Date().getTime() }; + const updatedOldDevice = { ...hundredDaysOld, last_seen_ts: new Date().getTime() }; const updatedDevices = { - [oldDevice.device_id]: updatedOldDevice, + [hundredDaysOld.device_id]: updatedOldDevice, [newDevice.device_id]: newDevice, }; const { container, rerender } = render(getComponent()); @@ -60,7 +75,108 @@ describe('', () => { const tiles = container.querySelectorAll('.mx_DeviceTile'); expect(tiles.length).toBe(2); - expect(tiles[0].getAttribute('data-testid')).toEqual(`device-tile-${oldDevice.device_id}`); + expect(tiles[0].getAttribute('data-testid')).toEqual(`device-tile-${hundredDaysOld.device_id}`); expect(tiles[1].getAttribute('data-testid')).toEqual(`device-tile-${newDevice.device_id}`); }); + + it('displays no results message when there are no devices', () => { + const { container } = render(getComponent({ devices: {} })); + + expect(container.getElementsByClassName('mx_FilteredDeviceList_noResults')).toMatchSnapshot(); + }); + + describe('filtering', () => { + const setFilter = async ( + container: HTMLElement, + option: DeviceSecurityVariation | string, + ) => await act(async () => { + const dropdown = container.querySelector('[aria-label="Filter devices"]'); + + fireEvent.click(dropdown); + // tick to let dropdown render + await flushPromises(); + + fireEvent.click(container.querySelector(`#device-list-filter__${option}`)); + }); + + it('does not display filter description when filter is falsy', () => { + const { container } = render(getComponent({ filter: undefined })); + const tiles = container.querySelectorAll('.mx_DeviceTile'); + expect(container.getElementsByClassName('mx_FilteredDeviceList_securityCard').length).toBeFalsy(); + expect(tiles.length).toEqual(5); + }); + + it('updates filter when prop changes', () => { + const { container, rerender } = render(getComponent({ filter: DeviceSecurityVariation.Verified })); + const tiles = container.querySelectorAll('.mx_DeviceTile'); + expect(tiles.length).toEqual(3); + expect(tiles[0].getAttribute('data-testid')).toEqual(`device-tile-${newDevice.device_id}`); + expect(tiles[1].getAttribute('data-testid')).toEqual(`device-tile-${hundredDaysOld.device_id}`); + expect(tiles[2].getAttribute('data-testid')).toEqual(`device-tile-${verifiedNoMetadata.device_id}`); + + rerender(getComponent({ filter: DeviceSecurityVariation.Inactive })); + + const rerenderedTiles = container.querySelectorAll('.mx_DeviceTile'); + expect(rerenderedTiles.length).toEqual(2); + expect(rerenderedTiles[0].getAttribute('data-testid')).toEqual(`device-tile-${hundredDaysOld.device_id}`); + expect(rerenderedTiles[1].getAttribute('data-testid')).toEqual( + `device-tile-${hundredDaysOldUnverified.device_id}`, + ); + }); + + it('calls onFilterChange handler', async () => { + const onFilterChange = jest.fn(); + const { container } = render(getComponent({ onFilterChange })); + await setFilter(container, DeviceSecurityVariation.Verified); + + expect(onFilterChange).toHaveBeenCalledWith(DeviceSecurityVariation.Verified); + }); + + it('calls onFilterChange handler correctly when setting filter to All', async () => { + const onFilterChange = jest.fn(); + const { container } = render(getComponent({ onFilterChange, filter: DeviceSecurityVariation.Verified })); + await setFilter(container, 'ALL'); + + // filter is cleared + expect(onFilterChange).toHaveBeenCalledWith(undefined); + }); + + it.each([ + [DeviceSecurityVariation.Verified, [newDevice, hundredDaysOld, verifiedNoMetadata]], + [DeviceSecurityVariation.Unverified, [hundredDaysOldUnverified, unverifiedNoMetadata]], + [DeviceSecurityVariation.Inactive, [hundredDaysOld, hundredDaysOldUnverified]], + ])('filters correctly for %s', (filter, expectedDevices) => { + const { container } = render(getComponent({ filter })); + expect(container.getElementsByClassName('mx_FilteredDeviceList_securityCard')).toMatchSnapshot(); + const tileDeviceIds = [...container.querySelectorAll('.mx_DeviceTile')] + .map(tile => tile.getAttribute('data-testid')); + expect(tileDeviceIds).toEqual(expectedDevices.map(device => `device-tile-${device.device_id}`)); + }); + + it.each([ + [DeviceSecurityVariation.Verified], + [DeviceSecurityVariation.Unverified], + [DeviceSecurityVariation.Inactive], + ])('renders no results correctly for %s', (filter) => { + const { container } = render(getComponent({ filter, devices: {} })); + expect(container.getElementsByClassName('mx_FilteredDeviceList_securityCard').length).toBeFalsy(); + expect(container.getElementsByClassName('mx_FilteredDeviceList_noResults')).toMatchSnapshot(); + }); + + it('clears filter from no results message', () => { + const onFilterChange = jest.fn(); + const { getByTestId } = render(getComponent({ + onFilterChange, + filter: DeviceSecurityVariation.Verified, + devices: { + [unverifiedNoMetadata.device_id]: unverifiedNoMetadata, + }, + })); + act(() => { + fireEvent.click(getByTestId('devices-clear-filter-btn')); + }); + + expect(onFilterChange).toHaveBeenCalledWith(undefined); + }); + }); }); diff --git a/test/components/views/settings/devices/__snapshots__/FilteredDeviceList-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/FilteredDeviceList-test.tsx.snap new file mode 100644 index 000000000000..c0f5b9af98cc --- /dev/null +++ b/test/components/views/settings/devices/__snapshots__/FilteredDeviceList-test.tsx.snap @@ -0,0 +1,173 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` displays no results message when there are no devices 1`] = ` +HTMLCollection [ +
+ No sessions found. +
, +] +`; + +exports[` filtering filters correctly for Inactive 1`] = ` +HTMLCollection [ +
+
+
+
+
+
+

+ Inactive sessions +

+

+ Consider signing out from old sessions (90 days or older) you don't use anymore +

+
+
+
, +] +`; + +exports[` filtering filters correctly for Unverified 1`] = ` +HTMLCollection [ +
+
+
+
+
+
+

+ Unverified sessions +

+

+ Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore. +

+
+
+
, +] +`; + +exports[` filtering filters correctly for Verified 1`] = ` +HTMLCollection [ +
+
+
+
+
+
+

+ Verified sessions +

+

+ For best security, sign out from any session that you don't recognize or use anymore. +

+
+
+
, +] +`; + +exports[` filtering renders no results correctly for Inactive 1`] = ` +HTMLCollection [ +
+ No inactive sessions found. +   + +
, +] +`; + +exports[` filtering renders no results correctly for Unverified 1`] = ` +HTMLCollection [ +
+ No unverified sessions found. +   + +
, +] +`; + +exports[` filtering renders no results correctly for Verified 1`] = ` +HTMLCollection [ +
+ No verified sessions found. +   + +
, +] +`;