feat: test for correct background color order is done

use libsodium for hash so we can unit test, remove crypto stub
pull/3083/head
William Grant 11 months ago
parent 91d1229023
commit bc5615e880

@ -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<string, number>();
@ -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}

@ -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(
<AvatarPlaceHolder
diameter={AvatarSize.XL}
name={displayName}
pubkey={''}
pubkey={''} // makes the hash will be undefined
dataTestId="avatar-placeholder"
/>
);
@ -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(
<AvatarPlaceHolder
diameter={AvatarSize.XL}
name={displayName}
pubkey={testPubkey}
dataTestId="avatar-placeholder"
/>
);
// 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
});

@ -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`
);
};

@ -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;
};

@ -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<PrimaryColorType> => [
{ id: 'green', ariaLabel: window.i18n('primaryColorGreen'), color: COLORS.PRIMARY.GREEN },
{ id: 'blue', ariaLabel: window.i18n('primaryColorBlue'), color: COLORS.PRIMARY.BLUE },

@ -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';

Loading…
Cancel
Save