From afbacb74a78e43e0838e1243f0112f5791bb6326 Mon Sep 17 00:00:00 2001 From: William Lee <43682783+wlee221@users.noreply.github.com> Date: Mon, 17 Jul 2023 12:24:47 -0700 Subject: [PATCH] refactor(vue): refactor Authenticator (#4246) --- packages/vue/.eslintrc.cjs | 2 + packages/vue/global-spec.ts | 4 +- packages/vue/jest.config.cjs | 14 +- .../__snapshots__/authenticator.spec.ts.snap | 1390 +++++++++++++++++ .../confirm-reset-password.spec.ts.snap | 283 ++++ .../confirm-sign-in.spec.ts.snap | 287 ++++ .../confirm-sign-up.spec.ts.snap | 152 ++ .../confirm-verify-user.spec.ts.snap | 143 ++ .../federated-sign-in-button.spec.ts.snap | 30 + .../federated-sign-in.spec.ts.snap | 251 +++ .../force-new-password.spec.ts.snap | 286 ++++ .../__snapshots__/reset-password.spec.ts.snap | 140 ++ .../__snapshots__/setup-totp.spec.ts.snap | 372 +++++ .../__snapshots__/sign-in.spec.ts.snap | 216 +++ .../__snapshots__/sign-up.spec.ts.snap | 309 ++++ .../__snapshots__/verify-user.spec.ts.snap | 146 ++ .../__tests__/alias-control.spec.ts | 55 +- .../__tests__/authenticator.spec.ts | 141 +- .../__tests__/confirm-reset-password.spec.ts | 180 +++ .../__tests__/confirm-sign-in.spec.ts | 141 ++ .../__tests__/confirm-sign-up.spec.ts | 137 ++ .../__tests__/confirm-verify-user.spec.ts | 136 ++ .../federated-sign-in-button.spec.ts | 62 + .../__tests__/federated-sign-in.spec.ts | 86 + .../__tests__/force-new-password.spec.ts | 189 +++ .../__tests__/password-control.spec.ts | 6 +- .../__tests__/reset-password.spec.ts | 122 ++ .../components/__tests__/setup-totp.spec.ts | 231 +++ .../src/components/__tests__/sign-in.spec.ts | 133 ++ .../src/components/__tests__/sign-up.spec.ts | 205 +++ .../components/__tests__/verify-user.spec.ts | 134 ++ packages/vue/src/components/authenticator.vue | 122 +- .../src/components/confirm-reset-password.vue | 49 +- .../vue/src/components/confirm-sign-in.vue | 60 +- .../vue/src/components/confirm-sign-up.vue | 13 +- .../src/components/confirm-verify-user.vue | 50 +- .../components/federated-sign-in-button.vue | 32 +- .../vue/src/components/federated-sign-in.vue | 15 +- .../vue/src/components/force-new-password.vue | 61 +- .../vue/src/components/reset-password.vue | 36 +- packages/vue/src/components/setup-totp.vue | 65 +- packages/vue/src/components/sign-in.vue | 58 +- .../src/components/sign-up-email-control.vue | 13 - packages/vue/src/components/sign-up.vue | 38 +- packages/vue/src/components/verify-user.vue | 54 +- .../__mock__/useAuthenticatorMock.ts | 29 + .../composables/__tests__/getQRFields.spec.ts | 46 + .../__tests__/useAuthenticator.spec.ts | 28 +- packages/vue/src/composables/useAuth.ts | 41 +- packages/vue/src/composables/useUtils.ts | 33 - packages/vue/src/types/index.ts | 11 +- 51 files changed, 6398 insertions(+), 439 deletions(-) create mode 100644 packages/vue/src/components/__tests__/__snapshots__/authenticator.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/confirm-reset-password.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/confirm-sign-in.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/confirm-sign-up.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/confirm-verify-user.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/federated-sign-in-button.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/federated-sign-in.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/force-new-password.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/reset-password.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/setup-totp.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/sign-in.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/sign-up.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/__snapshots__/verify-user.spec.ts.snap create mode 100644 packages/vue/src/components/__tests__/confirm-reset-password.spec.ts create mode 100644 packages/vue/src/components/__tests__/confirm-sign-in.spec.ts create mode 100644 packages/vue/src/components/__tests__/confirm-sign-up.spec.ts create mode 100644 packages/vue/src/components/__tests__/confirm-verify-user.spec.ts create mode 100644 packages/vue/src/components/__tests__/federated-sign-in-button.spec.ts create mode 100644 packages/vue/src/components/__tests__/federated-sign-in.spec.ts create mode 100644 packages/vue/src/components/__tests__/force-new-password.spec.ts create mode 100644 packages/vue/src/components/__tests__/reset-password.spec.ts create mode 100644 packages/vue/src/components/__tests__/setup-totp.spec.ts create mode 100644 packages/vue/src/components/__tests__/sign-in.spec.ts create mode 100644 packages/vue/src/components/__tests__/sign-up.spec.ts create mode 100644 packages/vue/src/components/__tests__/verify-user.spec.ts delete mode 100644 packages/vue/src/components/sign-up-email-control.vue create mode 100644 packages/vue/src/composables/__mock__/useAuthenticatorMock.ts create mode 100644 packages/vue/src/composables/__tests__/getQRFields.spec.ts delete mode 100644 packages/vue/src/composables/useUtils.ts diff --git a/packages/vue/.eslintrc.cjs b/packages/vue/.eslintrc.cjs index c69d73abf81..ec0eef44c5b 100644 --- a/packages/vue/.eslintrc.cjs +++ b/packages/vue/.eslintrc.cjs @@ -24,6 +24,8 @@ module.exports = { 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', '@typescript-eslint/ban-ts-comment': 'off', 'vue/script-setup-uses-vars': 'error', + // we intentionally use non-null assertion where types are inaccurate. + '@typescript-eslint/no-non-null-assertion': 'off', }, overrides: [ { diff --git a/packages/vue/global-spec.ts b/packages/vue/global-spec.ts index d14d6db72c6..4ac6eff38d1 100644 --- a/packages/vue/global-spec.ts +++ b/packages/vue/global-spec.ts @@ -1,11 +1,12 @@ import BaseAlert from './src/components/primitives/base-alert.vue'; -import BaseSelect from './src/components/primitives/base-select.vue'; import BaseFieldSet from './src/components/primitives/base-field-set.vue'; import BaseFooter from './src/components/primitives/base-footer.vue'; import BaseForm from './src/components/primitives/base-form.vue'; import BaseHeading from './src/components/primitives/base-heading.vue'; import BaseInput from './src/components/primitives/base-input.vue'; import BaseLabel from './src/components/primitives/base-label.vue'; +import BaseSelect from './src/components/primitives/base-select.vue'; +import BaseText from './src/components/primitives/base-text.vue'; import BaseTwoTabItem from './src/components/primitives/base-two-tab-item.vue'; import BaseTwoTabs from './src/components/primitives/base-two-tabs.vue'; import BaseWrapper from './src/components/primitives/base-wrapper.vue'; @@ -21,6 +22,7 @@ export const components = { BaseInput, BaseLabel, BaseSelect, + BaseText, BaseTwoTabItem, BaseTwoTabs, BaseWrapper, diff --git a/packages/vue/jest.config.cjs b/packages/vue/jest.config.cjs index 138921c0f33..5036e1675a8 100644 --- a/packages/vue/jest.config.cjs +++ b/packages/vue/jest.config.cjs @@ -1,6 +1,10 @@ module.exports = { collectCoverage: true, - collectCoverageFrom: ['/src/**/*.(ts|vue)'], + collectCoverageFrom: [ + '/src/**/*.(ts|vue)', + // ignore ___mock__ directories + '!/**/__mock__/*', + ], coveragePathIgnorePatterns: [ // ignore coverage for subdirectories' index files '/src/(components|composables|types)/index.ts', @@ -9,10 +13,10 @@ module.exports = { ], coverageThreshold: { global: { - branches: 22, - functions: 22, - lines: 46, - statements: 45, + branches: 89, + functions: 90, + lines: 93, + statements: 93, }, }, testEnvironment: 'jsdom', diff --git a/packages/vue/src/components/__tests__/__snapshots__/authenticator.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/authenticator.spec.ts.snap new file mode 100644 index 00000000000..2079a1eba6a --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/authenticator.spec.ts.snap @@ -0,0 +1,1390 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`authenticator renders confirmResetPassword subcomponent 1`] = ` +
+ +
+
+ + +
+ + + + + +
+ + +
+ + +
+ + +

+ +

+ +
+ + + + +
+ +
+ + + + + + + + + + + +
+ + +
+ + +
+ + +
+ + + + + + +
+ + +
+
+ + + +
+`; + +exports[`authenticator renders confirmSignIn subcomponent 1`] = ` +
+ +
+
+ + +
+ + + + + + +
+ + +
+ + +
+ + +

+ +

+ +
+ + + + +
+ +
+ + + + + + + + + + + +
+ + +
+ + +
+ + +
+ + + + + +
+ + +
+
+ + + +
+`; + +exports[`authenticator renders confirmSignUp subcomponent 1`] = ` +
+ +
+
+ + +
+ + + +
+ + +
+ +
+ + +

+ +

+ + + + + undefined. undefined. + + + + +
+ + + + +
+ + +
+ + + + + + + + + + + +
+ + +
+ +
+ + +
+ + + + + + + + +
+ + +
+
+ + + +
+`; + +exports[`authenticator renders confirmVerifyUser subcomponent 1`] = ` +
+ +
+
+ + +
+ + + + + + + + + + +
+ + +
+ + +
+ + +

+ +

+ +
+ + + + +
+ +
+ + + + + + + + + + + +
+ + +
+ + +
+ + +
+ +
+ + +
+
+ + + +
+`; + +exports[`authenticator renders forceNewPassword subcomponent 1`] = ` +
+ +
+
+ + +
+ + + + + + + + +
+ + +
+ + +
+ + +

+ +

+ +
+ + + + + + +
+ +
+ + + + + + + + + + + +
+ + +
+ + +
+ + +
+ + + +
+ + +
+
+ + + +
+`; + +exports[`authenticator renders resetPassword subcomponent 1`] = ` +
+ +
+
+ + +
+ + + + + +
+ +
+ + +

+ +

+ + +
+ + + + +
+ + +
+ + + + + + + + + + + +
+ + +
+ +
+ + + + + + + + +
+ + +
+
+ + + +
+`; + +exports[`authenticator renders setupTOTP subcomponent 1`] = ` +
+ +
+
+ + +
+ + + + + + + +
+ + +
+ + +
+ +
+ + +

+ Setup TOTP +

+ +
+ +

+ Loading... +

+
+ +
+ totp-mock-secret-code +
+
+ +
+ + + + +
+ +
+ + + +
+ +
+ + + + + + + + + + + +
+ + +
+ +
+ + +
+ + +
+ + + + +
+ + +
+
+ + + +
+`; + +exports[`authenticator renders signIn subcomponent 1`] = ` +
+ +
+
+ + +
+
+ +
+ + + + + + +
+ +
+
+ + + +
+ + +
+ + +
+ + +
+ + + Sign in + + + + +
+ + + + + + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + +
+ + + +
+ + + + + + + + +
+ + +
+
+ + + +
+`; + +exports[`authenticator renders signUp subcomponent 1`] = ` +
+ +
+
+ + +
+
+ +
+ + + + + + +
+ +
+
+ + + + +
+ + +
+ + +
+ + +
+ + + + +
+ + + + + + +
+ +
+ + +
+ +
+ + +
+ + +
+ + + + + + + + +
+ + +
+
+ + + +
+`; + +exports[`authenticator renders verifyUser subcomponent 1`] = ` +
+ +
+
+ + +
+ + + + + + + + + +
+ + +
+ + +
+ + +

+ +

+ +
+ + + +
+ + + + + +
+ +
+ +
+ + + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + + +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/confirm-reset-password.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/confirm-reset-password.spec.ts.snap new file mode 100644 index 00000000000..1896e8aebbd --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/confirm-reset-password.spec.ts.snap @@ -0,0 +1,283 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConfirmResetPassword renders as expected 1`] = ` +
+ +
+ + +
+ + +
+ + +

+ Reset Password +

+ +
+ + + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + +
+ + +
+ +
+ + + +
+
+ + + +
+ +
+ +
+ + + + + +
+ + +
+ +
+ + + +
+
+ + + +
+ +
+ +
+ + + + + +
+ +
+ + + + + + + + + + + +
+ + +
+ + +
+ + +
+ +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/confirm-sign-in.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/confirm-sign-in.spec.ts.snap new file mode 100644 index 00000000000..6c7f1a4c153 --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/confirm-sign-in.spec.ts.snap @@ -0,0 +1,287 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConfirmSignIn renders as expected for SMS challenge 1`] = ` +
+ +
+ + +
+ + +
+ + +

+ Confirm SMS Code +

+ +
+ + + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + +
+ + +
+ + +
+ + +
+ +
+`; + +exports[`ConfirmSignIn renders as expected for TOTP challenge 1`] = ` +
+ +
+ + +
+ + +
+ + +

+ Confirm TOTP Code +

+ +
+ + + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + +
+ + +
+ + +
+ + +
+ +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/confirm-sign-up.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/confirm-sign-up.spec.ts.snap new file mode 100644 index 00000000000..e02cef11fb7 --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/confirm-sign-up.spec.ts.snap @@ -0,0 +1,152 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConfirmSignUp renders as expected 1`] = ` +
+ +
+ + +
+ +
+ + +

+ We Texted You +

+ + + + + Your code is on the way. To log in, enter the code we sent you. It may take a minute to arrive. + + + + +
+ + + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + +
+ + +
+ + + + + + + + + + + +
+ + +
+ +
+ + +
+ +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/confirm-verify-user.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/confirm-verify-user.spec.ts.snap new file mode 100644 index 00000000000..9ee6039f3f5 --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/confirm-verify-user.spec.ts.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConfirmVerifyUser renders as expected 1`] = ` +
+ +
+ + +
+ + +
+ + +

+ Account recovery requires verified contact information +

+ +
+ + + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + +
+ + +
+ + +
+ + +
+ +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/federated-sign-in-button.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/federated-sign-in-button.spec.ts.snap new file mode 100644 index 00000000000..83937358900 --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/federated-sign-in-button.spec.ts.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FederatedSignInButton renders as expected 1`] = ` +
+ + + +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/federated-sign-in.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/federated-sign-in.spec.ts.snap new file mode 100644 index 00000000000..3e67d7660e8 --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/federated-sign-in.spec.ts.snap @@ -0,0 +1,251 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FederatedSignIn renders as expected with amazon provider 1`] = ` +
+ +
+`; + +exports[`FederatedSignIn renders as expected with apple provider 1`] = ` +
+ +
+`; + +exports[`FederatedSignIn renders as expected with facebook provider 1`] = ` +
+ +
+`; + +exports[`FederatedSignIn renders as expected with google provider 1`] = ` +
+ +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/force-new-password.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/force-new-password.spec.ts.snap new file mode 100644 index 00000000000..6e55427f9f9 --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/force-new-password.spec.ts.snap @@ -0,0 +1,286 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConfirmResetPassword renders as expected 1`] = ` +
+ +
+ + +
+ + +
+ + +

+ Change Password +

+ +
+ + + + + +
+ + +
+ +
+ + + +
+
+ + + +
+ +
+ +
+ + + + + +
+ + +
+ +
+ + + +
+
+ + + +
+ +
+ +
+ + + + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + + +
+ +
+ + + + + + + + + + + +
+ + +
+ + +
+ + +
+ +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/reset-password.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/reset-password.spec.ts.snap new file mode 100644 index 00000000000..dbf9ae06c95 --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/reset-password.spec.ts.snap @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ResetPassword renders as expected 1`] = ` +
+ + +
+ +
+ + +

+ Reset Password +

+ + +
+ + + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + +
+ + +
+ + + + + + + + + + + +
+ + +
+ +
+ + +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/setup-totp.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/setup-totp.spec.ts.snap new file mode 100644 index 00000000000..2e1dff50ac1 --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/setup-totp.spec.ts.snap @@ -0,0 +1,372 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SetupTOTP renders loading text as expected on init 1`] = ` +
+ +
+ + +
+ + +
+ +
+ + +

+ Setup TOTP +

+ +
+ +

+ Loading... +

+
+ +
+ totp-mock-secret-code +
+
+ +
+ COPY +
+ + + + +
+ +
+ + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + +
+ + +
+ +
+ + +
+ + +
+ +
+`; + +exports[`SetupTOTP renders qrcode image as expected after onMounted is done 1`] = ` +
+ +
+ + +
+ + +
+ +
+ + +

+ Setup TOTP +

+ +
+ + qr code +
+ +
+ totp-mock-secret-code +
+
+ +
+ COPY +
+ + + + +
+ +
+ + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + +
+ + +
+ +
+ + +
+ + +
+ +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/sign-in.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/sign-in.spec.ts.snap new file mode 100644 index 00000000000..329802bca7b --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/sign-in.spec.ts.snap @@ -0,0 +1,216 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SignIn renders as expected 1`] = ` +
+ + + +
+ + +
+ + +
+ + +
+ + + Sign in + + + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + +
+ + +
+ +
+ + + +
+
+ + + +
+ +
+ +
+ + + + + +
+ + + + + + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + +
+ + +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/sign-up.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/sign-up.spec.ts.snap new file mode 100644 index 00000000000..3e3cb0aacd9 --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/sign-up.spec.ts.snap @@ -0,0 +1,309 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SignUp renders as expected 1`] = ` +
+ + + +
+ + +
+ + +
+ + +
+ + + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + +
+ + +
+ +
+ + + +
+
+ + + +
+ +
+ +
+ + + + + +
+ + +
+ +
+ + + +
+
+ + + +
+ +
+ +
+ + + + + + + +
+ + +
+ +
+ + +
+
+ + + + +
+ +
+ +
+ + + + + + +
+ + + + + + +
+ +
+ + +
+ +
+ + +
+ + +
+`; diff --git a/packages/vue/src/components/__tests__/__snapshots__/verify-user.spec.ts.snap b/packages/vue/src/components/__tests__/__snapshots__/verify-user.spec.ts.snap new file mode 100644 index 00000000000..c69b03c9914 --- /dev/null +++ b/packages/vue/src/components/__tests__/__snapshots__/verify-user.spec.ts.snap @@ -0,0 +1,146 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VerifyUser renders as expected 1`] = ` +
+ +
+ + +
+ + +
+ + +

+ Account recovery requires verified contact information +

+ +
+ + + +
+ + + + + +
+ +
+ +
+ + + + + + + + + + + +
+ + +
+ + +
+ + +
+ +
+`; diff --git a/packages/vue/src/components/__tests__/alias-control.spec.ts b/packages/vue/src/components/__tests__/alias-control.spec.ts index 8745611dcba..3eaca119db9 100644 --- a/packages/vue/src/components/__tests__/alias-control.spec.ts +++ b/packages/vue/src/components/__tests__/alias-control.spec.ts @@ -1,7 +1,8 @@ -import AliasControl from '../alias-control.vue'; -import { components } from '../../../global-spec'; import { render, screen } from '@testing-library/vue'; +import { components } from '../../../global-spec'; +import AliasControl from '../alias-control.vue'; + describe('AliasControl', () => { it('renders a label with default class', async () => { render(AliasControl, { @@ -48,6 +49,56 @@ describe('AliasControl', () => { }); const field = await screen.findByRole('textbox'); expect(field).toHaveClass('amplify-input', 'amplify-field-group__control'); + expect(field).toHaveAttribute('type', 'text'); + }); + + it('renders phone number field with default class and attributes', async () => { + render(AliasControl, { + global: { + components, + }, + props: { + label: 'Phone Number', + name: 'phone_number', + placeholder: 'Phone Number', + type: 'tel', + }, + }); + const field = await screen.findByLabelText('Phone Number'); + expect(field).toHaveAttribute('type', 'tel'); + }); + + it('does not add required attribute if field is optional', async () => { + render(AliasControl, { + global: { + components, + }, + props: { + label: 'Username', + name: 'username', + placeholder: 'Username', + required: false, + }, + }); + const field = await screen.findByLabelText('Username'); + expect(field).not.toHaveAttribute('required'); + }); + + it('hides label if labelHidden is true', async () => { + render(AliasControl, { + global: { + components, + }, + props: { + label: 'Enter your Username', + name: 'username', + placeholder: 'Username', + labelHidden: true, + }, + }); + expect(screen.queryByText('Enter your Username')).toHaveClass( + 'amplify-visually-hidden' + ); }); it('should add aria-invalid attribute to text field when hasError is true', async () => { diff --git a/packages/vue/src/components/__tests__/authenticator.spec.ts b/packages/vue/src/components/__tests__/authenticator.spec.ts index 598058ce52d..5a761b475f8 100644 --- a/packages/vue/src/components/__tests__/authenticator.spec.ts +++ b/packages/vue/src/components/__tests__/authenticator.spec.ts @@ -1,53 +1,44 @@ import { reactive, Ref, ref } from 'vue'; -import { render, screen } from '@testing-library/vue'; +import { cleanup, render, screen } from '@testing-library/vue'; +import * as UIModule from '@aws-amplify/ui'; import { - AuthenticatorServiceFacade, + AmplifyUser, + AuthenticatorRoute, AuthEvent, AuthInterpreter, AuthMachineState, } from '@aws-amplify/ui'; import { components } from '../../../global-spec'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; import * as UseAuthComposables from '../../composables/useAuth'; import Authenticator from '../authenticator'; // mock `aws-amplify` to prevent logging auth errors during test runs jest.mock('aws-amplify'); -const mockServiceFacade: AuthenticatorServiceFacade = { - authStatus: 'authenticated', - codeDeliveryDetails: {} as AuthenticatorServiceFacade['codeDeliveryDetails'], - error: undefined as unknown as AuthenticatorServiceFacade['error'], - hasValidationErrors: false, - isPending: false, - route: 'idle', - socialProviders: [], - unverifiedContactMethods: { email: 'test#example.com' }, - user: {} as AuthenticatorServiceFacade['user'], - validationErrors: - undefined as unknown as AuthenticatorServiceFacade['validationErrors'], - totpSecretCode: null, - initializeMachine: jest.fn(), - resendCode: jest.fn(), - signOut: jest.fn(), - submitForm: jest.fn(), - updateForm: jest.fn(), - updateBlur: jest.fn(), - toFederatedSignIn: jest.fn(), - toResetPassword: jest.fn(), - toSignIn: jest.fn(), - toSignUp: jest.fn(), - skipVerification: jest.fn(), -}; +const routesWithComponent: AuthenticatorRoute[] = [ + 'confirmResetPassword', + 'confirmSignIn', + 'confirmSignUp', + 'confirmVerifyUser', + 'forceNewPassword', + 'resetPassword', + 'setupTOTP', + 'signIn', + 'signUp', + 'verifyUser', +]; + +const unsubscribeSpy = jest.fn(); class MockAuthService { public listeners: ((state: AuthMachineState) => void)[] = []; subscribe(callback: (state: AuthMachineState) => void) { this.listeners.push(callback); - const unsubscribe = jest.fn(); - return { unsubscribe }; + return { unsubscribe: unsubscribeSpy }; } start() { @@ -72,30 +63,28 @@ const setupState = { const mockStateRef = ref(idleState) as unknown as Ref; const sendSpy = jest.fn(); -const useAuthSpy = jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ authStatus: ref('unauthenticated'), state: mockStateRef, send: sendSpy, service: mockService, }); + const useAuthenticatorSpy = jest .spyOn(UseAuthComposables, 'useAuthenticator') - .mockReturnValue(reactive(mockServiceFacade)); + .mockReturnValue(reactive(baseMockServiceFacade)); + +jest.spyOn(UIModule, 'getSortedFormFields').mockReturnValue([]); describe('authenticator', () => { beforeEach(() => { mockService['listeners'] = []; - useAuthSpy.mockClear(); - useAuthenticatorSpy.mockClear(); + jest.clearAllMocks(); sendSpy.mockClear(); }); it('initializes the machine as expected', () => { - render(Authenticator, { - global: { - components, - }, - }); + render(Authenticator, { global: { components } }); expect(mockService['listeners'].length === 1); const listener = mockService['listeners'][0]; @@ -116,7 +105,7 @@ describe('authenticator', () => { }); }); - it('initializes with Authenticator props', () => { + it('initializes state machine with Authenticator props', () => { const props = { formFields: {}, initialState: 'signIn', @@ -125,12 +114,21 @@ describe('authenticator', () => { signUpAttributes: ['phone_number'], socialProviders: ['facebook'], }; - render(Authenticator, { - global: { - components, - }, - props, + render(Authenticator, { global: { components }, props }); + + const listener = mockService['listeners'][0]; + listener(setupState); + + expect(sendSpy).toBeCalledTimes(1); + expect(sendSpy).toHaveBeenCalledWith({ + type: 'INIT', + data: props, }); + }); + + it('initializes state machine with empty Authenticator props', () => { + const props = {}; + render(Authenticator, { global: { components }, props }); const listener = mockService['listeners'][0]; listener(setupState); @@ -142,10 +140,17 @@ describe('authenticator', () => { }); }); + it('unsubscribes hub after Authenticator unmounts', () => { + render(Authenticator, { global: { components } }); + cleanup(); + + expect(unsubscribeSpy).toHaveBeenCalled(); + }); + it('renders default slot if route is authenticated', () => { useAuthenticatorSpy.mockReturnValue( reactive({ - ...mockServiceFacade, + ...baseMockServiceFacade, route: 'authenticated', }) ); @@ -153,14 +158,48 @@ describe('authenticator', () => { const defaultContent = 'Default Slot'; render(Authenticator, { - global: { - components, - }, - slots: { - default: defaultContent, - }, + global: { components }, + slots: { default: defaultContent }, }); expect(screen.getByText(defaultContent)).toBeInTheDocument(); }); + + it.each(routesWithComponent)('renders %s subcomponent', (route) => { + let user = undefined as unknown as AmplifyUser; + + // some routes expect specific shape of user + if (route === 'confirmSignIn') { + user = { challengeName: 'SOFTWARE_TOKEN_MFA' } as AmplifyUser; + } else if (route === 'setupTOTP') { + user = { username: 'username' } as AmplifyUser; + } + + useAuthenticatorSpy.mockReturnValue( + reactive({ + ...baseMockServiceFacade, + route, + user, + totpSecretCode: + route === 'setupTOTP' ? 'totp-mock-secret-code' : undefined, + }) + ); + + const { container } = render(Authenticator, { global: { components } }); + expect(container).toMatchSnapshot(); + }); + + it('hides sign up tab if hideSignUp is true', async () => { + useAuthenticatorSpy.mockReturnValue( + reactive({ + ...baseMockServiceFacade, + route: 'signIn', + }) + ); + + const props = { hideSignUp: true }; + render(Authenticator, { global: { components }, props }); + + expect(screen.queryByText('Sign Up')).not.toBeInTheDocument(); + }); }); diff --git a/packages/vue/src/components/__tests__/confirm-reset-password.spec.ts b/packages/vue/src/components/__tests__/confirm-reset-password.spec.ts new file mode 100644 index 00000000000..43b7b6b8232 --- /dev/null +++ b/packages/vue/src/components/__tests__/confirm-reset-password.spec.ts @@ -0,0 +1,180 @@ +import { reactive, Ref, ref } from 'vue'; +import { fireEvent, render, screen } from '@testing-library/vue'; + +import * as UIModule from '@aws-amplify/ui'; +import { AuthInterpreter, AuthMachineState } from '@aws-amplify/ui'; + +import { components } from '../../../global-spec'; +import * as UseAuthComposables from '../../composables/useAuth'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import { UseAuthenticator } from '../../types'; +import ConfirmResetPassword from '../confirm-reset-password.vue'; + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +const updateBlurSpy = jest.fn(); +const updateFormSpy = jest.fn(); +const submitFormSpy = jest.fn(); +const resendCodeSpy = jest.fn(); + +const mockServiceFacade: UseAuthenticator = { + ...baseMockServiceFacade, + route: 'confirmResetPassword', + updateBlur: updateBlurSpy, + updateForm: updateFormSpy, + submitForm: submitFormSpy, + resendCode: resendCodeSpy, +}; + +const useAuthenticatorSpy = jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +jest.spyOn(UIModule, 'getActorContext').mockReturnValue({ + country_code: '+1', +}); + +jest.spyOn(UIModule, 'getSortedFormFields').mockReturnValue([ + [ + 'confirmation_code', + { + label: 'Code *', + placeholder: 'Code', + type: 'number', + }, + ], + [ + 'password', + { + label: 'New Password', + placeholder: 'New Password', + type: 'password', + }, + ], + [ + 'confirm_password', + { + label: 'Confirm Password', + placeholder: 'Confirm Password', + type: 'password', + }, + ], +]); + +const codeInputPrams = { name: 'confirmation_code', value: '123456' }; +const newPasswordInputParams = { + name: 'password', + value: 'verysecurepassword', +}; +const confirmPasswordInputParams = { + name: 'confirm_password', + value: 'verysecurepassword', +}; + +describe('ConfirmResetPassword', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders as expected', () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + const { container } = render(ConfirmResetPassword, { + global: { components }, + }); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('sends change event on form input', async () => { + render(ConfirmResetPassword, { global: { components } }); + + const codeField = await screen.findByLabelText('Code *'); + const newPasswordField = await screen.findByLabelText('New Password'); + const confirmPasswordField = await screen.findByLabelText( + 'Confirm Password' + ); + + await fireEvent.input(codeField, { target: codeInputPrams }); + expect(updateFormSpy).toHaveBeenCalledWith(codeInputPrams); + + await fireEvent.input(newPasswordField, { target: newPasswordInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(newPasswordInputParams); + + await fireEvent.input(confirmPasswordField, { + target: confirmPasswordInputParams, + }); + expect(updateFormSpy).toHaveBeenCalledWith(confirmPasswordInputParams); + }); + + it('sends blur event on form blur', async () => { + render(ConfirmResetPassword, { + global: { components }, + }); + + const codeField = await screen.findByLabelText('Code *'); + const newPasswordField = await screen.findByLabelText('New Password'); + const confirmPasswordField = await screen.findByLabelText( + 'Confirm Password' + ); + + await fireEvent.blur(codeField); + expect(updateBlurSpy).toHaveBeenCalledWith({ name: 'confirmation_code' }); + + await fireEvent.blur(newPasswordField); + expect(updateBlurSpy).toHaveBeenCalledWith({ name: 'password' }); + + await fireEvent.blur(confirmPasswordField); + expect(updateBlurSpy).toHaveBeenCalledWith({ name: 'confirm_password' }); + }); + + it('sends submit event on form submit', async () => { + render(ConfirmResetPassword, { global: { components } }); + + const submitButton = await screen.findByRole('button', { + name: 'Submit', + }); + + await fireEvent.click(submitButton); + expect(submitFormSpy).toHaveBeenCalledTimes(1); + }); + + it('displays error if present', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ ...mockServiceFacade, error: 'mockError' }) + ); + render(ConfirmResetPassword, { global: { components } }); + + expect(await screen.findByText('mockError')).toBeInTheDocument(); + }); + + it('handles resend code button as expected', async () => { + render(ConfirmResetPassword, { global: { components } }); + + const resendCodeButton = await screen.findByRole('button', { + name: 'Resend Code', + }); + await fireEvent.click(resendCodeButton); + + expect(resendCodeSpy).toHaveBeenCalledTimes(1); + }); + + it('disables the submit button if password reset is pending', async () => { + useAuthenticatorSpy.mockReturnValue( + reactive({ ...mockServiceFacade, isPending: true }) + ); + render(ConfirmResetPassword, { global: { components } }); + + const submitButton = await screen.findByRole('button', { + name: 'Submit', + }); + expect(submitButton).toBeDisabled(); + }); +}); diff --git a/packages/vue/src/components/__tests__/confirm-sign-in.spec.ts b/packages/vue/src/components/__tests__/confirm-sign-in.spec.ts new file mode 100644 index 00000000000..9410a922e9d --- /dev/null +++ b/packages/vue/src/components/__tests__/confirm-sign-in.spec.ts @@ -0,0 +1,141 @@ +import { reactive, Ref, ref } from 'vue'; +import { fireEvent, render, screen } from '@testing-library/vue'; + +import * as UIModule from '@aws-amplify/ui'; +import { + AmplifyUser, + AuthInterpreter, + AuthMachineState, +} from '@aws-amplify/ui'; + +import { components } from '../../../global-spec'; +import * as UseAuthComposables from '../../composables/useAuth'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import { UseAuthenticator } from '../../types'; +import ConfirmSignIn from '../confirm-sign-in.vue'; + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +const updateFormSpy = jest.fn(); +const submitFormSpy = jest.fn(); +const toSignInSpy = jest.fn(); + +const mockServiceFacade: UseAuthenticator = { + ...baseMockServiceFacade, + route: 'confirmSignIn', + updateForm: updateFormSpy, + submitForm: submitFormSpy, + toSignIn: toSignInSpy, + user: { challengeName: 'SOFTWARE_TOKEN_MFA' } as AmplifyUser, +}; + +const useAuthenticatorSpy = jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +jest.spyOn(UIModule, 'getActorContext').mockReturnValue({ + country_code: '+1', +}); + +jest.spyOn(UIModule, 'getSortedFormFields').mockReturnValue([ + [ + 'confirmation_code', + { + label: 'Code *', + placeholder: 'Code', + type: 'number', + }, + ], +]); + +const codeInputParams = { name: 'confirmation_code', value: '123456' }; + +describe('ConfirmSignIn', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders as expected for TOTP challenge', () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + const { container } = render(ConfirmSignIn, { global: { components } }); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('renders as expected for SMS challenge', () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + user: { challengeName: 'SMS_MFA' }, + } as UseAuthenticator) + ); + const { container } = render(ConfirmSignIn, { global: { components } }); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('sends change event on form input', async () => { + render(ConfirmSignIn, { global: { components } }); + + const codeField = await screen.findByLabelText('Code *'); + + await fireEvent.input(codeField, { target: codeInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(codeInputParams); + }); + + it('sends submit event on form submit', async () => { + render(ConfirmSignIn, { global: { components } }); + + const codeField = await screen.findByLabelText('Code *'); + + await fireEvent.input(codeField, { target: codeInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(codeInputParams); + + const submitButton = await screen.findByRole('button', { name: 'Confirm' }); + await fireEvent.click(submitButton); + + expect(submitFormSpy).toHaveBeenCalledTimes(1); + }); + + it('displays error if present', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ ...mockServiceFacade, error: 'mockError' }) + ); + render(ConfirmSignIn, { global: { components } }); + + expect(await screen.findByText('mockError')).toBeInTheDocument(); + }); + + it('handles back to sign in button as expected', async () => { + render(ConfirmSignIn, { global: { components } }); + + const backToSignInButton = await screen.findByRole('button', { + name: 'Back to Sign In', + }); + await fireEvent.click(backToSignInButton); + + expect(toSignInSpy).toHaveBeenCalledTimes(1); + }); + + it('disables the submit button if confirmation is pending', async () => { + useAuthenticatorSpy.mockReturnValue( + reactive({ ...mockServiceFacade, isPending: true }) + ); + render(ConfirmSignIn, { global: { components } }); + + const submitButton = await screen.findByRole('button', { name: 'Confirm' }); + expect(submitButton).toBeDisabled(); + }); +}); diff --git a/packages/vue/src/components/__tests__/confirm-sign-up.spec.ts b/packages/vue/src/components/__tests__/confirm-sign-up.spec.ts new file mode 100644 index 00000000000..8612b03cd50 --- /dev/null +++ b/packages/vue/src/components/__tests__/confirm-sign-up.spec.ts @@ -0,0 +1,137 @@ +import { reactive, Ref, ref } from 'vue'; +import { fireEvent, render, screen } from '@testing-library/vue'; + +import * as UIModule from '@aws-amplify/ui'; +import { + AuthenticatorServiceFacade, + AuthInterpreter, + AuthMachineState, +} from '@aws-amplify/ui'; + +import { components } from '../../../global-spec'; +import * as UseAuthComposables from '../../composables/useAuth'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import ConfirmSignUp from '../confirm-sign-up.vue'; + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +const updateFormSpy = jest.fn(); +const submitFormSpy = jest.fn(); +const resendCodeSpy = jest.fn(); + +const mockServiceFacade: AuthenticatorServiceFacade = { + ...baseMockServiceFacade, + route: 'confirmSignUp', + updateForm: updateFormSpy, + submitForm: submitFormSpy, + resendCode: resendCodeSpy, +}; + +const useAuthenticatorSpy = jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +jest.spyOn(UIModule, 'getActorContext').mockReturnValue({ + country_code: '+1', +}); + +jest.spyOn(UIModule, 'getSortedFormFields').mockReturnValue([ + [ + 'confirmation_code', + { + label: 'Confirmation Code', + placeholder: 'Enter your code', + type: 'number', + }, + ], +]); + +const codeInputParams = { name: 'confirmation_code', value: '123456' }; + +describe('ConfirmSignUp', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders as expected', () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + const { container } = render(ConfirmSignUp, { global: { components } }); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('sends change event on form input', async () => { + render(ConfirmSignUp, { global: { components } }); + + const codeField = await screen.findByLabelText('Confirmation Code'); + + await fireEvent.input(codeField, { target: codeInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(codeInputParams); + }); + + it('sends submit event on form submit', async () => { + render(ConfirmSignUp, { global: { components } }); + + const codeField = await screen.findByLabelText('Confirmation Code'); + + await fireEvent.input(codeField, { target: codeInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(codeInputParams); + + const submitButton = await screen.findByRole('button', { + name: 'Confirm', + }); + await fireEvent.click(submitButton); + expect(submitFormSpy).toHaveBeenCalledTimes(1); + }); + + it('displays error if it is present', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + error: 'mockError', + }) + ); + render(ConfirmSignUp, { global: { components } }); + + expect(await screen.findByText('mockError')).toBeInTheDocument(); + }); + + it('handles resend code button as expected', async () => { + render(ConfirmSignUp, { global: { components } }); + + const resendCodeButton = await screen.findByRole('button', { + name: 'Resend Code', + }); + + await fireEvent.click(resendCodeButton); + + expect(resendCodeSpy).toHaveBeenCalledTimes(1); + }); + + it('disables the submit button if if sign up is pending', async () => { + useAuthenticatorSpy.mockReturnValue( + reactive({ ...mockServiceFacade, isPending: true }) + ); + render(ConfirmSignUp, { global: { components } }); + + const submitButton = await screen.findByRole('button', { + name: 'Confirm', + }); + expect(submitButton).toBeDisabled(); + }); +}); diff --git a/packages/vue/src/components/__tests__/confirm-verify-user.spec.ts b/packages/vue/src/components/__tests__/confirm-verify-user.spec.ts new file mode 100644 index 00000000000..b9133abad20 --- /dev/null +++ b/packages/vue/src/components/__tests__/confirm-verify-user.spec.ts @@ -0,0 +1,136 @@ +import { reactive, Ref, ref } from 'vue'; +import { fireEvent, render, screen } from '@testing-library/vue'; + +import * as UIModule from '@aws-amplify/ui'; +import { + AuthenticatorServiceFacade, + AuthInterpreter, + AuthMachineState, +} from '@aws-amplify/ui'; + +import { components } from '../../../global-spec'; +import * as UseAuthComposables from '../../composables/useAuth'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import ConfirmVerifyUser from '../confirm-verify-user.vue'; + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +const updateFormSpy = jest.fn(); +const submitFormSpy = jest.fn(); +const skipVerificationSpy = jest.fn(); + +const mockServiceFacade: AuthenticatorServiceFacade = { + ...baseMockServiceFacade, + route: 'confirmSignUp', + updateForm: updateFormSpy, + submitForm: submitFormSpy, + skipVerification: skipVerificationSpy, +}; + +const useAuthenticatorSpy = jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +jest.spyOn(UIModule, 'getActorContext').mockReturnValue({ + country_code: '+1', +}); + +jest.spyOn(UIModule, 'getSortedFormFields').mockReturnValue([ + [ + 'confirmation_code', + { + label: 'Code *', + placeholder: 'Code', + type: 'number', + }, + ], +]); + +const codeInputParams = { name: 'confirmation_code', value: '123456' }; + +describe('ConfirmVerifyUser', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders as expected', () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + const { container } = render(ConfirmVerifyUser, { global: { components } }); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('sends change event on form input', async () => { + render(ConfirmVerifyUser, { global: { components } }); + + const codeField = await screen.findByLabelText('Code *'); + + await fireEvent.input(codeField, { target: codeInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(codeInputParams); + }); + + it('sends submit event on form submit', async () => { + render(ConfirmVerifyUser, { global: { components } }); + + const codeField = await screen.findByLabelText('Code *'); + + await fireEvent.input(codeField, { target: codeInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(codeInputParams); + + const submitButton = await screen.findByRole('button', { name: 'Submit' }); + await fireEvent.click(submitButton); + expect(submitFormSpy).toHaveBeenCalledTimes(1); + }); + + it('handles skip verification', async () => { + render(ConfirmVerifyUser, { global: { components } }); + + const skipButton = await screen.findByRole('button', { name: 'Skip' }); + await fireEvent.click(skipButton); + + expect(skipVerificationSpy).toHaveBeenCalledTimes(1); + }); + + it('displays error if present', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + error: 'mockError', + }) + ); + render(ConfirmVerifyUser, { global: { components } }); + + expect(await screen.findByText('mockError')).toBeInTheDocument(); + }); + + it('disables the submit button if sign up is pending', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + isPending: true, + }) + ); + render(ConfirmVerifyUser, { global: { components } }); + + const submitButton = await screen.findByRole('button', { + name: 'Submit', + }); + + expect(submitButton).toBeDisabled(); + }); +}); diff --git a/packages/vue/src/components/__tests__/federated-sign-in-button.spec.ts b/packages/vue/src/components/__tests__/federated-sign-in-button.spec.ts new file mode 100644 index 00000000000..af704bcfa08 --- /dev/null +++ b/packages/vue/src/components/__tests__/federated-sign-in-button.spec.ts @@ -0,0 +1,62 @@ +import { reactive } from 'vue'; +import { screen, render, fireEvent } from '@testing-library/vue'; + +import { components } from '../../../global-spec'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import * as UseAuthComposables from '../../composables/useAuth'; +import FederatedSignInButton from '../federated-sign-in-button.vue'; + +const toFederatedSignInSpy = jest.fn(); + +const mockServiceFacade = { + ...baseMockServiceFacade, + toFederatedSignIn: toFederatedSignInSpy, +}; + +jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +describe('FederatedSignInButton', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders as expected', () => { + const signInText = 'Sign In with Amazon'; + + const { container } = render(FederatedSignInButton, { + global: { components }, + props: { provider: 'amazon' }, + slots: { default: signInText }, + }); + + expect(container).toMatchSnapshot(); + }); + + it('does not render anything if there is no provider', () => { + const { container } = render(FederatedSignInButton, { + global: { components }, + }); + + expect(container.firstChild?.hasChildNodes()).toBe(false); + }); + + it('calls toFederatedSignIn on click', async () => { + const signInText = 'Sign In with Amazon'; + render(FederatedSignInButton, { + global: { components }, + props: { provider: 'amazon' }, + slots: { default: signInText }, + }); + + const signInButton = await screen.findByRole('button', { + name: signInText, + }); + fireEvent.click(signInButton); + + expect(toFederatedSignInSpy).toHaveBeenCalledWith({ + provider: 'amazon', + }); + }); +}); diff --git a/packages/vue/src/components/__tests__/federated-sign-in.spec.ts b/packages/vue/src/components/__tests__/federated-sign-in.spec.ts new file mode 100644 index 00000000000..f9ae9908cfc --- /dev/null +++ b/packages/vue/src/components/__tests__/federated-sign-in.spec.ts @@ -0,0 +1,86 @@ +import { reactive, ref, Ref } from 'vue'; +import { render, screen } from '@testing-library/vue'; + +import { components } from '../../../global-spec'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import * as UseAuthComposables from '../../composables/useAuth'; +import FederatedSignIn from '../federated-sign-in.vue'; + +import { + AuthInterpreter, + AuthMachineState, + SocialProvider, + authenticatorTextUtil, +} from '@aws-amplify/ui'; + +const socialProviders: SocialProvider[] = [ + 'amazon', + 'apple', + 'facebook', + 'google', +]; + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +const mockServiceFacade = { ...baseMockServiceFacade, route: 'signIn' }; +const useAuthenticatorSpy = jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +describe('FederatedSignIn', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does not render anything if socialProviders does not exist', () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + socialProviders: undefined, + }) + ); + + const { container } = render(FederatedSignIn, { global: { components } }); + expect(container.firstChild?.hasChildNodes()).toBe(false); + }); + + it('does not render anything if socialProviders array is empty', () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + socialProviders: [], + }) + ); + + const { container } = render(FederatedSignIn, { global: { components } }); + expect(container.firstChild?.hasChildNodes()).toBe(false); + }); + + it.each(socialProviders)( + 'renders as expected with %s provider', + async (socialProvider) => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + socialProviders: [socialProvider], + }) + ); + + const { container } = render(FederatedSignIn, { global: { components } }); + expect(container).toMatchSnapshot(); + + const { getSignInWithFederationText } = authenticatorTextUtil; + const socialSignInText = getSignInWithFederationText( + 'signIn', + socialProvider + ); + + expect(await screen.findByText(socialSignInText)).toBeInTheDocument(); + } + ); +}); diff --git a/packages/vue/src/components/__tests__/force-new-password.spec.ts b/packages/vue/src/components/__tests__/force-new-password.spec.ts new file mode 100644 index 00000000000..7e04832b44b --- /dev/null +++ b/packages/vue/src/components/__tests__/force-new-password.spec.ts @@ -0,0 +1,189 @@ +import { reactive, Ref, ref } from 'vue'; +import { fireEvent, render, screen } from '@testing-library/vue'; + +import * as UIModule from '@aws-amplify/ui'; +import { AuthInterpreter, AuthMachineState } from '@aws-amplify/ui'; + +import { components } from '../../../global-spec'; +import * as UseAuthComposables from '../../composables/useAuth'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import { UseAuthenticator } from '../../types'; +import ForceNewPassword from '../force-new-password.vue'; + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +const updateBlurSpy = jest.fn(); +const updateFormSpy = jest.fn(); +const submitFormSpy = jest.fn(); +const toSignInSpy = jest.fn(); + +const mockServiceFacade: UseAuthenticator = { + ...baseMockServiceFacade, + route: 'confirmResetPassword', + updateBlur: updateBlurSpy, + updateForm: updateFormSpy, + submitForm: submitFormSpy, + toSignIn: toSignInSpy, +}; + +const useAuthenticatorSpy = jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +jest.spyOn(UIModule, 'getActorContext').mockReturnValue({ + country_code: '+1', +}); + +jest.spyOn(UIModule, 'getSortedFormFields').mockReturnValue([ + [ + 'password', + { + label: 'Password', + placeholder: 'Enter your Password', + type: 'password', + }, + ], + [ + 'confirm_password', + { + label: 'Confirm Password', + placeholder: 'Please confirm your Password', + type: 'password', + }, + ], + [ + 'preferred_username', + { + label: 'Preferred Username', + placeholder: 'Enter your Preferred Username', + type: 'text', + }, + ], +]); + +const passwordInputParams = { + name: 'password', + value: 'verysecurepassword', +}; +const confirmPasswordInputParams = { + name: 'confirm_password', + value: 'verysecurepassword', +}; +const preferredUsernameInputParams = { + name: 'preferred_username', + value: 'verysecurepassword', +}; + +describe('ConfirmResetPassword', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders as expected', () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + const { container } = render(ForceNewPassword, { + global: { components }, + }); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('sends change event on form input', async () => { + render(ForceNewPassword, { + global: { components }, + }); + + const passwordField = await screen.findByLabelText('Password'); + const confirmPasswordField = await screen.findByLabelText( + 'Confirm Password' + ); + const preferredUsernameField = await screen.findByLabelText( + 'Preferred Username' + ); + + await fireEvent.input(passwordField, { target: passwordInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(passwordInputParams); + + await fireEvent.input(confirmPasswordField, { + target: confirmPasswordInputParams, + }); + expect(updateFormSpy).toHaveBeenCalledWith(confirmPasswordInputParams); + + await fireEvent.input(preferredUsernameField, { + target: preferredUsernameInputParams, + }); + expect(updateFormSpy).toHaveBeenCalledWith(preferredUsernameInputParams); + }); + + it('sends blur event on form blur', async () => { + render(ForceNewPassword, { global: { components } }); + + const passwordField = await screen.findByLabelText('Password'); + const confirmPasswordField = await screen.findByLabelText( + 'Confirm Password' + ); + const preferredUsernameField = await screen.findByLabelText( + 'Preferred Username' + ); + + await fireEvent.blur(passwordField); + expect(updateBlurSpy).toHaveBeenCalledWith({ name: 'password' }); + + await fireEvent.blur(confirmPasswordField); + expect(updateBlurSpy).toHaveBeenCalledWith({ name: 'confirm_password' }); + + await fireEvent.blur(preferredUsernameField); + expect(updateBlurSpy).toHaveBeenCalledWith({ name: 'preferred_username' }); + }); + + it('sends submit event on form submit', async () => { + render(ForceNewPassword, { global: { components } }); + + const submitButton = await screen.findByRole('button', { + name: 'Change Password', + }); + + await fireEvent.click(submitButton); + expect(submitFormSpy).toHaveBeenCalledTimes(1); + }); + + it('displays error if present', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ ...mockServiceFacade, error: 'mockError' }) + ); + render(ForceNewPassword, { global: { components } }); + + expect(await screen.findByText('mockError')).toBeInTheDocument(); + }); + + it('handles back to sign in button as expected', async () => { + render(ForceNewPassword, { global: { components } }); + + const backToSignInButton = await screen.findByRole('button', { + name: 'Back to Sign In', + }); + await fireEvent.click(backToSignInButton); + + expect(toSignInSpy).toHaveBeenCalledTimes(1); + }); + + it('disables the submit button if password change is pending', async () => { + useAuthenticatorSpy.mockReturnValue( + reactive({ ...mockServiceFacade, isPending: true }) + ); + render(ForceNewPassword, { global: { components } }); + + const submitButton = await screen.findByRole('button', { + name: 'Changing…', + }); + expect(submitButton).toBeDisabled(); + }); +}); diff --git a/packages/vue/src/components/__tests__/password-control.spec.ts b/packages/vue/src/components/__tests__/password-control.spec.ts index d0e5e4a7968..9620d47b023 100644 --- a/packages/vue/src/components/__tests__/password-control.spec.ts +++ b/packages/vue/src/components/__tests__/password-control.spec.ts @@ -1,8 +1,10 @@ -import PasswordControl from '../password-control.vue'; -import { components } from '../../../global-spec'; import { fireEvent, render, screen } from '@testing-library/vue'; + import { ComponentClassName } from '@aws-amplify/ui'; +import { components } from '../../../global-spec'; +import PasswordControl from '../password-control.vue'; + describe('PasswordControl', () => { it('should render as expected', () => { // to mock random id diff --git a/packages/vue/src/components/__tests__/reset-password.spec.ts b/packages/vue/src/components/__tests__/reset-password.spec.ts new file mode 100644 index 00000000000..6b345440a94 --- /dev/null +++ b/packages/vue/src/components/__tests__/reset-password.spec.ts @@ -0,0 +1,122 @@ +import { reactive, Ref, ref } from 'vue'; +import { fireEvent, render, screen } from '@testing-library/vue'; + +import * as UIModule from '@aws-amplify/ui'; +import { AuthInterpreter, AuthMachineState } from '@aws-amplify/ui'; + +import { components } from '../../../global-spec'; +import * as UseAuthComposables from '../../composables/useAuth'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import { UseAuthenticator } from '../../types'; +import ResetPassword from '../reset-password.vue'; + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +const updateFormSpy = jest.fn(); +const submitFormSpy = jest.fn(); +const toSignInSpy = jest.fn(); + +const mockServiceFacade: UseAuthenticator = { + ...baseMockServiceFacade, + route: 'resetPassword', + updateForm: updateFormSpy, + submitForm: submitFormSpy, + toSignIn: toSignInSpy, +}; + +const useAuthenticatorSpy = jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +jest.spyOn(UIModule, 'getActorContext').mockReturnValue({ + country_code: '+1', +}); + +jest.spyOn(UIModule, 'getSortedFormFields').mockReturnValue([ + [ + 'email', + { + label: 'Email', + placeholder: 'Enter your Email', + type: 'email', + }, + ], +]); + +const emailInputParams = { name: 'email', value: 'user@example.com' }; + +describe('ResetPassword', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders as expected', () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + const { container } = render(ResetPassword, { global: { components } }); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('sends change event on form input', async () => { + render(ResetPassword, { global: { components } }); + + const emailField = await screen.findByLabelText('Email'); + + await fireEvent.input(emailField, { target: emailInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(emailInputParams); + }); + + it('sends submit event on form submit', async () => { + render(ResetPassword, { global: { components } }); + + const emailField = await screen.findByLabelText('Email'); + await fireEvent.input(emailField, { target: emailInputParams }); + + const submitButton = await screen.findByRole('button', { + name: 'Send code', + }); + + await fireEvent.click(submitButton); + expect(submitFormSpy).toHaveBeenCalledTimes(1); + }); + + it('displays error if present', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ ...mockServiceFacade, error: 'mockError' }) + ); + render(ResetPassword, { global: { components } }); + + expect(await screen.findByText('mockError')).toBeInTheDocument(); + }); + + it('handles back to sign in button as expected', async () => { + render(ResetPassword, { global: { components } }); + + const backToSignInButton = await screen.findByRole('button', { + name: 'Back to Sign In', + }); + await fireEvent.click(backToSignInButton); + + expect(toSignInSpy).toHaveBeenCalledTimes(1); + }); + + it('disables the submit button if confirmation is pending', async () => { + useAuthenticatorSpy.mockReturnValue( + reactive({ ...mockServiceFacade, isPending: true }) + ); + render(ResetPassword, { global: { components } }); + + const submitButton = await screen.findByRole('button', { + name: 'Send code', + }); + expect(submitButton).toBeDisabled(); + }); +}); diff --git a/packages/vue/src/components/__tests__/setup-totp.spec.ts b/packages/vue/src/components/__tests__/setup-totp.spec.ts new file mode 100644 index 00000000000..3e32cee4137 --- /dev/null +++ b/packages/vue/src/components/__tests__/setup-totp.spec.ts @@ -0,0 +1,231 @@ +import { reactive, Ref, ref } from 'vue'; +import { fireEvent, render, screen, waitFor } from '@testing-library/vue'; +import QRCode from 'qrcode'; + +import * as UIModule from '@aws-amplify/ui'; +import { + AmplifyUser, + AuthInterpreter, + AuthMachineState, +} from '@aws-amplify/ui'; + +import { components } from '../../../global-spec'; +import * as UseAuthComposables from '../../composables/useAuth'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import { UseAuthenticator } from '../../types'; +import SetupTOTP from '../setup-totp.vue'; + +// mock clipboard +const writeClipboardTextSpy = jest.fn(); +Object.assign(navigator, { clipboard: { writeText: writeClipboardTextSpy } }); + +const consoleWarnSpy = jest.spyOn(console, 'warn'); + +const toDataURLSpy = jest.spyOn(QRCode, 'toDataURL'); + +const getTotpCodeURLSpy = jest.spyOn(UIModule, 'getTotpCodeURL'); + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +const submitFormSpy = jest.fn(); +const toSignInSpy = jest.fn(); +const updateFormSpy = jest.fn(); + +const mockServiceFacade = { + ...baseMockServiceFacade, + QRFields: null, + route: 'signIn', + submitForm: submitFormSpy, + toSignIn: toSignInSpy, + totpSecretCode: 'totp-mock-secret-code', + updateForm: updateFormSpy, + user: { username: 'testuser' } as unknown as AmplifyUser, +} as UseAuthenticator; + +const useAuthenticatorSpy = jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +jest.spyOn(UIModule, 'getActorContext').mockReturnValue({ + country_code: '+1', +}); + +jest.spyOn(UIModule, 'getSortedFormFields').mockReturnValue([ + [ + 'confirmation_code', + { + label: 'Code *', + placeholder: 'Enter your Confirmation Code', + type: 'number', + }, + ], +]); + +const codeInputParams = { name: 'confirmation_code', value: '123456' }; + +describe('SetupTOTP', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders loading text as expected on init', async () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + const { container } = render(SetupTOTP, { global: { components } }); + await screen.findByText('Loading...'); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('renders qrcode image as expected after onMounted is done', async () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + const { container } = render(SetupTOTP, { global: { components } }); + + // wait for qrcode to render + await screen.findByAltText('qr code'); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('sends change event on form input', async () => { + render(SetupTOTP, { global: { components } }); + + const codeField = await screen.findByLabelText('Code *'); + + await fireEvent.input(codeField, { target: codeInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(codeInputParams); + }); + + it('sends submit event on form submit', async () => { + render(SetupTOTP, { global: { components } }); + + const codeField = await screen.findByLabelText('Code *'); + + await fireEvent.input(codeField, { target: codeInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(codeInputParams); + + const submitButton = await screen.findByRole('button', { name: 'Confirm' }); + await fireEvent.click(submitButton); + + expect(submitFormSpy).toHaveBeenCalledTimes(1); + }); + + it('copies secret code to clipboard when copy button is clicked', async () => { + render(SetupTOTP, { global: { components } }); + + // wait for qrcode to render + await screen.findByAltText('qr code'); + + const copyButton = await screen.findByText('COPY'); + copyButton.click(); + + expect(writeClipboardTextSpy).toHaveBeenCalledTimes(1); + expect(writeClipboardTextSpy).toHaveBeenCalledWith('totp-mock-secret-code'); + }); + + it('does not update clipboard if copy button is clicked without valid totp code', async () => { + useAuthenticatorSpy.mockReturnValue( + reactive({ + ...mockServiceFacade, + totpSecretCode: undefined, + }) + ); + + render(SetupTOTP, { global: { components } }); + + const copyButton = await screen.findByText('COPY'); + copyButton.click(); + + expect(writeClipboardTextSpy).not.toHaveBeenCalled(); + }); + + it('logs error message if QRCode.toDataURL fails', async () => { + toDataURLSpy.mockImplementation(() => Promise.reject()); + + render(SetupTOTP, { global: { components } }); + + // wait until async function `toDataURL` resolves and call console.warn + waitFor(() => expect(consoleWarnSpy).toHaveBeenCalled()); + }); + + it('handles back to sign in button as expected', async () => { + render(SetupTOTP, { global: { components } }); + + const backToSignInButton = await screen.findByRole('button', { + name: 'Back to Sign In', + }); + + fireEvent.click(backToSignInButton); + + expect(toSignInSpy).toHaveBeenCalledTimes(1); + }); + + it('uses custom values in QRFields if present', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + QRFields: { + totpIssuer: 'custom-issuer', + totpUsername: 'custom-username', + }, + } as UseAuthenticator) + ); + + render(SetupTOTP, { global: { components } }); + + // wait for qrcode to render + await screen.findByAltText('qr code'); + + expect(getTotpCodeURLSpy).toHaveBeenCalledWith( + 'custom-issuer', + 'custom-username', + 'totp-mock-secret-code' + ); + }); + + it('does not call getTotpCodeURL if totpCodeURL is not present', () => { + useAuthenticatorSpy.mockReturnValue( + reactive({ + ...mockServiceFacade, + totpSecretCode: undefined, + }) + ); + + render(SetupTOTP, { global: { components } }); + expect(getTotpCodeURLSpy).not.toHaveBeenCalled(); + }); + + it('renders error if present', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + error: 'mockError', + }) + ); + render(SetupTOTP, { global: { components } }); + + expect(await screen.findByText('mockError')).toBeInTheDocument(); + }); + + it('disables submit button if totp setup is pending', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ ...mockServiceFacade, isPending: true }) + ); + render(SetupTOTP, { global: { components } }); + const submitButton = await screen.findByRole('button', { + name: 'Confirm', + }); + + expect(submitButton).toBeDisabled(); + }); +}); diff --git a/packages/vue/src/components/__tests__/sign-in.spec.ts b/packages/vue/src/components/__tests__/sign-in.spec.ts new file mode 100644 index 00000000000..302521b5389 --- /dev/null +++ b/packages/vue/src/components/__tests__/sign-in.spec.ts @@ -0,0 +1,133 @@ +import { reactive, Ref, ref } from 'vue'; +import { fireEvent, render, screen } from '@testing-library/vue'; + +import * as UIModule from '@aws-amplify/ui'; +import { AuthInterpreter, AuthMachineState } from '@aws-amplify/ui'; + +import { components } from '../../../global-spec'; +import * as UseAuthComposables from '../../composables/useAuth'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import SignIn from '../sign-in.vue'; + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +const updateFormSpy = jest.fn(); +const submitFormSpy = jest.fn(); +const toResetPasswordSpy = jest.fn(); + +const mockServiceFacade = { + ...baseMockServiceFacade, + route: 'signIn', + updateForm: updateFormSpy, + submitForm: submitFormSpy, + toResetPassword: toResetPasswordSpy, +}; + +const useAuthenticatorSpy = jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +jest.spyOn(UIModule, 'getActorContext').mockReturnValue({ + country_code: '+1', +}); + +jest.spyOn(UIModule, 'getSortedFormFields').mockReturnValue([ + ['username', { label: 'Username', placeholder: 'Enter your Username' }], + [ + 'password', + { label: 'Password', placeholder: 'Enter your Password', type: 'password' }, + ], +]); + +const usernameInputParams = { name: 'username', value: 'username' }; +const passwordInputParams = { name: 'password', value: 'verysecurepassword' }; + +describe('SignIn', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders as expected', () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + const { container } = render(SignIn, { global: { components } }); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('sends change event on form input', async () => { + render(SignIn, { global: { components } }); + const usernameField = await screen.findByLabelText('Username'); + const passwordField = await screen.findByLabelText('Password'); + + await fireEvent.input(usernameField, { target: usernameInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(usernameInputParams); + + await fireEvent.input(passwordField, { target: passwordInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(passwordInputParams); + }); + + it('sends submit event on form submit', async () => { + render(SignIn, { global: { components } }); + const usernameField = await screen.findByLabelText('Username'); + const passwordField = await screen.findByLabelText('Password'); + + await fireEvent.input(usernameField, { target: usernameInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(usernameInputParams); + + await fireEvent.input(passwordField, { target: passwordInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(passwordInputParams); + + const submitButton = await screen.findByRole('button', { name: 'Sign in' }); + await fireEvent.click(submitButton); + + expect(submitFormSpy).toHaveBeenCalled(); + }); + + it('forgot password button navigates to reset password screen', async () => { + render(SignIn, { global: { components } }); + + const forgotPasswordButton = await screen.findByRole('button', { + name: 'Forgot your password?', + }); + + await fireEvent.click(forgotPasswordButton); + + expect(toResetPasswordSpy).toHaveBeenCalled(); + }); + + it('displays error if it is present', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + error: 'mockError', + }) + ); + render(SignIn, { global: { components } }); + + expect(await screen.findByText('mockError')).toBeInTheDocument(); + }); + + it('disables the submit button if sign in is pending', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + isPending: true, + }) + ); + render(SignIn, { global: { components } }); + + const submitButton = await screen.findByRole('button', { + name: 'Signing in', + }); + + expect(submitButton).toBeDisabled(); + }); +}); diff --git a/packages/vue/src/components/__tests__/sign-up.spec.ts b/packages/vue/src/components/__tests__/sign-up.spec.ts new file mode 100644 index 00000000000..5a1824e14d4 --- /dev/null +++ b/packages/vue/src/components/__tests__/sign-up.spec.ts @@ -0,0 +1,205 @@ +import { reactive, Ref, ref } from 'vue'; +import { fireEvent, render, screen } from '@testing-library/vue'; + +import * as UIModule from '@aws-amplify/ui'; +import { + AuthenticatorServiceFacade, + AuthInterpreter, + AuthMachineState, +} from '@aws-amplify/ui'; + +import { components } from '../../../global-spec'; +import * as UseAuthComposables from '../../composables/useAuth'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import SignUp from '../sign-up.vue'; + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +const updateFormSpy = jest.fn(); +const updateBlurSpy = jest.fn(); +const submitFormSpy = jest.fn(); + +const mockServiceFacade: AuthenticatorServiceFacade = { + ...baseMockServiceFacade, + route: 'signIn', + updateBlur: updateBlurSpy, + updateForm: updateFormSpy, + submitForm: submitFormSpy, +}; + +const useAuthenticatorSpy = jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +jest.spyOn(UIModule, 'getActorContext').mockReturnValue({ + country_code: '+1', +}); + +const getSortedFormFieldsSpy = jest + .spyOn(UIModule, 'getSortedFormFields') + .mockReturnValue([ + ['username', { label: 'Username', placeholder: 'Enter your Username' }], + [ + 'password', + { + label: 'Password', + placeholder: 'Enter your Password', + type: 'password', + }, + ], + [ + 'confirm_password', + { + label: 'Confirm Password', + placeholder: 'Please your Password', + type: 'password', + }, + ], + ['email', { label: 'Email', placeholder: 'Enter your Email' }], + ]); + +const usernameInputParams = { name: 'username', value: 'username' }; +const passwordInputParams = { name: 'password', value: 'verysecurepassword' }; +const confirmPasswordInputParams = { + name: 'confirm_password', + value: 'verysecurepassword', +}; +const emailInputParams = { name: 'email', value: 'email@example.com' }; +const checkboxInputParams = { + name: 'mycheckbox', + value: 'checkboxvalue', +}; + +describe('SignUp', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders as expected', () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + const { container } = render(SignUp, { global: { components } }); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('sends change event on form input', async () => { + render(SignUp, { global: { components } }); + const usernameField = await screen.findByLabelText('Username'); + const passwordField = await screen.findByLabelText('Password'); + const confirmPasswordField = await screen.findByLabelText( + 'Confirm Password' + ); + const emailField = await screen.findByLabelText('Email'); + + await fireEvent.input(usernameField, { target: usernameInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(usernameInputParams); + + await fireEvent.input(passwordField, { target: passwordInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(passwordInputParams); + + await fireEvent.input(confirmPasswordField, { + target: confirmPasswordInputParams, + }); + expect(updateFormSpy).toHaveBeenCalledWith(confirmPasswordInputParams); + + await fireEvent.input(emailField, { target: emailInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(emailInputParams); + }); + + it('handles blur event', async () => { + render(SignUp, { global: { components } }); + const usernameField = await screen.findByLabelText('Username'); + + await fireEvent.blur(usernameField); + expect(updateBlurSpy).toHaveBeenCalledWith({ name: 'username' }); + }); + + it('handles checkbox event', async () => { + getSortedFormFieldsSpy.mockReturnValueOnce([ + ['mycheckbox', { label: 'My Checkbox', type: 'checkbox' }], + ]); + render(SignUp, { global: { components } }); + + const checkboxField = await screen.findByLabelText('My Checkbox'); + + // check the checkbox + await fireEvent.click(checkboxField, { target: checkboxInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith({ + name: 'mycheckbox', + value: 'checkboxvalue', + }); + + // uncheck the checkbox. Value should be undefined. + await fireEvent.click(checkboxField, { target: checkboxInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith({ + name: 'mycheckbox', + }); + }); + + it('sends submit event on form submit', async () => { + render(SignUp, { global: { components } }); + const usernameField = await screen.findByLabelText('Username'); + const passwordField = await screen.findByLabelText('Password'); + const confirmPasswordField = await screen.findByLabelText( + 'Confirm Password' + ); + const emailField = await screen.findByLabelText('Email'); + + await fireEvent.input(usernameField, { target: usernameInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(usernameInputParams); + + await fireEvent.input(passwordField, { target: passwordInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(passwordInputParams); + + await fireEvent.input(confirmPasswordField, { + target: confirmPasswordInputParams, + }); + expect(updateFormSpy).toHaveBeenCalledWith(confirmPasswordInputParams); + + await fireEvent.input(emailField, { target: emailInputParams }); + expect(updateFormSpy).toHaveBeenCalledWith(emailInputParams); + + const submitButton = await screen.findByRole('button', { + name: 'Create Account', + }); + await fireEvent.click(submitButton); + + expect(submitFormSpy).toHaveBeenCalledTimes(1); + }); + + it('displays error if it is present', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + error: 'mockError', + }) + ); + render(SignUp, { global: { components } }); + + expect(await screen.findByText('mockError')).toBeInTheDocument(); + }); + + it('disables the submit button if sign up is pending', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + isPending: true, + }) + ); + render(SignUp, { global: { components } }); + + const submitButton = await screen.findByRole('button', { + name: 'Create Account', + }); + + expect(submitButton).toBeDisabled(); + }); +}); diff --git a/packages/vue/src/components/__tests__/verify-user.spec.ts b/packages/vue/src/components/__tests__/verify-user.spec.ts new file mode 100644 index 00000000000..ccd89d6bb15 --- /dev/null +++ b/packages/vue/src/components/__tests__/verify-user.spec.ts @@ -0,0 +1,134 @@ +import { reactive, Ref, ref } from 'vue'; +import { fireEvent, render, screen } from '@testing-library/vue'; + +import * as UIModule from '@aws-amplify/ui'; +import { + AuthenticatorServiceFacade, + AuthInterpreter, + AuthMachineState, + UnverifiedContactMethods, +} from '@aws-amplify/ui'; + +import { components } from '../../../global-spec'; +import * as UseAuthComposables from '../../composables/useAuth'; +import { baseMockServiceFacade } from '../../composables/__mock__/useAuthenticatorMock'; +import VerifyUser from '../verify-user.vue'; + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +jest.spyOn(UseAuthComposables, 'useAuth').mockReturnValue({ + authStatus: ref('unauthenticated'), + send: jest.fn(), + service: undefined as unknown as AuthInterpreter, + state: ref(undefined) as unknown as Ref, +}); + +const updateFormSpy = jest.fn(); +const submitFormSpy = jest.fn(); +const skipVerificationSpy = jest.fn(); +const unverifiedContactMethods: UnverifiedContactMethods = { + email: 'test@example.com', +}; + +const mockServiceFacade: AuthenticatorServiceFacade = { + ...baseMockServiceFacade, + route: 'verifyUser', + updateForm: updateFormSpy, + skipVerification: skipVerificationSpy, + submitForm: submitFormSpy, + unverifiedContactMethods, +}; + +const useAuthenticatorSpy = jest + .spyOn(UseAuthComposables, 'useAuthenticator') + .mockReturnValue(reactive(mockServiceFacade)); + +jest.spyOn(UIModule, 'getActorContext').mockReturnValue({ + country_code: '+1', +}); + +describe('VerifyUser', () => { + it('renders as expected', () => { + // mock random value so that snapshots are consistent + const mathRandomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.1); + + const { container } = render(VerifyUser, { global: { components } }); + expect(container).toMatchSnapshot(); + + mathRandomSpy.mockRestore(); + }); + + it('sends change event on form input', async () => { + render(VerifyUser, { global: { components } }); + + const checkboxField = await screen.findByLabelText('Email'); + + await fireEvent.click(checkboxField); + expect(updateFormSpy).toHaveBeenCalledWith({ + name: 'unverifiedAttr', + value: 'email', + }); + }); + + it('sends submit event on form submit', async () => { + render(VerifyUser, { global: { components } }); + + const checkboxField = await screen.findByLabelText('Email'); + + await fireEvent.click(checkboxField); + expect(updateFormSpy).toHaveBeenCalledWith({ + name: 'unverifiedAttr', + value: 'email', + }); + + const submitButton = await screen.findByRole('button', { name: 'Verify' }); + await fireEvent.click(submitButton); + + expect(submitFormSpy).toHaveBeenCalledTimes(1); + }); + + it('handles skip verification', async () => { + render(VerifyUser, { global: { components } }); + + const skipButton = await screen.findByRole('button', { + name: 'Skip', + }); + + await fireEvent.click(skipButton); + + expect(skipVerificationSpy).toHaveBeenCalledTimes(1); + }); + + it('displays error if it is present', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + error: 'mockError', + }) + ); + render(VerifyUser, { global: { components } }); + + expect(await screen.findByText('mockError')).toBeInTheDocument(); + }); + + it('disables the submit button if sign up is pending', async () => { + useAuthenticatorSpy.mockReturnValueOnce( + reactive({ + ...mockServiceFacade, + isPending: true, + }) + ); + render(VerifyUser, { global: { components } }); + + const submitButton = await screen.findByRole('button', { + name: 'Verify', + }); + + expect(submitButton).toBeDisabled(); + }); +}); diff --git a/packages/vue/src/components/authenticator.vue b/packages/vue/src/components/authenticator.vue index dee0264ee61..9ca269683cf 100644 --- a/packages/vue/src/components/authenticator.vue +++ b/packages/vue/src/components/authenticator.vue @@ -1,16 +1,27 @@ @@ -76,7 +70,7 @@ function onBlur(e: Event) { > @@ -88,17 +82,18 @@ function onBlur(e: Event) { - - {{ translate(actorState?.context?.remoteError) }} + + {{ translate(error) }} {{ confirmResetPasswordText }} + {{ confirmResetPasswordText }} + +import { computed, toRefs, useAttrs } from 'vue'; + import { authenticatorTextUtil, - getActorState, getFormDataFromEvent, - SignInState, translate, } from '@aws-amplify/ui'; -import { computed, ComputedRef, useAttrs } from 'vue'; -import { useAuth, useAuthenticator } from '../composables/useAuth'; +import { useAuthenticator } from '../composables/useAuth'; +import { UseAuthenticator } from '../types'; import BaseFormFields from './primitives/base-form-fields.vue'; +/** @deprecated Component events are deprecated and not maintained. */ const emit = defineEmits(['confirmSignInSubmit', 'backToSignInClicked']); const attrs = useAttrs(); -const { state, send } = useAuth(); - -const props = useAuthenticator(); +// `facade` is manually typed to `UseAuthenticator` for temporary type safety. +const facade: UseAuthenticator = useAuthenticator(); +const { submitForm, toSignIn, updateForm } = facade; +const { user, error, isPending } = toRefs(facade); -const actorState = computed(() => - getActorState(state.value) -) as ComputedRef; -const challengeName = actorState.value.context.challengeName; +const challengeName = computed(() => user.value.challengeName); // Text Util const { getBackToSignInText, getConfirmText, getChallengeText } = authenticatorTextUtil; // Computed Properties -const confirmSignInHeading = computed(() => getChallengeText(challengeName)); +const confirmSignInHeading = computed(() => + getChallengeText(challengeName.value) +); const backSignInText = computed(() => getBackToSignInText()); const confirmText = computed(() => getConfirmText()); // Methods const onInput = (e: Event): void => { const { name, value } = e.target as HTMLInputElement; - send({ - type: 'CHANGE', - //@ts-ignore - data: { name, value }, - }); + updateForm({ name, value }); }; const onConfirmSignInSubmit = (e: Event): void => { + // TODO(BREAKING): remove unused emit + // istanbul ignore next if (attrs?.onConfirmSignInSubmit) { emit('confirmSignInSubmit', e); } else { - submit(e); + submitForm(getFormDataFromEvent(e)); } }; -const submit = (e: Event): void => { - props.submitForm(getFormDataFromEvent(e)); -}; - const onBackToSignInClicked = (): void => { + // TODO(BREAKING): remove unused emit + // istanbul ignore next if (attrs?.onBackToSignInClicked) { emit('backToSignInClicked'); } else { - send({ - type: 'SIGN_IN', - }); + toSignIn(); } }; @@ -75,7 +70,7 @@ const onBackToSignInClicked = (): void => { > @@ -86,8 +81,8 @@ const onBackToSignInClicked = (): void => { - - {{ translate(actorState?.context?.remoteError) }} + + {{ translate(error) }} { :loading="false" :variation="'primary'" style="font-weight: normal" - :disabled="actorState.matches('confirmSignIn.pending')" - >{{ confirmText }} + {{ confirmText }} + { type="button" @click.prevent="onBackToSignInClicked" > - {{ backSignInText }} + {{ backSignInText }} + { }; const onConfirmSignUpSubmit = (e: Event): void => { + // TODO(BREAKING): remove unused emit + // istanbul ignore next if (attrs?.onConfirmSignUpSubmit) { emit('confirmSignUpSubmit', e); } else { @@ -54,6 +61,8 @@ const submit = (e: Event): void => { }; const onLostCodeClicked = (): void => { + // TODO(BREAKING): remove unused emit + // istanbul ignore next if (attrs?.onLostCodeClicked) { emit('lostCodeClicked'); } else { diff --git a/packages/vue/src/components/confirm-verify-user.vue b/packages/vue/src/components/confirm-verify-user.vue index 35e5eb6889e..e5ce96fb72f 100644 --- a/packages/vue/src/components/confirm-verify-user.vue +++ b/packages/vue/src/components/confirm-verify-user.vue @@ -1,27 +1,25 @@ @@ -71,7 +67,7 @@ const onSkipClicked = (): void => { @@ -83,15 +79,15 @@ const onSkipClicked = (): void => { - - {{ translate(actorState?.context.remoteError) }} + + {{ translate(error) }} {{ submitText }} { type="button" @click.prevent="onSkipClicked" > - {{ skipText }} + {{ skipText }} + - {{ backSignInText }} + {{ backSignInText }} + getSendCodeText()); // Methods const onResetPasswordSubmit = (e: Event): void => { + // TODO(BREAKING): remove unused emit + // istanbul ignore next if (attrs?.onResetPasswordSubmit) { emit('resetPasswordSubmit', e); } else { - submit(e); + submitForm(getFormDataFromEvent(e)); } }; -const submit = (e: Event): void => { - submitForm(getFormDataFromEvent(e)); -}; - const onInput = (e: Event): void => { const { name, value } = e.target as HTMLInputElement; - send({ - type: 'CHANGE', - data: { name, value }, - }); + updateForm({ name, value }); }; const onBackToSignInClicked = (): void => { + // TODO(BREAKING): remove unused emit + // istanbul ignore next if (attrs?.onBackToSignInClicked) { emit('backToSignInClicked'); } else { - send({ - type: 'SIGN_IN', - }); + toSignIn(); } }; @@ -87,8 +86,9 @@ const onBackToSignInClicked = (): void => { :variation="'primary'" type="submit" :disabled="isPending" - >{{ sendCodeText }} + {{ sendCodeText }} + { type="button" @click.prevent="onBackToSignInClicked" > - {{ backSignInText }} + {{ backSignInText }} + -import { onMounted, reactive, computed, ComputedRef, useAttrs, ref } from 'vue'; +import { computed, ref, toRefs, useAttrs, onMounted, reactive } from 'vue'; import QRCode from 'qrcode'; import { Logger } from 'aws-amplify'; import { authenticatorTextUtil, - getActorState, getFormDataFromEvent, - SignInState, translate, getTotpCodeURL, } from '@aws-amplify/ui'; -import { useAuth, useAuthenticator } from '../composables/useAuth'; +import { useAuthenticator } from '../composables/useAuth'; +import { UseAuthenticator } from '../types'; import BaseFormFields from './primitives/base-form-fields.vue'; const logger = new Logger('SetupTOTP-logger'); -const props = useAuthenticator(); +// `facade` is manually typed to `UseAuthenticator` for temporary type safety. +const facade: UseAuthenticator = useAuthenticator(); +const { updateForm, submitForm, toSignIn } = facade; +const { error, isPending, QRFields, totpSecretCode, user } = toRefs(facade); const attrs = useAttrs(); -const emit = defineEmits(['confirmSetupTOTPSubmit', 'backToSignInClicked']); - -const { state, send } = useAuth(); -const { - value: { context }, -} = state; -const actorState = computed(() => - getActorState(state.value) -) as ComputedRef; -const { totpSecretCode, user } = actorState.value.context; +/** @deprecated Component events are deprecated and not maintained. */ +const emit = defineEmits(['confirmSetupTOTPSubmit', 'backToSignInClicked']); -const formOverrides = context?.config?.formFields?.setupTOTP; -const { totpIssuer = 'AWSCognito', totpUsername = user?.username } = - formOverrides?.['QR'] ?? {}; +const { totpIssuer = 'AWSCognito', totpUsername = user.value.username } = + QRFields.value ?? {}; const totpCodeURL = - typeof totpSecretCode === 'string' && typeof totpUsername === 'string' - ? getTotpCodeURL(totpIssuer, totpUsername, totpSecretCode) + totpSecretCode.value && totpUsername + ? getTotpCodeURL(totpIssuer, totpUsername, totpSecretCode.value) : null; const qrCode = reactive({ @@ -53,15 +46,15 @@ const { getCopyText, getCopiedText, getBackToSignInText, getConfirmText } = const copyTextLabel = ref(getCopyText()); function copyText() { - if (typeof totpSecretCode === 'string') { - navigator.clipboard.writeText(totpSecretCode); + if (totpSecretCode.value) { + navigator.clipboard.writeText(totpSecretCode.value); } copyTextLabel.value = getCopiedText(); } // lifecycle hooks onMounted(async () => { - if (!user || !totpCodeURL) { + if (!user.value || !totpCodeURL) { return; } try { @@ -80,26 +73,26 @@ const confirmText = computed(() => getConfirmText()); // Methods const onInput = (e: Event): void => { const { name, value } = e.target as HTMLInputElement; - send({ type: 'CHANGE', data: { name, value } }); + updateForm({ name, value }); }; const onSetupTOTPSubmit = (e: Event): void => { + // TODO(BREAKING): remove unused emit + // istanbul ignore next if (attrs?.onConfirmSetupTOTPSubmit) { emit('confirmSetupTOTPSubmit', e); } else { - submit(e); + submitForm(getFormDataFromEvent(e)); } }; -const submit = (e: Event): void => { - props.submitForm(getFormDataFromEvent(e)); -}; - const onBackToSignInClicked = (): void => { + // TODO(BREAKING): remove unused emit + // istanbul ignore next if (attrs?.onBackToSignInClicked) { emit('backToSignInClicked'); } else { - send({ type: 'SIGN_IN' }); + toSignIn(); } }; @@ -114,7 +107,7 @@ const onBackToSignInClicked = (): void => { > @@ -156,8 +149,8 @@ const onBackToSignInClicked = (): void => { - - {{ translate(actorState.context.remoteError) }} + + {{ translate(error) }} { :loading="false" :variation="'primary'" type="submit" - :disabled="actorState.matches('confirmSignIn.pending')" + :disabled="isPending" > {{ confirmText }} @@ -178,8 +171,8 @@ const onBackToSignInClicked = (): void => { type="button" @click.prevent="onBackToSignInClicked" > - {{ backSignInText }} + {{ backSignInText }} + -import { computed, ComputedRef, useAttrs } from 'vue'; +import { computed, toRefs, useAttrs } from 'vue'; import { authenticatorTextUtil, - getActorState, getFormDataFromEvent, - SignInState, translate, } from '@aws-amplify/ui'; -import FederatedSignIn from './federated-sign-in.vue'; - -// @xstate -import { useAuth, useAuthenticator } from '../composables/useAuth'; +import { useAuthenticator } from '../composables/useAuth'; +import { UseAuthenticator } from '../types'; import BaseFormFields from './primitives/base-form-fields.vue'; +import FederatedSignIn from './federated-sign-in.vue'; -const props = useAuthenticator(); +// `facade` is manually typed to `UseAuthenticator` for temporary type safety. +const facade: UseAuthenticator = useAuthenticator(); +const { submitForm, updateForm, toResetPassword } = facade; +const { error, isPending } = toRefs(facade); const attrs = useAttrs(); + +/** @deprecated Component events are deprecated and not maintained. */ const emit = defineEmits([ 'signInSubmit', 'forgotPasswordClicked', @@ -31,41 +33,31 @@ const { getForgotPasswordText, getSignInText, getSigningInText } = // Computed Properties const forgotYourPasswordLink = computed(() => getForgotPasswordText()); const signInButtonText = computed(() => getSignInText()); -const signIngButtonText = computed(() => getSigningInText()); - -const { state, send } = useAuth(); -const actorState = computed(() => - getActorState(state.value) -) as ComputedRef; +const signingInButtonText = computed(() => getSigningInText()); // Methods - const onInput = (e: Event): void => { const { name, value } = e.target as HTMLInputElement; - send({ - type: 'CHANGE', - //@ts-ignore - data: { name, value }, - }); + updateForm({ name, value }); }; const onSignInSubmit = (e: Event): void => { + // TODO(BREAKING): remove unused emit + // istanbul ignore next if (attrs?.onSignInSubmit) { emit('signInSubmit', e); } else { - submit(e); + submitForm(getFormDataFromEvent(e)); } }; -const submit = (e: Event): void => { - props.submitForm(getFormDataFromEvent(e)); -}; - const onForgotPasswordClicked = (): void => { + // TODO(BREAKING): remove unused emit + // istanbul ignore next if (attrs?.onForgotPasswordClicked) { emit('forgotPasswordClicked'); } else { - send({ type: 'RESET_PASSWORD' }); + toResetPassword(); } }; @@ -94,7 +86,7 @@ const onForgotPasswordClicked = (): void => {