diff --git a/packages/ui/src/__mocks__/logto.tsx b/packages/ui/src/__mocks__/logto.tsx index 885e34394..5aba77c0c 100644 --- a/packages/ui/src/__mocks__/logto.tsx +++ b/packages/ui/src/__mocks__/logto.tsx @@ -288,3 +288,17 @@ export const mockSignInMethodSettingsTestCases: Array = [ }, ], ]; + +export const getBoundingClientRectMock = (mock: Partial) => + jest.fn(() => ({ + width: 0, + height: 0, + x: 0, + y: 0, + top: 0, + bottom: 0, + left: 0, + right: 0, + ...mock, + toJSON: jest.fn(), + })); diff --git a/packages/ui/src/components/InputFields/SmartInputField/AnimatedPrefix/index.tsx b/packages/ui/src/components/InputFields/SmartInputField/AnimatedPrefix/index.tsx index 3472efe11..b66bff101 100644 --- a/packages/ui/src/components/InputFields/SmartInputField/AnimatedPrefix/index.tsx +++ b/packages/ui/src/components/InputFields/SmartInputField/AnimatedPrefix/index.tsx @@ -1,7 +1,7 @@ import { onResize, useIsomorphicLayoutEffect } from '@react-spring/shared'; import { useSpring, animated, config } from '@react-spring/web'; import type { Nullable } from '@silverhand/essentials'; -import { cloneElement, useCallback, useRef } from 'react'; +import { cloneElement, useCallback, useRef, useState } from 'react'; import * as styles from './index.module.scss'; @@ -11,6 +11,7 @@ type Props = { }; const AnimatedPrefix = ({ children, isVisible }: Props) => { + const [domReady, setDomReady] = useState(false); const elementRef = useRef>(null); // Get target width for the children @@ -21,11 +22,14 @@ const AnimatedPrefix = ({ children, isVisible }: Props) => { const [animation, api] = useSpring( () => ({ width: getTargetWidth(), config: { ...config.default, clamp: true } }), - [getTargetWidth] + [getTargetWidth, domReady] ); useIsomorphicLayoutEffect(() => { const { current } = elementRef; + + setDomReady(true); + const cleanup = current && onResize( diff --git a/packages/ui/src/components/InputFields/SmartInputField/index.test.tsx b/packages/ui/src/components/InputFields/SmartInputField/index.test.tsx index dfb76a650..5456c5551 100644 --- a/packages/ui/src/components/InputFields/SmartInputField/index.test.tsx +++ b/packages/ui/src/components/InputFields/SmartInputField/index.test.tsx @@ -3,6 +3,7 @@ import { Globals } from '@react-spring/web'; import { assert } from '@silverhand/essentials'; import { fireEvent, render } from '@testing-library/react'; +import { getBoundingClientRectMock } from '@/__mocks__/logto'; import { getDefaultCountryCallingCode } from '@/utils/country-code'; import type { IdentifierInputType } from '.'; @@ -15,6 +16,7 @@ jest.mock('i18next', () => ({ describe('SmartInputField Component', () => { const onChange = jest.fn(); + const defaultCountryCallingCode = getDefaultCountryCallingCode(); const renderInputField = (props: { @@ -27,6 +29,11 @@ describe('SmartInputField Component', () => { Globals.assign({ skipAnimation: true, }); + + // eslint-disable-next-line @silverhand/fp/no-mutation + window.HTMLDivElement.prototype.getBoundingClientRect = getBoundingClientRectMock({ + width: 100, + }); }); afterEach(() => { @@ -65,10 +72,7 @@ describe('SmartInputField Component', () => { const countryCode = queryAllByText(`+${defaultCountryCallingCode}`); expect(countryCode).toHaveLength(2); - // Country code select should have a >0 width. - // The React Spring acquires the child element's width ahead of elementRef is properly set. - // So the value returns null. Assert style is null to represent the width is >0. - expect(queryByTestId('prefix')?.getAttribute('style')).toBe(null); + expect(queryByTestId('prefix')?.style.width).toBe('100px'); const selector = container.querySelector('select'); assert(selector, new Error('selector should not be null')); diff --git a/packages/ui/src/pages/ForgotPassword/index.test.tsx b/packages/ui/src/pages/ForgotPassword/index.test.tsx index c0612a6a1..feeb9715f 100644 --- a/packages/ui/src/pages/ForgotPassword/index.test.tsx +++ b/packages/ui/src/pages/ForgotPassword/index.test.tsx @@ -5,7 +5,7 @@ import { MemoryRouter, useLocation } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; -import { mockSignInExperienceSettings } from '@/__mocks__/logto'; +import { mockSignInExperienceSettings, getBoundingClientRectMock } from '@/__mocks__/logto'; import type { SignInExperienceResponse } from '@/types'; import ForgotPassword from '.'; @@ -43,6 +43,11 @@ describe('ForgotPassword', () => { Globals.assign({ skipAnimation: true, }); + + // eslint-disable-next-line @silverhand/fp/no-mutation + window.HTMLDivElement.prototype.getBoundingClientRect = getBoundingClientRectMock({ + width: 100, + }); }); afterEach(() => { @@ -79,6 +84,7 @@ describe('ForgotPassword', () => { const { queryByText, queryAllByText, container, queryByTestId } = renderPage(settings); const inputField = container.querySelector('input[name="identifier"]'); const countryCodeSelectorPrefix = queryByTestId('prefix'); + assert(inputField, new Error('input field not found')); expect(queryByText('description.reset_password')).not.toBeNull(); @@ -88,12 +94,7 @@ describe('ForgotPassword', () => { if (state.identifier === SignInIdentifier.Phone && settings.phone) { expect(inputField.getAttribute('value')).toBe(phone); - - // Country code select should have a >0 width. - // The React Spring acquires the child element's width ahead of elementRef is properly set. - // So the value returns null. Assert style is null to represent the width is >0. - expect(countryCodeSelectorPrefix?.getAttribute('style')).toBeNull(); - + expect(countryCodeSelectorPrefix?.style.width).toBe('100px'); expect(queryAllByText(`+${countryCode}`)).toHaveLength(2); } else if (state.identifier === SignInIdentifier.Phone) { // Phone Number not enabled @@ -107,7 +108,7 @@ describe('ForgotPassword', () => { } else if (state.identifier === SignInIdentifier.Email) { // Only PhoneNumber is enabled expect(inputField.getAttribute('value')).toBe(''); - expect(countryCodeSelectorPrefix?.getAttribute('style')).toBeNull(); + expect(countryCodeSelectorPrefix?.style.width).toBe('100px'); } if (state.identifier === SignInIdentifier.Username && settings.email) { @@ -116,7 +117,7 @@ describe('ForgotPassword', () => { } else if (state.identifier === SignInIdentifier.Username) { // Only PhoneNumber is enabled expect(inputField.getAttribute('value')).toBe(''); - expect(countryCodeSelectorPrefix?.getAttribute('style')).toBeNull(); + expect(countryCodeSelectorPrefix?.style.width).toBe('100px'); } }); });