diff --git a/res/css/components/views/settings/devices/_DeviceSecurityCard.pcss b/res/css/components/views/settings/devices/_DeviceSecurityCard.pcss index 52d3acc011..2c267b4314 100644 --- a/res/css/components/views/settings/devices/_DeviceSecurityCard.pcss +++ b/res/css/components/views/settings/devices/_DeviceSecurityCard.pcss @@ -64,7 +64,7 @@ limitations under the License. margin: 0 0 $spacing-4 0; } .mx_DeviceSecurityCard_description { - margin: 0 0 $spacing-8 0; + margin: 0; font-size: $font-12px; color: $secondary-content; } diff --git a/res/css/components/views/settings/devices/_FilteredDeviceList.pcss b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss index 307241c78f..e0deb5546d 100644 --- a/res/css/components/views/settings/devices/_FilteredDeviceList.pcss +++ b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss @@ -15,9 +15,45 @@ limitations under the License. */ .mx_FilteredDeviceList { + .mx_Dropdown { + flex: 1 0 80px; + } +} + +.mx_FilteredDeviceList_header { + display: flex; + flex-direction: row; + align-items: center; + box-sizing: border-box; + + width: 100%; + height: 48px; + padding: 0 $spacing-16; + margin-bottom: $spacing-32; + + background-color: $system; + border-radius: 8px; + color: $secondary-content; +} + +.mx_FilteredDeviceList_headerLabel { + flex: 1 1 100%; +} + +.mx_FilteredDeviceList_list { list-style-type: none; display: grid; grid-gap: $spacing-16; margin: 0; padding: 0 $spacing-8; } + +.mx_FilteredDeviceList_securityCard { + margin-bottom: $spacing-32; +} + +.mx_FilteredDeviceList_noResults { + width: 100%; + text-align: center; + margin-bottom: $spacing-32; +} diff --git a/src/components/views/settings/devices/DeviceTile.tsx b/src/components/views/settings/devices/DeviceTile.tsx index 3b553ac401..c791d2cd25 100644 --- a/src/components/views/settings/devices/DeviceTile.tsx +++ b/src/components/views/settings/devices/DeviceTile.tsx @@ -22,7 +22,7 @@ import { formatDate, formatRelativeTime } from "../../../../DateUtils"; import TooltipTarget from "../../elements/TooltipTarget"; import { Alignment } from "../../elements/Tooltip"; import Heading from "../../typography/Heading"; -import { INACTIVE_DEVICE_AGE_MS, isDeviceInactive } from "./filter"; +import { INACTIVE_DEVICE_AGE_DAYS, isDeviceInactive } from "./filter"; import { DeviceWithVerification } from "./types"; export interface DeviceTileProps { device: DeviceWithVerification; @@ -64,12 +64,11 @@ const getInactiveMetadata = (device: DeviceWithVerification): { id: string, valu if (!isInactive) { return undefined; } - const inactiveAgeDays = Math.round(INACTIVE_DEVICE_AGE_MS / MS_DAY); return { id: 'inactive', value: ( <> { - _t('Inactive for %(inactiveAgeDays)s+ days', { inactiveAgeDays }) + + _t('Inactive for %(inactiveAgeDays)s+ days', { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }) + ` (${formatLastActivity(device.last_seen_ts)})` } ), diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx index 116066d78f..4fc8703161 100644 --- a/src/components/views/settings/devices/FilteredDeviceList.tsx +++ b/src/components/views/settings/devices/FilteredDeviceList.tsx @@ -16,40 +16,178 @@ limitations under the License. import React from 'react'; +import { _t } from '../../../../languageHandler'; +import AccessibleButton from '../../elements/AccessibleButton'; +import Dropdown from '../../elements/Dropdown'; +import DeviceSecurityCard from './DeviceSecurityCard'; import DeviceTile from './DeviceTile'; -import { filterDevicesBySecurityRecommendation } from './filter'; -import { DevicesDictionary, DeviceWithVerification } from './types'; +import { + filterDevicesBySecurityRecommendation, + INACTIVE_DEVICE_AGE_DAYS, +} from './filter'; +import { + DevicesDictionary, + DeviceSecurityVariation, + DeviceWithVerification, +} from './types'; interface Props { devices: DevicesDictionary; + filter?: DeviceSecurityVariation; + onFilterChange: (filter: DeviceSecurityVariation | undefined) => void; } // devices without timestamp metadata should be sorted last const sortDevicesByLatestActivity = (left: DeviceWithVerification, right: DeviceWithVerification) => (right.last_seen_ts || 0) - (left.last_seen_ts || 0); -const getFilteredSortedDevices = (devices: DevicesDictionary) => - filterDevicesBySecurityRecommendation(Object.values(devices), []) +const getFilteredSortedDevices = (devices: DevicesDictionary, filter: DeviceSecurityVariation) => + filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : []) .sort(sortDevicesByLatestActivity); +const ALL_FILTER_ID = 'ALL'; + +const FilterSecurityCard: React.FC<{ filter?: DeviceSecurityVariation | string }> = ({ filter }) => { + switch (filter) { + case DeviceSecurityVariation.Verified: + return
+ +
+ ; + case DeviceSecurityVariation.Unverified: + return
+ +
+ ; + case DeviceSecurityVariation.Inactive: + return
+ +
+ ; + default: + return null; + } +}; + +const getNoResultsMessage = (filter: DeviceSecurityVariation): string => { + switch (filter) { + case DeviceSecurityVariation.Verified: + return _t('No verified sessions found.'); + case DeviceSecurityVariation.Unverified: + return _t('No unverified sessions found.'); + case DeviceSecurityVariation.Inactive: + return _t('No inactive sessions found.'); + default: + return _t('No sessions found.'); + } +}; +interface NoResultsProps { filter: DeviceSecurityVariation, clearFilter: () => void} +const NoResults: React.FC = ({ filter, clearFilter }) => +
+ { getNoResultsMessage(filter) } + { + /* No clear filter button when filter is falsy (ie 'All') */ + !!filter && + <> +   + + { _t('Show all') } + + + } +
; + /** * Filtered list of devices * Sorted by latest activity descending - * TODO(kerrya) Filtering to added as part of PSG-648 */ -const FilteredDeviceList: React.FC = ({ devices }) => { - const sortedDevices = getFilteredSortedDevices(devices); +const FilteredDeviceList: React.FC = ({ devices, filter, onFilterChange }) => { + const sortedDevices = getFilteredSortedDevices(devices, filter); - return
    - { sortedDevices.map((device) => -
  1. - -
  2. , + const options = [ + { id: ALL_FILTER_ID, label: _t('All') }, + { + id: DeviceSecurityVariation.Verified, + label: _t('Verified'), + description: _t('Ready for secure messaging'), + }, + { + id: DeviceSecurityVariation.Unverified, + label: _t('Unverified'), + description: _t('Not ready for secure messaging'), + }, + { + id: DeviceSecurityVariation.Inactive, + label: _t('Inactive'), + description: _t( + 'Inactive for %(inactiveAgeDays)s days or longer', + { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }, + ), + }, + ]; - ) } -
; + const onFilterOptionChange = (filterId: DeviceSecurityVariation | typeof ALL_FILTER_ID) => { + onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation); + }; + + return
+
+ + { _t('Sessions') } + + + { options.map(({ id, label }) => +
{ label }
, + ) } +
+
+ { !!sortedDevices.length + ? + : onFilterChange(undefined)} /> + } +
    + { sortedDevices.map((device) => +
  1. + +
  2. , + + ) } +
+
+ ; }; export default FilteredDeviceList; diff --git a/src/components/views/settings/devices/SecurityRecommendations.tsx b/src/components/views/settings/devices/SecurityRecommendations.tsx index fcb8f086c9..00181f5674 100644 --- a/src/components/views/settings/devices/SecurityRecommendations.tsx +++ b/src/components/views/settings/devices/SecurityRecommendations.tsx @@ -20,16 +20,19 @@ import { _t } from '../../../../languageHandler'; import AccessibleButton from '../../elements/AccessibleButton'; import SettingsSubsection from '../shared/SettingsSubsection'; import DeviceSecurityCard from './DeviceSecurityCard'; -import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_MS } from './filter'; -import { DevicesDictionary, DeviceSecurityVariation } from './types'; +import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter'; +import { + DeviceSecurityVariation, + DeviceWithVerification, + DevicesDictionary, +} from './types'; interface Props { devices: DevicesDictionary; } -const MS_DAY = 24 * 60 * 60 * 1000; const SecurityRecommendations: React.FC = ({ devices }) => { - const devicesArray = Object.values(devices); + const devicesArray = Object.values(devices); const unverifiedDevicesCount = filterDevicesBySecurityRecommendation( devicesArray, @@ -44,7 +47,7 @@ const SecurityRecommendations: React.FC = ({ devices }) => { return null; } - const inactiveAgeDays = INACTIVE_DEVICE_AGE_MS / MS_DAY; + const inactiveAgeDays = INACTIVE_DEVICE_AGE_DAYS; // TODO(kerrya) stubbed until PSG-640/652 const noop = () => {}; diff --git a/src/components/views/settings/devices/filter.ts b/src/components/views/settings/devices/filter.ts index 302c66969b..ad2bc92152 100644 --- a/src/components/views/settings/devices/filter.ts +++ b/src/components/views/settings/devices/filter.ts @@ -18,7 +18,9 @@ import { DeviceWithVerification, DeviceSecurityVariation } from "./types"; type DeviceFilterCondition = (device: DeviceWithVerification) => boolean; +const MS_DAY = 24 * 60 * 60 * 1000; export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days +export const INACTIVE_DEVICE_AGE_DAYS = INACTIVE_DEVICE_AGE_MS / MS_DAY; export const isDeviceInactive: DeviceFilterCondition = device => !!device.last_seen_ts && device.last_seen_ts < Date.now() - INACTIVE_DEVICE_AGE_MS; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 3a0a9b976b..ddd2293254 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { _t } from "../../../../../languageHandler"; import { useOwnDevices } from '../../devices/useOwnDevices'; @@ -22,10 +22,12 @@ import SettingsSubsection from '../../shared/SettingsSubsection'; import FilteredDeviceList from '../../devices/FilteredDeviceList'; import CurrentDeviceSection from '../../devices/CurrentDeviceSection'; import SecurityRecommendations from '../../devices/SecurityRecommendations'; +import { DeviceSecurityVariation } from '../../devices/types'; import SettingsTab from '../SettingsTab'; const SessionManagerTab: React.FC = () => { const { devices, currentDeviceId, isLoading } = useOwnDevices(); + const [filter, setFilter] = useState(); const { [currentDeviceId]: currentDevice, ...otherDevices } = devices; const shouldShowOtherSessions = Object.keys(otherDevices).length > 0; @@ -46,7 +48,11 @@ const SessionManagerTab: React.FC = () => { )} data-testid='other-sessions-section' > - + } ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2928afb553..efc1036039 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1708,13 +1708,26 @@ "This session is ready for secure messaging.": "This session is ready for secure messaging.", "Unverified session": "Unverified session", "Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.", - "Security recommendations": "Security recommendations", - "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", + "Verified sessions": "Verified sessions", + "For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.", "Unverified sessions": "Unverified sessions", "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.", - "View all": "View all", "Inactive sessions": "Inactive sessions", "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore", + "No verified sessions found.": "No verified sessions found.", + "No unverified sessions found.": "No unverified sessions found.", + "No inactive sessions found.": "No inactive sessions found.", + "No sessions found.": "No sessions found.", + "Show all": "Show all", + "All": "All", + "Ready for secure messaging": "Ready for secure messaging", + "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", + "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", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", "Invalid Email Address": "Invalid Email Address", @@ -2234,7 +2247,6 @@ "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", - "Show all": "Show all", "Reactions": "Reactions", "%(reactors)s reacted with %(content)s": "%(reactors)s reacted with %(content)s", "reacted with %(shortName)s": "reacted with %(shortName)s", diff --git a/test/components/views/settings/DevicesPanel-test.tsx b/test/components/views/settings/DevicesPanel-test.tsx index e03274c0ae..d9a66ab7bd 100644 --- a/test/components/views/settings/DevicesPanel-test.tsx +++ b/test/components/views/settings/DevicesPanel-test.tsx @@ -128,7 +128,7 @@ describe('', () => { await flushPromises(); // modal rendering has some weird sleeps - await sleep(10); + await sleep(100); expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith([device2.device_id], undefined); diff --git a/test/components/views/settings/devices/FilteredDeviceList-test.tsx b/test/components/views/settings/devices/FilteredDeviceList-test.tsx index 3545e0b261..f814a6d703 100644 --- a/test/components/views/settings/devices/FilteredDeviceList-test.tsx +++ b/test/components/views/settings/devices/FilteredDeviceList-test.tsx @@ -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('', () => { - 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('', () => { 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('', () => { 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 0000000000..c0f5b9af98 --- /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. +   + +
, +] +`;