Device manager - device list filtering (PSG-648) (#9181)

* add device filtering

* improve dropdown styling

* test device filtering

* update type imports

* fix types

* security card margin

* more specific type for onFilterOptionChange
This commit is contained in:
Kerry 2022-08-16 16:05:10 +02:00 committed by GitHub
parent aa9191bc34
commit 6f2c761fb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 529 additions and 43 deletions

View file

@ -128,7 +128,7 @@ describe('<DevicesPanel />', () => {
await flushPromises();
// modal rendering has some weird sleeps
await sleep(10);
await sleep(100);
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([device2.device_id], undefined);

View file

@ -15,25 +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/types';
import { flushPromises, mockPlatformPeg } from '../../../../test-utils';
mockPlatformPeg();
const MS_DAY = 86400000;
describe('<FilteredDeviceList />', () => {
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 = {}) =>
@ -43,14 +57,16 @@ describe('<FilteredDeviceList />', () => {
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());
@ -59,7 +75,108 @@ describe('<FilteredDeviceList />', () => {
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);
});
});
});

View file

@ -0,0 +1,173 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<FilteredDeviceList /> displays no results message when there are no devices 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_noResults"
>
No sessions found.
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering filters correctly for Inactive 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_securityCard"
>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Inactive"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Inactive sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Consider signing out from old sessions (90 days or older) you don't use anymore
</p>
</div>
</div>
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering filters correctly for Unverified 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_securityCard"
>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.
</p>
</div>
</div>
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering filters correctly for Verified 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_securityCard"
>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Verified"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Verified sessions
</p>
<p
class="mx_DeviceSecurityCard_description"
>
For best security, sign out from any session that you don't recognize or use anymore.
</p>
</div>
</div>
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering renders no results correctly for Inactive 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_noResults"
>
No inactive sessions found.
 
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="devices-clear-filter-btn"
role="button"
tabindex="0"
>
Show all
</div>
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering renders no results correctly for Unverified 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_noResults"
>
No unverified sessions found.
 
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="devices-clear-filter-btn"
role="button"
tabindex="0"
>
Show all
</div>
</div>,
]
`;
exports[`<FilteredDeviceList /> filtering renders no results correctly for Verified 1`] = `
HTMLCollection [
<div
class="mx_FilteredDeviceList_noResults"
>
No verified sessions found.
 
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="devices-clear-filter-btn"
role="button"
tabindex="0"
>
Show all
</div>
</div>,
]
`;