diff --git a/ts/components/avatar/AvatarPlaceHolder/AvatarPlaceHolder.tsx b/ts/components/avatar/AvatarPlaceHolder/AvatarPlaceHolder.tsx index a09e077c4..dde44ba6f 100644 --- a/ts/components/avatar/AvatarPlaceHolder/AvatarPlaceHolder.tsx +++ b/ts/components/avatar/AvatarPlaceHolder/AvatarPlaceHolder.tsx @@ -1,5 +1,7 @@ import { useEffect, useState } from 'react'; +import { getSodiumRenderer } from '../../../session/crypto'; import { allowOnlyOneAtATime } from '../../../session/utils/Promise'; +import { toHex } from '../../../session/utils/String'; import { COLORS } from '../../../themes/constants/colors'; import { getInitials } from '../../../util/getInitials'; import { MemberAvatarPlaceHolder } from '../../icon/MemberAvatarPlaceHolder'; @@ -11,19 +13,15 @@ type Props = { dataTestId?: string; }; +/** NOTE we use libsodium instead of crypto.subtle.digest because node:crypto.subtle.digest does not work the same way and we need to unit test this component */ const sha512FromPubkeyOneAtAtime = async (pubkey: string) => { return allowOnlyOneAtATime(`sha512FromPubkey-${pubkey}`, async () => { - const buf = await crypto.subtle.digest('SHA-512', new TextEncoder().encode(pubkey)); - - return Array.prototype.map - .call(new Uint8Array(buf), (x: any) => `00${x.toString(16)}`.slice(-2)) - .join(''); + const sodium = await getSodiumRenderer(); + const buf = sodium.crypto_hash_sha512(pubkey); + return toHex(buf); }); }; -// for testing purposes only -export const AvatarPlaceHolderUtils = { sha512FromPubkeyOneAtAtime }; - // do not do this on every avatar, just cache the values so we can reuse them across the app // key is the pubkey, value is the hash const cachedHashes = new Map(); @@ -55,7 +53,7 @@ function useHashBasedOnPubkey(pubkey: string) { } // eslint-disable-next-line more/no-then - void AvatarPlaceHolderUtils.sha512FromPubkeyOneAtAtime(pubkey).then(sha => { + void sha512FromPubkeyOneAtAtime(pubkey).then(sha => { if (isInProgress) { setIsLoading(false); // Generate the seed simulate the .hashCode as Java @@ -116,9 +114,9 @@ export const AvatarPlaceHolder = (props: Props) => { fontSize={fontSize} x="50%" y="50%" - fill="white" + fill="var(--white-color)" textAnchor="middle" - stroke="white" + stroke="var(--white-color)" strokeWidth={1} alignmentBaseline="central" height={fontSize} diff --git a/ts/test/components/AvatarPlaceHolder_test.tsx b/ts/test/components/AvatarPlaceHolder_test.tsx index 18edd4498..f5778f742 100644 --- a/ts/test/components/AvatarPlaceHolder_test.tsx +++ b/ts/test/components/AvatarPlaceHolder_test.tsx @@ -1,17 +1,12 @@ /* eslint-disable import/no-extraneous-dependencies */ import chai, { expect } from 'chai'; import chaiDom from 'chai-dom'; -import { isEqual } from 'lodash'; import Sinon from 'sinon'; import { AvatarSize } from '../../components/avatar/Avatar'; -import { - AvatarPlaceHolder, - AvatarPlaceHolderUtils, -} from '../../components/avatar/AvatarPlaceHolder/AvatarPlaceHolder'; +import { AvatarPlaceHolder } from '../../components/avatar/AvatarPlaceHolder/AvatarPlaceHolder'; import { MemberAvatarPlaceHolder } from '../../components/icon/MemberAvatarPlaceHolder'; -import { allowOnlyOneAtATime } from '../../session/utils/Promise'; +import { COLORS } from '../../themes/constants/colors'; import { TestUtils } from '../test-utils'; -import { stubCrypto } from '../test-utils/utils'; import { cleanup, renderComponent, waitFor } from './renderComponent'; chai.use(chaiDom); @@ -20,21 +15,8 @@ describe('AvatarPlaceHolder', () => { const pubkey = TestUtils.generateFakePubKeyStr(); const displayName = 'Hello World'; - beforeEach(async () => { + beforeEach(() => { TestUtils.stubWindowLog(); - - // NOTE this is the best way I have found to stub the crypto module for now - const crypto = await stubCrypto(); - // code must match the original exactly - Sinon.stub(AvatarPlaceHolderUtils, 'sha512FromPubkeyOneAtAtime').returns( - allowOnlyOneAtATime(`sha512FromPubkey-${'pubkey'}`, async () => { - const buf = await crypto.subtle.digest('SHA-512', new TextEncoder().encode(pubkey)); - - return Array.prototype.map - .call(new Uint8Array(buf), (x: any) => `00${x.toString(16)}`.slice(-2)) - .join(''); - }) - ); }); afterEach(() => { @@ -53,7 +35,7 @@ describe('AvatarPlaceHolder', () => { /> ); - // we need to wait for the component to render after calculating the hash + // calculating the hash and initials needs to be done first await waitFor(() => { result.getByText('HW'); }); @@ -63,14 +45,14 @@ describe('AvatarPlaceHolder', () => { expect(el.outerHTML, 'should not be undefined').to.not.equal(undefined); expect(el.outerHTML, 'should not be an empty string').to.not.equal(''); expect(el.tagName, 'should be an svg').to.equal('svg'); + result.unmount(); }); - it('should render the MemberAvatarPlaceholder if we are loading or there is no hash', async () => { const result = renderComponent( ); @@ -82,10 +64,65 @@ describe('AvatarPlaceHolder', () => { const el2 = result2.getByTestId('member-avatar-placeholder'); // The data test ids are different so we don't use the outerHTML for comparison - expect(isEqual(el.innerHTML, el2.innerHTML)).to.equal(true); + expect(el.innerHTML).to.equal(el2.innerHTML); + result.unmount(); + }); + it('should render the background color using COLORS.PRIMARY with the correct order', async () => { + const testPubkeys = [ + '0541214ef26572066f0535140b1d6d021218299321c6001e2cdcaaa8cd5c9382fc', // green + '0541214ef26572066f0535140b1d6d021218299321c6001e2cdcaaa8cd5c9382fa', // blue + '0541214ef26572066f0535140b1d6d021218299321c6001e2cdcaaa8cd5c9382fd', // yellow + '0541214ef26572066f0535140b1d6d021218299321c6001e2cdcaaa8cd5c9382ff', // pink + '0541214ef26572066f0535140b1d6d021218299321c6001e2cdcaaa8cd5c9382ed', // purple + '0541214ef26572066f0535140b1d6d021218299321c6001e2cdcaaa8cd5c9382f9', // orange + '0541214ef26572066f0535140b1d6d021218299321c6001e2cdcaaa8cd5c9382eb', // red + ]; + + // NOTE we can trust the order of Object.keys and Object.values to be correct since our typescript build target is 'esnext' + const primaryColorKeys = Object.keys(COLORS.PRIMARY); + const primaryColorValues = Object.values(COLORS.PRIMARY); + + async function testBackgroundColor(testPubkey: string, expectedColorValue: string) { + const result = renderComponent( + + ); + + // calculating the hash and initials needs to be done first + await waitFor(() => { + result.getByText('HW'); + }); + + const el = result.getByTestId('avatar-placeholder'); + const circle = el.querySelector('circle'); + const circleColor = circle?.getAttribute('fill'); + expect(circleColor, 'background color should not be null').to.not.equal(null); + expect(circleColor, 'background color should not be undefined').to.not.equal(undefined); + expect(circleColor, 'background color should not be an empty string').to.not.equal(''); + expect( + primaryColorValues.includes(circleColor!), + 'background color should be in COLORS.PRIMARY' + ).to.equal(true); + expect( + circleColor, + `background color should be ${primaryColorKeys[primaryColorValues.indexOf(expectedColorValue)]} (${expectedColorValue}) and not ${primaryColorKeys[primaryColorValues.indexOf(circleColor!)]} (${circleColor}) for testPubkey ${testPubkeys.indexOf(testPubkey)} (${testPubkey})` + ).to.equal(expectedColorValue); + result.unmount(); + } + + // NOTE this is the standard order of background colors for avatars on each platform + await testBackgroundColor(testPubkeys[0], COLORS.PRIMARY.GREEN); + await testBackgroundColor(testPubkeys[1], COLORS.PRIMARY.BLUE); + await testBackgroundColor(testPubkeys[2], COLORS.PRIMARY.YELLOW); + await testBackgroundColor(testPubkeys[3], COLORS.PRIMARY.PINK); + await testBackgroundColor(testPubkeys[4], COLORS.PRIMARY.PURPLE); + await testBackgroundColor(testPubkeys[5], COLORS.PRIMARY.ORANGE); + await testBackgroundColor(testPubkeys[6], COLORS.PRIMARY.RED); }); - // TODO it should render the correct theme colors - // TODO given a pubkey it should render the correct color // TODO given a name it should render the correct initials }); diff --git a/ts/test/test-utils/utils/components.ts b/ts/test/test-utils/utils/components.ts index cfdca9a99..1d9519eb6 100644 --- a/ts/test/test-utils/utils/components.ts +++ b/ts/test/test-utils/utils/components.ts @@ -6,7 +6,7 @@ const printHTMLElement = async (element: HTMLElement, name?: string) => { throw Error('window.log is not defined. Have you turned on enableLogRedirect?'); } - return window.log.debug(`\nRender Result${name ? ` (${name})` : ''}:\n${prettyDOM(element)}\n`); + return window.log.debug(`\nHTML Element${name ? ` (${name})` : ''}:\n${prettyDOM(element)}\n`); }; const printRenderResult = async (result: RenderResult, name?: string) => { if (!window.log || !enableLogRedirect) { @@ -14,7 +14,7 @@ const printRenderResult = async (result: RenderResult, name?: string) => { } return window.log.debug( - `\nHTML Element${name ? ` (${name})` : ''}:\n${prettyDOM(result.baseElement)}\n` + `\nRender Result${name ? ` (${name})` : ''}:\n${prettyDOM(result.baseElement)}\n` ); }; diff --git a/ts/test/test-utils/utils/stubbing.ts b/ts/test/test-utils/utils/stubbing.ts index 5a3a95bf4..8a5f63a7b 100644 --- a/ts/test/test-utils/utils/stubbing.ts +++ b/ts/test/test-utils/utils/stubbing.ts @@ -155,10 +155,3 @@ export const stubStorage = () => { }, }; }; - -/** The crypto module from the browser is not accessible during tests so we use the NodeJS module instead */ -export const stubCrypto = async () => { - const nodeCrypto = await import('node:crypto'); - - return nodeCrypto; -}; diff --git a/ts/themes/constants/colors.tsx b/ts/themes/constants/colors.tsx index 296d6a574..ad7f68a7d 100644 --- a/ts/themes/constants/colors.tsx +++ b/ts/themes/constants/colors.tsx @@ -96,7 +96,6 @@ export type PrimaryColorStateType = type PrimaryColorType = { id: PrimaryColorStateType; ariaLabel: string; color: string }; -// NOTE: Make sure order matches COLORS.PRIMARY export const getPrimaryColors = (): Array => [ { id: 'green', ariaLabel: window.i18n('primaryColorGreen'), color: COLORS.PRIMARY.GREEN }, { id: 'blue', ariaLabel: window.i18n('primaryColorBlue'), color: COLORS.PRIMARY.BLUE }, diff --git a/ts/themes/switchPrimaryColor.tsx b/ts/themes/switchPrimaryColor.tsx index 67b660683..ca583739c 100644 --- a/ts/themes/switchPrimaryColor.tsx +++ b/ts/themes/switchPrimaryColor.tsx @@ -1,5 +1,5 @@ -import { find } from 'lodash'; import { Dispatch } from '@reduxjs/toolkit'; +import { find } from 'lodash'; import { applyPrimaryColor } from '../state/ducks/primaryColor'; import { COLORS, ColorsType, getPrimaryColors, PrimaryColorStateType } from './constants/colors';