diff --git a/.changeset/chilled-rice-listen.md b/.changeset/chilled-rice-listen.md deleted file mode 100644 index d8d3bada649..00000000000 --- a/.changeset/chilled-rice-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@aws-amplify/ui-react-liveness": patch ---- - -fix(liveness): Fix photosensitivity text customization diff --git a/.changeset/plenty-bears-live.md b/.changeset/plenty-bears-live.md new file mode 100644 index 00000000000..147af6a6c07 --- /dev/null +++ b/.changeset/plenty-bears-live.md @@ -0,0 +1,5 @@ +--- +"@aws-amplify/ui-react-liveness": patch +--- + +chore(deps): Update client-rekognitionstreaming sdk and custom fetch handler diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 366924fe6b2..0658cda8422 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,8 +22,8 @@ the requirements below. - [ ] Have read the [Pull Request Guidelines](https://github.com/aws-amplify/amplify-ui/blob/main/CONTRIBUTING.md) - [ ] PR description included -- [ ] Relevant documentation is changed or added (and PR referenced) - [ ] `yarn test` passes and tests are updated/added -- [ ] No side effects or [`sideEffects`](https://github.com/aws-amplify/amplify-ui/blob/main/packages/react/CONTRIBUTING.md#code-standards) field updated +- [ ] PR title and commit messages follow [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) syntax +- [ ] _If this change should result in a version bump_, [changeset added](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md) (This can be done after creating the PR.) This does not apply to changes made to `docs`, `e2e`, `examples`, or other private packages. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b47982f0534..5224fdd2228 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,8 +44,10 @@ You should open an issue to discuss your pull request, unless it's a trivial cha 1. [`nvm install`](https://github.com/nvm-sh/nvm) 1. [`nvm use`](https://github.com/nvm-sh/nvm) 1. `yarn setup` -1. Within your fork, create a new branch based on the issue you're addressing -- `git checkout -b angular/remove-browser-module` +1. Within your fork, create a new branch based on the issue you're addressing, e.g. `git checkout -b angular/remove-browser-module` +1. Commit your code using [conventional commit messages](https://www.conventionalcommits.org/en/v1.0.0/#summary), e.g. `git commit -m "chore: remove browser module"`. 1. Once your work is committed, validate your changes according to [local development guides](#local-development-guides). +1. If this is a change to any customer-facing aspect of a component, for example a new prop, feature, or a breaking change, update or add relevant documentation. If this is a large change, documentation updates can be made in a separate PR, but should be noted as a followup in the PR description. See the specific contributing guide for documentation [here](docs/README.md#contributing) 1. Push your branch with `git push origin -u` 1. Open a PR against this repo from your newly published branch. 1. Add a [changeset](https://github.com/changesets/changesets) that describes your changes. More info [here](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). Please make sure that your changeset only bumps `@aws-amplify/*` packages and does not bump any of private packages like `docs`, `e2e`, `examples`, etc. If you only updated a private package like `docs`, `e2e`, or `examples`, skip this step. diff --git a/build-system-tests/scripts/mega-app-copy-files.sh b/build-system-tests/scripts/mega-app-copy-files.sh index d1a68b4f366..56e6e603efa 100755 --- a/build-system-tests/scripts/mega-app-copy-files.sh +++ b/build-system-tests/scripts/mega-app-copy-files.sh @@ -73,6 +73,7 @@ if [ "$FRAMEWORK" == "react-native" ]; then AWS_EXPORTS_FILE="templates/template-react-native-aws-exports.js" else AWS_EXPORTS_FILE="templates/template-aws-exports.js" + AWS_EXPORTS_DECLARATION_FILE="templates/template-aws-exports.d.ts" fi echo "Installing json and strip-json-comments" @@ -117,6 +118,8 @@ fi if [[ "$FRAMEWORK" == 'react' && "$BUILD_TOOL" == 'vite' ]]; then echo "cp $AWS_EXPORTS_FILE mega-apps/${MEGA_APP_NAME}/src/aws-exports.js" cp $AWS_EXPORTS_FILE mega-apps/${MEGA_APP_NAME}/src/aws-exports.js + echo "cp $AWS_EXPORTS_DECLARATION_FILE mega-apps/${MEGA_APP_NAME}/src/aws-exports.d.ts" + cp $AWS_EXPORTS_DECLARATION_FILE mega-apps/${MEGA_APP_NAME}/src/aws-exports.d.ts echo "cp templates/components/react/vite/App.tsx mega-apps/${MEGA_APP_NAME}/src/App.tsx" cp templates/components/react/vite/App.tsx mega-apps/${MEGA_APP_NAME}/src/App.tsx @@ -124,8 +127,8 @@ if [[ "$FRAMEWORK" == 'react' && "$BUILD_TOOL" == 'vite' ]]; then # https://ui.docs.amplify.aws/react/getting-started/troubleshooting#vite echo "cp templates/components/react/vite/index.html mega-apps/${MEGA_APP_NAME}/index.html" cp templates/components/react/vite/index.html mega-apps/${MEGA_APP_NAME}/index.html - echo "cp templates/components/react/vite/template-tsconfig-vite-${BUILD_TOOL_VERSION}.json mega-apps/${MEGA_APP_NAME}/tsconfig.json" - cp templates/components/react/vite/template-tsconfig-vite-${BUILD_TOOL_VERSION}.json mega-apps/${MEGA_APP_NAME}/tsconfig.json + echo "cp templates/components/react/vite/template-tsconfig-vite-${BUILD_TOOL_VERSION}.json mega-apps/${MEGA_APP_NAME}/tsconfig.app.json" + cp templates/components/react/vite/template-tsconfig-vite-${BUILD_TOOL_VERSION}.json mega-apps/${MEGA_APP_NAME}/tsconfig.app.json echo "cp templates/components/react/vite/vite.config.ts mega-apps/${MEGA_APP_NAME}/vite.config.ts" cp templates/components/react/vite/vite.config.ts mega-apps/${MEGA_APP_NAME}/vite.config.ts fi @@ -166,10 +169,12 @@ if [[ "$FRAMEWORK" == 'vue' ]]; then cp templates/components/vue/App.vue mega-apps/${MEGA_APP_NAME}/src/App.vue echo "cp $AWS_EXPORTS_FILE mega-apps/${MEGA_APP_NAME}/src/aws-exports.js" cp $AWS_EXPORTS_FILE mega-apps/${MEGA_APP_NAME}/src/aws-exports.js + echo "cp $AWS_EXPORTS_DECLARATION_FILE mega-apps/${MEGA_APP_NAME}/src/aws-exports.d.ts" + cp $AWS_EXPORTS_DECLARATION_FILE mega-apps/${MEGA_APP_NAME}/src/aws-exports.d.ts # remove comments from JSON files because `json` package can't process comments - echo "npx strip-json-comments mega-apps/${MEGA_APP_NAME}/tsconfig.json >tmpfile && mv tmpfile mega-apps/${MEGA_APP_NAME}/tsconfig.json && rm -f tmpfile" - npx strip-json-comments mega-apps/${MEGA_APP_NAME}/tsconfig.json >tmpfile && mv tmpfile mega-apps/${MEGA_APP_NAME}/tsconfig.json && rm -f tmpfile + echo "npx strip-json-comments mega-apps/${MEGA_APP_NAME}/tsconfig.app.json >tmpfile && mv tmpfile mega-apps/${MEGA_APP_NAME}/tsconfig.app.json && rm -f tmpfile" + npx strip-json-comments mega-apps/${MEGA_APP_NAME}/tsconfig.app.json >tmpfile && mv tmpfile mega-apps/${MEGA_APP_NAME}/tsconfig.app.json && rm -f tmpfile # See Troubleshooting: https://ui.docs.amplify.aws/vue/getting-started/troubleshooting if [[ "$BUILD_TOOL" == 'vite' ]]; then @@ -188,8 +193,8 @@ if [[ "$FRAMEWORK" == 'vue' ]]; then npx json -I -f mega-apps/${MEGA_APP_NAME}/tsconfig.json -e "this.allowJs=true" else echo "add allowJs: true to tsconfig for aws-exports.js" - echo "npx json -I -f mega-apps/${MEGA_APP_NAME}/tsconfig.json -e \"this.compilerOptions.allowJs=true\"" - npx json -I -f mega-apps/${MEGA_APP_NAME}/tsconfig.json -e "this.compilerOptions.allowJs=true" + echo "npx json -I -f mega-apps/${MEGA_APP_NAME}/tsconfig.app.json -e \"this.compilerOptions.allowJs=true\"" + npx json -I -f mega-apps/${MEGA_APP_NAME}/tsconfig.app.json -e "this.compilerOptions.allowJs=true" fi fi diff --git a/build-system-tests/templates/components/react/vite/template-tsconfig-vite-latest.json b/build-system-tests/templates/components/react/vite/template-tsconfig-vite-latest.json index eee7fb8a65c..e4554777c5f 100644 --- a/build-system-tests/templates/components/react/vite/template-tsconfig-vite-latest.json +++ b/build-system-tests/templates/components/react/vite/template-tsconfig-vite-latest.json @@ -1,12 +1,9 @@ { "compilerOptions": { + "composite": true, "allowJs": true, "target": "ESNext", - "lib": [ - "DOM", - "DOM.Iterable", - "ESNext" - ], + "lib": ["DOM", "DOM.Iterable", "ESNext"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", @@ -20,12 +17,5 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": [ - "src" - ], - "references": [ - { - "path": "./tsconfig.node.json" - } - ] + "include": ["src"] } diff --git a/build-system-tests/templates/template-aws-exports.d.ts b/build-system-tests/templates/template-aws-exports.d.ts new file mode 100644 index 00000000000..40d2ce15655 --- /dev/null +++ b/build-system-tests/templates/template-aws-exports.d.ts @@ -0,0 +1,2 @@ +declare const awsmobile: Record +export default awsmobile; \ No newline at end of file diff --git a/examples/next/package.json b/examples/next/package.json index 9ca9d50a8c7..600a487e2f4 100644 --- a/examples/next/package.json +++ b/examples/next/package.json @@ -13,7 +13,7 @@ "@aws-amplify/geo": "3.0.31", "@aws-amplify/ui-react": "^6.1.12", "@aws-amplify/ui-react-geo": "^2.0.16", - "@aws-amplify/ui-react-liveness": "^3.0.23", + "@aws-amplify/ui-react-liveness": "^3.0.24", "@aws-amplify/ui-react-notifications": "^2.0.20", "@aws-amplify/ui-react-storage": "^3.1.3", "@aws-sdk/credential-providers": "^3.370.0", diff --git a/package.json b/package.json index 70eb46e534b..462a232db5f 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,8 @@ "tough-cookie": "4.1.3", "trim-newlines": "3.0.1", "webpack-dev-middleware": "^5.3.4", - "yaml": "2.2.2" + "yaml": "2.2.2", + "ws": "^8.17.1" }, "devDependencies": { "@aws-amplify/eslint-config-amplify-ui": "0.0.0", diff --git a/packages/react-core/src/hooks/__tests__/useDataState.spec.ts b/packages/react-core/src/hooks/__tests__/useDataState.spec.ts new file mode 100644 index 00000000000..7df93c08cb7 --- /dev/null +++ b/packages/react-core/src/hooks/__tests__/useDataState.spec.ts @@ -0,0 +1,63 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useDataState from '../useDataState'; + +const asyncAction = jest.fn((_prev: string, next: string) => + Promise.resolve(next) +); +const syncAction = jest.fn((_prev: string, next: string) => next); + +describe('useDataState', () => { + it.each([ + { type: 'async', action: asyncAction }, + { type: 'sync', action: syncAction }, + ])( + 'handles a $type action as expected in the happy path', + async ({ action }) => { + const initData = 'initial-data'; + const nextData = 'next-data'; + + const { result, waitForNextUpdate } = renderHook(() => + useDataState(action, 'initial-data') + ); + + // first render + const [initState, handleAction] = result.current; + + expect(action).not.toHaveBeenCalled(); + + expect(initState.data).toBe(initData); + expect(initState.isLoading).toBe(false); + expect(initState.message).toBeUndefined(); + + // call action + act(() => { + handleAction(nextData); + }); + + // loading result + const [loadingState] = result.current; + + expect(action).toHaveBeenCalledTimes(1); + expect(action).toHaveBeenCalledWith(initData, nextData); + + expect(loadingState.data).toBe(initData); + expect(loadingState.isLoading).toBe(true); + expect(loadingState.message).toBeUndefined(); + + await waitForNextUpdate(); + + // action complete + const [nextState] = result.current; + + expect(action).toHaveBeenCalledTimes(1); + expect(action).toHaveBeenCalledWith(initData, nextData); + + expect(nextState.data).toBe(nextData); + expect(nextState.isLoading).toBe(false); + expect(nextState.message).toBeUndefined(); + } + ); + + it.todo('only returns the values of the last call to handleAction'); + it.todo('handles exceptions thrown from provided action'); +}); diff --git a/packages/react-core/src/hooks/index.ts b/packages/react-core/src/hooks/index.ts index ca53c0bac5a..e9baad30fd3 100644 --- a/packages/react-core/src/hooks/index.ts +++ b/packages/react-core/src/hooks/index.ts @@ -1,3 +1,4 @@ +export { default as useDataState } from './useDataState'; export { default as useDeprecationWarning, UseDeprecationWarning, diff --git a/packages/react-core/src/hooks/useDataState.ts b/packages/react-core/src/hooks/useDataState.ts new file mode 100644 index 00000000000..935aac151f6 --- /dev/null +++ b/packages/react-core/src/hooks/useDataState.ts @@ -0,0 +1,42 @@ +import React from 'react'; + +interface ActionState { + data: T; + isLoading: boolean; + message: string | undefined; +} + +const getActionState = (data: T): ActionState => ({ + data, + isLoading: false, + message: undefined, +}); + +export default function useDataState( + action: (prevData: Awaited, ...input: K[]) => T | Promise, + initialData: Awaited +): [state: ActionState>, handleAction: (...input: K[]) => void] { + const [actionState, setActionState] = React.useState>>( + () => getActionState(initialData) + ); + + const prevData = React.useRef(initialData); + + const handleAction: (...input: K[]) => void = React.useCallback( + (...input) => { + setActionState((prev) => ({ ...prev, isLoading: true })); + + Promise.resolve(action(prevData.current, ...input)) + .then((data) => { + prevData.current = data; + setActionState(getActionState(data)); + }) + .catch(({ message }: Error) => { + setActionState((prev) => ({ ...prev, isLoading: false, message })); + }); + }, + [action] + ); + + return [actionState, handleAction]; +} diff --git a/packages/react-liveness/CHANGELOG.md b/packages/react-liveness/CHANGELOG.md index 7dbecda7d3a..a25bcb1e51a 100644 --- a/packages/react-liveness/CHANGELOG.md +++ b/packages/react-liveness/CHANGELOG.md @@ -1,5 +1,11 @@ # @aws-amplify/ui-react-liveness +## 3.0.24 + +### Patch Changes + +- [#5303](https://github.com/aws-amplify/amplify-ui/pull/5303) [`47fb5ef77`](https://github.com/aws-amplify/amplify-ui/commit/47fb5ef778847c11310e4200bbfed17e9edf250a) Thanks [@esauerbo](https://github.com/esauerbo)! - fix(liveness): Fix photosensitivity text customization + ## 3.0.23 ### Patch Changes diff --git a/packages/react-liveness/package.json b/packages/react-liveness/package.json index 9bc61bc0e32..fcb9a6cd37d 100644 --- a/packages/react-liveness/package.json +++ b/packages/react-liveness/package.json @@ -1,6 +1,6 @@ { "name": "@aws-amplify/ui-react-liveness", - "version": "3.0.23", + "version": "3.0.24", "main": "dist/index.js", "module": "dist/esm/index.mjs", "exports": { @@ -49,7 +49,7 @@ "dependencies": { "@aws-amplify/ui": "6.0.16", "@aws-amplify/ui-react": "6.1.12", - "@aws-sdk/client-rekognitionstreaming": "3.398.0", + "@aws-sdk/client-rekognitionstreaming": "3.600.0", "@aws-sdk/util-format-url": "^3.410.0", "@smithy/eventstream-serde-browser": "^2.0.4", "@smithy/fetch-http-handler": "^2.1.3", @@ -81,7 +81,7 @@ "name": "FaceLivenessDetector", "path": "dist/esm/index.mjs", "import": "{ FaceLivenessDetector }", - "limit": "280 kB" + "limit": "287 kB" } ] } diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__tests__/liveness.test.ts b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__tests__/liveness.test.ts index 40b5f35451b..db90860ebb5 100644 --- a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__tests__/liveness.test.ts +++ b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/__tests__/liveness.test.ts @@ -23,6 +23,7 @@ import { IlluminationState, LivenessErrorState, } from '../../types'; +import { SessionInformation } from '@aws-sdk/client-rekognitionstreaming'; const context = getMockContext(); @@ -314,7 +315,7 @@ describe('Liveness Helper', () => { }); it('should work even if there are no color sequences', async () => { - const mockSessionInfo = { + const mockSessionInfo: SessionInformation = { Challenge: { FaceMovementAndLightChallenge: { ChallengeConfig: { @@ -347,7 +348,7 @@ describe('Liveness Helper', () => { }); it('should not return values if color sequences do not contain durations', async () => { - const mockSessionInfo = { + const mockSessionInfo: SessionInformation = { Challenge: { FaceMovementAndLightChallenge: { ChallengeConfig: { diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/createStreamingClient/CustomWebSocketFetchHandler.ts b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/createStreamingClient/CustomWebSocketFetchHandler.ts index a7a2f0cbd6a..b2e9025dc93 100644 --- a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/createStreamingClient/CustomWebSocketFetchHandler.ts +++ b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/createStreamingClient/CustomWebSocketFetchHandler.ts @@ -85,7 +85,8 @@ export class CustomWebSocketFetchHandler { public readonly metadata: RequestHandlerMetadata = { handlerProtocol: 'websocket/h1.1', }; - private readonly configPromise: Promise; + private config: WebSocketFetchHandlerOptions; + private configPromise: Promise; private readonly httpHandler: RequestHandler; private readonly sockets: Record = {}; private readonly utf8decoder = new TextDecoder(); // default 'utf-8' or 'utf8' @@ -98,9 +99,11 @@ export class CustomWebSocketFetchHandler { ) { this.httpHandler = httpHandler; if (typeof options === 'function') { - this.configPromise = options().then((opts) => opts ?? {}); + this.config = {}; + this.configPromise = options().then((opts) => (this.config = opts ?? {})); } else { - this.configPromise = Promise.resolve(options ?? {}); + this.config = options ?? {}; + this.configPromise = Promise.resolve(this.config); } } @@ -146,14 +149,31 @@ export class CustomWebSocketFetchHandler { }; } + updateHttpClientConfig( + key: keyof WebSocketFetchHandlerOptions, + value: WebSocketFetchHandlerOptions[typeof key] + ): void { + this.configPromise = this.configPromise.then((config) => { + return { + ...config, + [key]: value, + }; + }); + } + + httpHandlerConfigs(): WebSocketFetchHandlerOptions { + return this.config ?? {}; + } + /** * Removes all closing/closed sockets from the socket pool for URL. */ private removeNotUsableSockets(url: string): void { this.sockets[url] = (this.sockets[url] ?? []).filter( (socket) => - ![WebSocket.CLOSING, WebSocket.CLOSED].includes( - socket.readyState as 2 | 3 + !( + socket.readyState === WebSocket.CLOSING || + socket.readyState === WebSocket.CLOSED ) ); } @@ -188,13 +208,7 @@ export class CustomWebSocketFetchHandler { // initialize as no-op. let reject: (err?: unknown) => void = () => {}; - let resolve: ({ - done, - value, - }: { - done: boolean; - value: Uint8Array; - }) => void = () => {}; + let resolve: (result: IteratorResult) => void = () => {}; socket.onmessage = (event) => { resolve({ @@ -218,7 +232,7 @@ export class CustomWebSocketFetchHandler { } else { resolve({ done: true, - value: undefined as any, // unchecked because done=true. + value: undefined, }); } }; @@ -246,7 +260,6 @@ export class CustomWebSocketFetchHandler { } continue; } - socket.send(inputChunk); } } catch (err) { @@ -254,7 +267,9 @@ export class CustomWebSocketFetchHandler { // would already be settled by the time sending chunk throws error. // Instead, the notify the output stream to throw if there's // exceptions - streamError = err as Error | undefined; + if (err instanceof Error) { + streamError = err; + } } finally { // WS status code: https://tools.ietf.org/html/rfc6455#section-7.4 socket.close(WS_CLOSURE_CODE.SUCCESS_CODE); diff --git a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/createStreamingClient/__tests__/CustomWebsocketFetchHandler.test.ts b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/createStreamingClient/__tests__/CustomWebsocketFetchHandler.test.ts index 0ea291df290..3491f3f93a9 100644 --- a/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/createStreamingClient/__tests__/CustomWebsocketFetchHandler.test.ts +++ b/packages/react-liveness/src/components/FaceLivenessDetector/service/utils/createStreamingClient/__tests__/CustomWebsocketFetchHandler.test.ts @@ -16,17 +16,17 @@ Object.defineProperty(window, 'TextDecoder', { value: TextDecoder, }); -describe(CustomWebSocketFetchHandler.name, () => { - const mockHostname = 'localhost:6789'; - const mockUrl = `ws://${mockHostname}/`; +const mockHostname = 'localhost:6789'; +const mockUrl = `ws://${mockHostname}/`; +describe(CustomWebSocketFetchHandler.name, () => { beforeEach(() => { jest.clearAllMocks(); }); describe('should handle WebSocket connections', () => { beforeEach(() => { - (global as any).WebSocket = WebSocket; + global.WebSocket = WebSocket; }); afterEach(() => { @@ -93,7 +93,7 @@ describe(CustomWebSocketFetchHandler.name, () => { }); it('should throw in output stream if input stream throws', async () => { - expect.assertions(3); + expect.assertions(2); const handler = new CustomWebSocketFetchHandler(); //Using Node stream is fine because they are also async iterables. const payload = new PassThrough(); @@ -109,19 +109,48 @@ describe(CustomWebSocketFetchHandler.name, () => { ); await server.connected; payload.emit('error', new Error('FakeError')); - try { + await expect(async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars for await (const chunk of responsePayload) { /** pass */ - console.log(chunk); - continue; } - } catch (err) { - expect(err).toBeDefined(); - expect((err as any).message).toEqual('FakeError'); - // @ts-expect-error Property 'sockets' is private and only accessible within class 'WebSocketHandler'. - expect(handler.sockets[mockUrl].length).toBe(0); - } + }).rejects.toThrow('FakeError'); + + // @ts-expect-error Property 'sockets' is private and only accessible within class 'WebSocketHandler'. + expect(handler.sockets[mockUrl].length).toBe(0); + }); + + it('should return timeout error if cannot setup ws connection', async () => { + const originalSetTimeout = globalThis.setTimeout; + + global.setTimeout = jest.fn( + (fn: (...args: any[]) => void, ms?: number, ...args: any[]) => { + return originalSetTimeout(fn, ms, ...args); + } + ) as unknown as typeof setTimeout; + + const connectionTimeout = 1000; + const handler = new CustomWebSocketFetchHandler(async () => ({ + connectionTimeout, + })); + //Using Node stream is fine because they are also async iterables. + const payload = new PassThrough(); + const mockInvalidHostname = 'localhost:9876'; + const mockInvalidUrl = `ws://${mockInvalidHostname}/`; + + await expect( + handler.handle( + new HttpRequest({ + body: payload, + hostname: mockInvalidHostname, //invalid websocket endpoint + protocol: 'ws:', + }) + ) + ).rejects.toThrow('Websocket connection timeout'); + + // @ts-expect-error Property 'sockets' is private and only accessible within class 'WebSocketHandler'. + expect(handler.sockets[mockInvalidUrl].length).toBe(0); + globalThis.setTimeout = originalSetTimeout; }); }); diff --git a/packages/react-liveness/src/version.ts b/packages/react-liveness/src/version.ts index cf372b58362..14c17101309 100644 --- a/packages/react-liveness/src/version.ts +++ b/packages/react-liveness/src/version.ts @@ -1 +1 @@ -export const VERSION = '3.0.23'; +export const VERSION = '3.0.24'; diff --git a/packages/react/src/context/elements/__tests__/createElementsContext.spec.tsx b/packages/react/src/context/elements/__tests__/createElementsContext.spec.tsx new file mode 100644 index 00000000000..94597dc5843 --- /dev/null +++ b/packages/react/src/context/elements/__tests__/createElementsContext.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; + +import createElementsContext from '../createElementsContext'; +import { ButtonElementBase, ViewElementBase } from '../defaultElements'; + +const elements = { Button: ButtonElementBase, View: ViewElementBase }; + +describe('createElementsContext', () => { + it('useElement exposes the expected elements', () => { + const { useElement } = createElementsContext(elements); + + const { + result: { current: Button }, + } = renderHook(() => useElement('Button')); + const { + result: { current: View }, + } = renderHook(() => useElement('View')); + + expect(Button).toBe(ButtonElementBase); + expect(View).toBe(ViewElementBase); + }); + + it('Passing `elements` ElementsProvider overrides the default `Elements`', () => { + const { ElementsProvider, useElement } = createElementsContext(elements); + + const OtherButton = () => <>Hi; + const OtherView = () => <>Hi; + const wrapper = (props: { children?: React.ReactNode }) => ( + + ); + + const { + result: { current: Button }, + } = renderHook(() => useElement('Button'), { wrapper }); + const { + result: { current: View }, + } = renderHook(() => useElement('View'), { wrapper }); + + expect(Button).not.toBe(ButtonElementBase); + expect(Button).toBe(OtherButton); + + expect(View).not.toBe(ViewElementBase); + expect(View).toBe(OtherView); + }); +}); diff --git a/packages/react/src/context/elements/createElementsContext.tsx b/packages/react/src/context/elements/createElementsContext.tsx new file mode 100644 index 00000000000..17d13bad84d --- /dev/null +++ b/packages/react/src/context/elements/createElementsContext.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { RenderNothing } from '@aws-amplify/ui-react-core'; +import { Elements, ElementsProviderProps } from './types'; + +interface CreateElementsContextResult { + ElementsProvider: (props: ElementsProviderProps) => JSX.Element; + useElement: (name: K) => T[K]; +} + +const createElementsContext = ( + defaultValue: T +): CreateElementsContextResult => { + const ElementsContext = React.createContext(defaultValue); + return { + ElementsProvider: ({ children, elements }): JSX.Element => ( + + {children} + + ), + useElement: (name) => + // fallback to `RenderNothing` on lookup fail + React.useContext(ElementsContext)?.[name] ?? RenderNothing, + }; +}; + +export default createElementsContext; diff --git a/packages/react/src/context/elements/defaultElements.tsx b/packages/react/src/context/elements/defaultElements.tsx new file mode 100644 index 00000000000..55a7f2e80dd --- /dev/null +++ b/packages/react/src/context/elements/defaultElements.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { ElementsBase } from './types'; + +export const ButtonElementBase: ElementsBase['Button'] = React.forwardRef( + ({ isDisabled, ...rest }, ref) => ( +