diff --git a/src/renderer/src/components/UserAvatar.tsx b/src/renderer/src/components/UserAvatar.tsx index 0abe82a59..3c69bcba0 100644 --- a/src/renderer/src/components/UserAvatar.tsx +++ b/src/renderer/src/components/UserAvatar.tsx @@ -6,6 +6,12 @@ import { useAvatarSource } from '../crud'; import { avatarSize } from '../control'; import { useOrbitData } from '../hoc/useOrbitData'; +const emptyUser: UserD = { + id: '', + type: 'user', + attributes: { avatarUrl: null, name: '', familyName: '' }, +} as UserD; + interface IProps { userRec?: UserD; } @@ -19,15 +25,7 @@ export function UserAvatar(props: IProps) { ? [] : users.filter((u) => u.id === user && u.attributes); const firstUser = curUserRec[0] as UserD; - const curUser = userRec - ? userRec - : firstUser - ? firstUser - : { - id: '', - type: 'user', - attributes: { avatarUrl: null, name: '', familyName: '' }, - }; + const curUser = userRec ? userRec : firstUser ? firstUser : emptyUser; const source = useAvatarSource(curUser.attributes?.familyName || '', curUser); diff --git a/src/renderer/src/crud/useAvatarSource.test.ts b/src/renderer/src/crud/useAvatarSource.test.ts new file mode 100644 index 000000000..b084b40b1 --- /dev/null +++ b/src/renderer/src/crud/useAvatarSource.test.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +const mockDataPath = jest.fn(); +const mockRemoteId = jest.fn(() => '9'); +const mockExists = jest.fn(); +const mockIsWindows = jest.fn(); + +jest.mock('../../api-variable', () => ({ + isElectron: true, +})); + +jest.mock('../utils/dataPath', () => ({ + dataPath: mockDataPath, + PathType: { AVATARS: 'avatars' }, +})); + +jest.mock('./remoteId', () => ({ + remoteId: mockRemoteId, +})); + +jest.mock('../context/useGlobal', () => ({ + useGlobal: () => [{}, jest.fn()], +})); + +describe('useAvatarSource', () => { + const rec = { + id: '', + type: 'user', + attributes: { avatarUrl: null, name: '', familyName: '' }, + }; + + function load() { + jest.resetModules(); + (window as unknown as { api: unknown }).api = { + exists: mockExists, + isWindows: mockIsWindows, + }; + const { renderHook, waitFor } = require('@testing-library/react/pure'); + const { useAvatarSource } = require('./useAvatarSource'); + return { renderHook, waitFor, useAvatarSource }; + } + + beforeEach(() => { + mockDataPath.mockReset(); + mockRemoteId.mockReset().mockReturnValue('9'); + mockExists.mockReset(); + mockIsWindows.mockReset().mockResolvedValue(true); + }); + + it('does not use the offline data directory as an avatar src', async () => { + mockDataPath.mockResolvedValue('C:/Users/shent/transcriber'); + mockExists.mockResolvedValue(true); + const { renderHook, waitFor, useAvatarSource } = load(); + const { result } = renderHook(() => useAvatarSource('', rec)); + await waitFor(() => expect(result.current).toBe('')); + expect(mockExists).not.toHaveBeenCalled(); + }); + + it('uses file:// for a local png that exists', async () => { + mockDataPath.mockResolvedValue('C:/Users/shent/transcriber/9.png'); + mockExists.mockResolvedValue(true); + const { renderHook, waitFor, useAvatarSource } = load(); + const { result } = renderHook(() => + useAvatarSource('Smith', { ...rec, id: 'u1' }) + ); + await waitFor(() => expect(result.current).toMatch(/^file:\/\/.*9\.png$/)); + }); +}); diff --git a/src/renderer/src/crud/useAvatarSource.ts b/src/renderer/src/crud/useAvatarSource.ts index e0a7bf1a3..b215ed8e8 100644 --- a/src/renderer/src/crud/useAvatarSource.ts +++ b/src/renderer/src/crud/useAvatarSource.ts @@ -1,7 +1,8 @@ import { useEffect, useState } from 'react'; +import path from 'path-browserify'; import { User } from '../model'; import { dataPath, PathType } from '../utils/dataPath'; -import { remoteId } from '../crud'; +import { remoteId } from './remoteId'; import { isElectron } from '../../api-variable'; import { RecordIdentity, RecordKeyMap } from '@orbit/records'; import { useGlobal } from '../context/useGlobal'; @@ -22,11 +23,10 @@ export const useAvatarSource = (name: string, rec: RecordIdentity) => { '.png', }); if (src && isElectron && !src.startsWith('http')) { - if (await ipc?.exists(src)) { - const url = (await ipc?.isWindows()) - ? new URL(src).toString().slice(8) - : src; - src = `file://${url}`; + // exists() is true for the offline data directory; only files are avatars + if (path.extname(src) && (await ipc?.exists(src))) { + const start = (await ipc?.isWindows()) ? 8 : 7; + src = `file://${new URL(`file://${src}`).toString().slice(start)}`; } else src = ''; } setSource(src); diff --git a/src/renderer/src/utils/dataPath.test.ts b/src/renderer/src/utils/dataPath.test.ts index 346fe9c22..55755dd22 100644 --- a/src/renderer/src/utils/dataPath.test.ts +++ b/src/renderer/src/utils/dataPath.test.ts @@ -85,6 +85,16 @@ describe('dataPath', () => { expect(p).toBe('C:\\\\home/offline/burrito/TST/text/metadata.json'); }); + it('dataPath() still returns the offline data root when that folder does not exist yet', async () => { + const { mod } = load({ + isElectron: true, + offlineData: 'transcriber', + existsImpl: async () => false, + }); + // ElectronImport / ProjectDownload call dataPath() then createFolder(where) + await expect(mod.dataPath()).resolves.toBe('C:\\\\home/transcriber'); + }); + it('returns empty string when offlineData is empty and relPath is not http', async () => { const { mod } = load({ isElectron: false, offlineData: '' }); await expect(