From b8f8bb5bfde1ad923fdeb02c16c0c63cce5498f1 Mon Sep 17 00:00:00 2001 From: Jeremy Kahn Date: Sat, 20 Aug 2022 16:52:31 -0500 Subject: [PATCH] feat: persist userId --- package-lock.json | 35 ++++++++++++++++++++ package.json | 1 + src/Bootstrap.test.tsx | 75 ++++++++++++++++++++++++++++++++++++------ src/Bootstrap.tsx | 68 ++++++++++++++++++++++++++++++-------- src/models/settings.ts | 3 ++ src/models/storage.ts | 3 ++ 6 files changed, 161 insertions(+), 24 deletions(-) create mode 100644 src/models/settings.ts create mode 100644 src/models/storage.ts diff --git a/package-lock.json b/package-lock.json index 0713359..c490e08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", "fast-memoize": "^2.5.2", + "localforage": "^1.10.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^8.0.3", @@ -15987,6 +15988,22 @@ "node": ">=8.9.0" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/localforage/node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -35404,6 +35421,24 @@ "json5": "^2.1.2" } }, + "localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "requires": { + "lie": "3.1.1" + }, + "dependencies": { + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "requires": { + "immediate": "~3.0.5" + } + } + } + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", diff --git a/package.json b/package.json index fcb4560..36f45ee 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", "fast-memoize": "^2.5.2", + "localforage": "^1.10.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^8.0.3", diff --git a/src/Bootstrap.test.tsx b/src/Bootstrap.test.tsx index 9c33f9b..c5702a3 100644 --- a/src/Bootstrap.test.tsx +++ b/src/Bootstrap.test.tsx @@ -1,15 +1,70 @@ -import React from 'react' -import { render } from '@testing-library/react' +import { act, render } from '@testing-library/react' import { MemoryRouter as Router } from 'react-router-dom' +import localforage from 'localforage' -import Bootstrap from './Bootstrap' +import { PersistedStorageKeys } from 'models/storage' -const StubBootstrap = () => ( - - - -) +import Bootstrap, { BootstrapProps } from './Bootstrap' -test('renders', () => { - render() +const mockPersistedStorage = + jest.createMockFromModule>('localforage') + +const mockGetUuid = jest.fn() + +const mockGetItem = jest.fn() +const mockSetItem = jest.fn() + +beforeEach(() => { + mockGetItem.mockImplementation(() => Promise.resolve(null)) + mockSetItem.mockImplementation((data: any) => Promise.resolve(data)) +}) + +const renderBootstrap = async (overrides: BootstrapProps = {}) => { + Object.assign(mockPersistedStorage, { + getItem: mockGetItem, + setItem: mockSetItem, + }) + + render( + + + + ) + + // https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning#an-alternative-waiting-for-the-mocked-promise + await act(async () => { + await Promise.resolve() + }) +} + +test('renders', async () => { + await renderBootstrap() +}) + +test('checks persistedStorage for user settings', async () => { + await renderBootstrap() + expect(mockGetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS) +}) + +test('persists user settings if none were already persisted', async () => { + await renderBootstrap({ + getUuid: mockGetUuid.mockImplementation(() => 'abc123'), + }) + + expect(mockSetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS, { + userId: 'abc123', + }) +}) + +test('does not update user settings if they were already persisted', async () => { + mockGetItem.mockImplementation(() => ({ + userId: 'abc123', + })) + + await renderBootstrap() + + expect(mockSetItem).not.toHaveBeenCalled() }) diff --git a/src/Bootstrap.tsx b/src/Bootstrap.tsx index aacf45e..f687001 100644 --- a/src/Bootstrap.tsx +++ b/src/Bootstrap.tsx @@ -1,24 +1,64 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Routes, Route } from 'react-router-dom' import { v4 as uuid } from 'uuid' +import localforage from 'localforage' -import { Home } from './pages/Home/' -import { PublicRoom } from './pages/PublicRoom/' +import { Home } from 'pages/Home/' +import { PublicRoom } from 'pages/PublicRoom/' +import { UserSettings } from 'models/settings' +import { PersistedStorageKeys } from 'models/storage' -function Bootstrap() { - const [userId] = useState(uuid()) +export interface BootstrapProps { + persistedStorage?: typeof localforage + getUuid?: typeof uuid +} + +function Bootstrap({ + persistedStorage = localforage.createInstance({ + name: 'chitchatter', + description: 'Persisted settings data for chitchatter', + }), + getUuid = uuid, +}: BootstrapProps) { + const [hasLoadedSettings, setHasLoadedSettings] = useState(false) + const [settings, setSettings] = useState({ userId: getUuid() }) + const { userId } = settings + + useEffect(() => { + ;(async () => { + if (hasLoadedSettings) return + + const persistedUserSettings = + await persistedStorage.getItem( + PersistedStorageKeys.USER_SETTINGS + ) + + if (persistedUserSettings) { + setSettings(persistedUserSettings) + } else { + await persistedStorage.setItem( + PersistedStorageKeys.USER_SETTINGS, + settings + ) + } + + setHasLoadedSettings(true) + })() + }, [hasLoadedSettings, persistedStorage, settings, userId]) return (
- - {['/', '/index.html'].map(path => ( - } /> - ))} - } - /> - + {hasLoadedSettings ? ( + + {['/', '/index.html'].map(path => ( + } /> + ))} + } + /> + + ) : null}
) } diff --git a/src/models/settings.ts b/src/models/settings.ts new file mode 100644 index 0000000..c9572f1 --- /dev/null +++ b/src/models/settings.ts @@ -0,0 +1,3 @@ +export interface UserSettings { + userId: string +} diff --git a/src/models/storage.ts b/src/models/storage.ts new file mode 100644 index 0000000..8cce8f1 --- /dev/null +++ b/src/models/storage.ts @@ -0,0 +1,3 @@ +export enum PersistedStorageKeys { + USER_SETTINGS = 'userSettings', +}