From fe4e854ea8524b637feb7d7a7cbc0878b759e203 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 3 Dec 2024 13:06:30 -0800 Subject: [PATCH 01/15] chore(vue): Add custom pages and links --- packages/vue/src/components/uiComponents.ts | 132 ++++++++++++++++++-- packages/vue/src/errors/messages.ts | 20 ++- packages/vue/src/keys.ts | 10 +- packages/vue/src/types.ts | 27 ++++ packages/vue/src/utils/useCustomPages.ts | 131 +++++++++++++++++++ 5 files changed, 309 insertions(+), 11 deletions(-) create mode 100644 packages/vue/src/utils/useCustomPages.ts diff --git a/packages/vue/src/components/uiComponents.ts b/packages/vue/src/components/uiComponents.ts index 26afc929242..8f0deacf770 100644 --- a/packages/vue/src/components/uiComponents.ts +++ b/packages/vue/src/components/uiComponents.ts @@ -19,10 +19,24 @@ import { userButtonMenuActionRenderedError, userButtonMenuItemsRenderedError, userButtonMenuLinkRenderedError, + userProfileLinkRenderedError, + userProfilePageRenderedError, } from '../errors/messages'; -import { UserButtonInjectionKey, UserButtonMenuItemsInjectionKey } from '../keys'; -import type { CustomPortalsRendererProps, UserButtonActionProps, UserButtonLinkProps } from '../types'; +import { + OrganizationProfileInjectionKey, + UserButtonInjectionKey, + UserButtonMenuItemsInjectionKey, + UserProfileInjectionKey, +} from '../keys'; +import type { + CustomPortalsRendererProps, + UserButtonActionProps, + UserButtonLinkProps, + UserProfileLinkProps, + UserProfilePageProps, +} from '../types'; import { useUserButtonCustomMenuItems } from '../utils/useCustomMenuItems'; +import { useUserProfileCustomPages } from '../utils/useCustomPages'; import { ClerkLoaded } from './controlComponents'; type AnyObject = Record; @@ -71,16 +85,70 @@ const Portal = defineComponent((props: MountProps) => { return () => h(ClerkLoaded, () => h('div', { ref: portalRef })); }); -export const UserProfile = defineComponent((props: UserProfileProps) => { +const _UserProfile = defineComponent((props: UserProfileProps, { slots }) => { const clerk = useClerk(); + const { customPages, customPagesPortals, addCustomPage } = useUserProfileCustomPages(); - return () => + const finalProps = computed(() => ({ + ...props, + customPages: customPages.value, + })); + + provide(UserProfileInjectionKey, { + addCustomPage, + }); + + return () => [ h(Portal, { mount: clerk.value?.mountUserProfile, unmount: clerk.value?.unmountUserProfile, updateProps: (clerk.value as any)?.__unstable__updateProps, + props: finalProps.value, + }), + h(CustomPortalsRenderer, { customPagesPortals: customPagesPortals.value }), + slots.default?.(), + ]; +}); + +export const UserProfilePage = defineComponent( + (props: UserProfilePageProps, { slots }) => { + const ctx = inject(UserProfileInjectionKey); + if (!ctx) { + return errorThrower.throw(userProfilePageRenderedError); + } + + ctx.addCustomPage({ props, + slots, + component: UserProfilePage, + }); + + return () => null; + }, + { name: 'UserProfilePage' }, +); + +export const UserProfileLink = defineComponent( + (props: UserProfileLinkProps, { slots }) => { + const ctx = inject(UserProfileInjectionKey); + if (!ctx) { + return errorThrower.throw(userProfileLinkRenderedError); + } + + ctx.addCustomPage({ + props, + slots, + component: UserProfileLink, }); + + return () => null; + }, + { name: 'UserProfileLink' }, +); + +export const UserProfile = Object.assign(_UserProfile, { + Page: UserProfilePage, + Link: UserProfileLink, }); type UserButtonPropsWithoutCustomMenuItems = Without; @@ -89,16 +157,23 @@ const _UserButton = defineComponent((props: UserButtonPropsWithoutCustomMenuItem const clerk = useClerk(); const { customMenuItems, customMenuItemsPortals, addCustomMenuItem } = useUserButtonCustomMenuItems(); + const { customPages, customPagesPortals, addCustomPage } = useUserProfileCustomPages(); const finalProps = computed(() => ({ ...props, + userProfileProps: { + ...(props.userProfileProps || {}), + customPages: customPages.value, + }, customMenuItems: customMenuItems.value, - // TODO: Add custom pages })); provide(UserButtonInjectionKey, { addCustomMenuItem, }); + provide(UserProfileInjectionKey, { + addCustomPage, + }); return () => [ h(Portal, { @@ -108,7 +183,7 @@ const _UserButton = defineComponent((props: UserButtonPropsWithoutCustomMenuItem props: finalProps.value, }), h(CustomPortalsRenderer, { - // TODO: Add custom pages portals + customPagesPortals: customPagesPortals.value, customMenuItemsPortals: customMenuItemsPortals.value, }), slots.default?.(), @@ -166,7 +241,7 @@ export const UserButton = Object.assign(_UserButton, { MenuItems, Action: MenuAction, Link: MenuLink, - // TODO: Add custom pages + UserProfilePage, }); export const GoogleOneTap = defineComponent((props: GoogleOneTapProps) => { @@ -239,7 +314,43 @@ export const OrganizationList = defineComponent((props: OrganizationListProps) = }); }); -export const OrganizationProfile = defineComponent((props: OrganizationProfileProps) => { +export const OrganizationProfilePage = defineComponent( + (props: UserProfilePageProps, { slots }) => { + const ctx = inject(OrganizationProfileInjectionKey); + if (!ctx) { + return errorThrower.throw(userProfilePageRenderedError); + } + + ctx.addCustomPage({ + props, + slots, + component: OrganizationProfilePage, + }); + + return () => null; + }, + { name: 'OrganizationProfilePage' }, +); + +export const OrganizationProfileLink = defineComponent( + (props: UserProfileLinkProps, { slots }) => { + const ctx = inject(OrganizationProfileInjectionKey); + if (!ctx) { + return errorThrower.throw(userProfileLinkRenderedError); + } + + ctx.addCustomPage({ + props, + slots, + component: OrganizationProfileLink, + }); + + return () => null; + }, + { name: 'OrganizationProfileLink' }, +); + +const _OrganizationProfile = defineComponent((props: OrganizationProfileProps) => { const clerk = useClerk(); return () => @@ -251,6 +362,11 @@ export const OrganizationProfile = defineComponent((props: OrganizationProfilePr }); }); +export const OrganizationProfile = Object.assign(_OrganizationProfile, { + Page: OrganizationProfilePage, + Link: OrganizationProfileLink, +}); + export const Waitlist = defineComponent((props: WaitlistProps) => { const clerk = useClerk(); diff --git a/packages/vue/src/errors/messages.ts b/packages/vue/src/errors/messages.ts index aad303f8098..b294b8b2603 100644 --- a/packages/vue/src/errors/messages.ts +++ b/packages/vue/src/errors/messages.ts @@ -14,10 +14,26 @@ export const userButtonMenuLinkRenderedError = ' component needs to be a direct child of ``.'; export const userButtonMenuItemLinkWrongProps = - 'Missing requirements. component requires props: href, label and slot: labelIcon'; + 'Missing requirements. component requires props: href, label and slots: labelIcon.'; export const userButtonMenuItemActionWrongProps = - 'Missing requirements. component requires props: label and slot: labelIcon'; + 'Missing requirements. component requires props: label and slots: labelIcon.'; export const userButtonMenuItemsRenderedError = ' component needs to be a direct child of ``.'; + +export const customPageWrongProps = (componentName: string) => + `Missing requirements. <${componentName}.Page /> component requires props: url, label and slots: labelIcon and a default slot for page content`; + +export const customLinkWrongProps = (componentName: string) => + `Missing requirements. <${componentName}.Link /> component requires the following props: url, label and slots: labelIcon.`; + +export const userProfilePageRenderedError = + ' component needs to be a direct child of `` or ``.'; +export const userProfileLinkRenderedError = + ' component needs to be a direct child of `` or ``.'; + +export const organizationProfilePageRenderedError = + ' component needs to be a direct child of `` or ``.'; +export const organizationProfileLinkRenderedError = + ' component needs to be a direct child of `` or ``.'; diff --git a/packages/vue/src/keys.ts b/packages/vue/src/keys.ts index 1958d1ad57f..7a5ca1f5d38 100644 --- a/packages/vue/src/keys.ts +++ b/packages/vue/src/keys.ts @@ -1,6 +1,6 @@ import type { InjectionKey } from 'vue'; -import type { AddCustomMenuItemParams, VueClerkInjectionKeyType } from './types'; +import type { AddCustomMenuItemParams, AddCustomPagesParams, VueClerkInjectionKeyType } from './types'; export const ClerkInjectionKey = Symbol('clerk') as InjectionKey; @@ -11,3 +11,11 @@ export const UserButtonInjectionKey = Symbol('UserButton') as InjectionKey<{ export const UserButtonMenuItemsInjectionKey = Symbol('UserButton.MenuItems') as InjectionKey<{ addCustomMenuItem(params: AddCustomMenuItemParams): void; }>; + +export const UserProfileInjectionKey = Symbol('UserProfile') as InjectionKey<{ + addCustomPage(params: AddCustomPagesParams): void; +}>; + +export const OrganizationProfileInjectionKey = Symbol('OrganizationProfile') as InjectionKey<{ + addCustomPage(params: AddCustomPagesParams): void; +}>; diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index ed1a5c71aa4..da32f0e3e30 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -5,6 +5,7 @@ import type { ClerkOptions, ClientResource, CustomMenuItem, + CustomPage, OrganizationCustomPermissionKey, OrganizationCustomRoleKey, OrganizationResource, @@ -57,6 +58,32 @@ export type AddCustomMenuItemParams = { component: Component; }; +export type AddCustomPagesParams = { + props: CustomItemOrPageWithoutHandler; + slots: { + default?: Slot; + labelIcon?: Slot; + }; + component: Component; +}; + +type PageProps = + | { + label: string; + url: string; + } + | { + label: T; + url?: never; + }; + +export type UserProfilePageProps = PageProps<'account' | 'security'>; + +export type UserProfileLinkProps = { + url: string; + label: string; +}; + type ButtonActionProps = | { label: string; diff --git a/packages/vue/src/utils/useCustomPages.ts b/packages/vue/src/utils/useCustomPages.ts new file mode 100644 index 00000000000..6f83745cc7e --- /dev/null +++ b/packages/vue/src/utils/useCustomPages.ts @@ -0,0 +1,131 @@ +import { logErrorInDevMode } from '@clerk/shared/utils'; +import type { CustomPage } from '@clerk/types'; +import type { Component } from 'vue'; +import { ref } from 'vue'; + +import { UserProfileLink, UserProfilePage } from '../components/uiComponents'; +import { customLinkWrongProps, customPageWrongProps } from '../errors/messages'; +import type { AddCustomPagesParams } from '../types'; +import { isThatComponent } from './componentValidation'; +import { useCustomElementPortal } from './useCustomElementPortal'; + +export const useUserProfileCustomPages = () => { + const { customPages, customPagesPortals, addCustomPage } = useCustomPages({ + reorderItemsLabels: ['account', 'security'], + PageComponent: UserProfilePage, + LinkComponent: UserProfileLink, + componentName: 'UserProfile', + }); + + const addUserProfileCustomPage = (params: AddCustomPagesParams) => { + return addCustomPage(params); + }; + + return { + customPages, + customPagesPortals, + addCustomPage: addUserProfileCustomPage, + }; +}; + +export const useOrganizationProfileCustomPages = () => { + const { customPages, customPagesPortals, addCustomPage } = useCustomPages({ + reorderItemsLabels: ['general', 'members'], + PageComponent: UserProfilePage, + LinkComponent: UserProfileLink, + componentName: 'OrganizationProfile', + }); + + const addOrganizationProfileCustomPage = (params: AddCustomPagesParams) => { + return addCustomPage(params); + }; + + return { + customPages, + customPagesPortals, + addCustomPage: addOrganizationProfileCustomPage, + }; +}; + +type UseCustomPagesParams = { + LinkComponent: Component; + PageComponent: Component; + reorderItemsLabels: string[]; + componentName: string; +}; + +export const useCustomPages = (customPagesParams: UseCustomPagesParams) => { + const customPages = ref([]); + const { portals: customPagesPortals, mount, unmount } = useCustomElementPortal(); + const { PageComponent, LinkComponent, reorderItemsLabels, componentName } = customPagesParams; + + const addCustomPage = (params: AddCustomPagesParams) => { + const { props, slots, component } = params; + const { label, url } = props; + + if (isThatComponent(component, PageComponent)) { + if (isReorderItem(props, slots, reorderItemsLabels)) { + // This is a reordering item + customPages.value.push({ label }); + } else if (isCustomPage(props, slots)) { + // This is a custom page + customPages.value.push({ + label, + url, + mountIcon(el) { + mount(el, slots.labelIcon!); + }, + unmountIcon: unmount, + mount(el) { + mount(el, slots.default!); + }, + unmount, + }); + } else { + logErrorInDevMode(customPageWrongProps(componentName)); + return; + } + } + + if (isThatComponent(component, LinkComponent)) { + if (isExternalLink(props, slots)) { + // This is an external link + customPages.value.push({ + label, + url, + mountIcon(el) { + mount(el, slots.labelIcon!); + }, + unmountIcon: unmount, + }); + } else { + logErrorInDevMode(customLinkWrongProps(componentName)); + return; + } + } + }; + + return { + customPages, + customPagesPortals, + addCustomPage, + }; +}; + +const isReorderItem = (props: any, slots: AddCustomPagesParams['slots'], validItems: string[]): boolean => { + const { label, url } = props; + const { default: defaultSlot, labelIcon } = slots; + return !defaultSlot && !url && !labelIcon && validItems.some(v => v === label); +}; + +const isCustomPage = (props: any, slots: AddCustomPagesParams['slots']): boolean => { + const { label, url } = props; + const { default: defaultSlot, labelIcon } = slots; + return !!defaultSlot && !!url && !!labelIcon && !!label; +}; + +const isExternalLink = (props: any, slots: AddCustomPagesParams['slots']): boolean => { + const { label, url } = props; + const { default: defaultSlot, labelIcon } = slots; + return !defaultSlot && !!url && !!labelIcon && !!label; +}; From efa65ec7851b7dfa7024e6edb43e96053b8d0c45 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 3 Dec 2024 13:34:18 -0800 Subject: [PATCH 02/15] chore(vue): Use correct page and link components for organization --- packages/vue/src/utils/useCustomPages.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/vue/src/utils/useCustomPages.ts b/packages/vue/src/utils/useCustomPages.ts index 6f83745cc7e..2ced0a7f096 100644 --- a/packages/vue/src/utils/useCustomPages.ts +++ b/packages/vue/src/utils/useCustomPages.ts @@ -3,7 +3,12 @@ import type { CustomPage } from '@clerk/types'; import type { Component } from 'vue'; import { ref } from 'vue'; -import { UserProfileLink, UserProfilePage } from '../components/uiComponents'; +import { + OrganizationProfileLink, + OrganizationProfilePage, + UserProfileLink, + UserProfilePage, +} from '../components/uiComponents'; import { customLinkWrongProps, customPageWrongProps } from '../errors/messages'; import type { AddCustomPagesParams } from '../types'; import { isThatComponent } from './componentValidation'; @@ -31,8 +36,8 @@ export const useUserProfileCustomPages = () => { export const useOrganizationProfileCustomPages = () => { const { customPages, customPagesPortals, addCustomPage } = useCustomPages({ reorderItemsLabels: ['general', 'members'], - PageComponent: UserProfilePage, - LinkComponent: UserProfileLink, + PageComponent: OrganizationProfilePage, + LinkComponent: OrganizationProfileLink, componentName: 'OrganizationProfile', }); From d632900c6056aefac51452a41be07051386a5abe Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 3 Dec 2024 13:39:17 -0800 Subject: [PATCH 03/15] test(vue): Add e2e test for custom page inside user button --- .../vue-vite/src/components/CustomUserButton.vue | 12 ++++++++++++ integration/tests/vue/components.test.ts | 10 ++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/integration/templates/vue-vite/src/components/CustomUserButton.vue b/integration/templates/vue-vite/src/components/CustomUserButton.vue index c973ec12ed5..a398a2a7c15 100644 --- a/integration/templates/vue-vite/src/components/CustomUserButton.vue +++ b/integration/templates/vue-vite/src/components/CustomUserButton.vue @@ -18,6 +18,11 @@ const isActionClicked = ref(false);
Icon
+ + +
+ +
Icon
+
+

Custom Terms Page

+

This is the custom terms page

+
+
Is action clicked: {{ isActionClicked }}
diff --git a/integration/tests/vue/components.test.ts b/integration/tests/vue/components.test.ts index af6c01f4ae1..088a8823da6 100644 --- a/integration/tests/vue/components.test.ts +++ b/integration/tests/vue/components.test.ts @@ -66,13 +66,19 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te await u.po.userButton.waitForPopover(); // Check if custom menu items are visible - await u.po.userButton.toHaveVisibleMenuItems([/Custom link/i, /Custom action/i]); + await u.po.userButton.toHaveVisibleMenuItems([/Custom link/i, /Custom page/i, /Custom action/i]); // Click custom action await u.page.getByRole('menuitem', { name: /Custom action/i }).click(); await expect(u.page.getByText('Is action clicked: true')).toBeVisible(); - // Trigger the popover again + // Click custom action and check for custom page availbility + await u.page.getByRole('menuitem', { name: /Custom page/i }).click(); + await u.po.userProfile.waitForUserProfileModal(); + await expect(u.page.getByRole('heading', { name: 'Custom Terms Page' })).toBeVisible(); + + // Close the modal and trigger the popover again + await u.page.locator('.cl-modalCloseButton').click(); await u.po.userButton.toggleTrigger(); await u.po.userButton.waitForPopover(); From ab7d4136a264c4f50b8fab1c26bdac3d7581097b Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 3 Dec 2024 13:39:57 -0800 Subject: [PATCH 04/15] test(vue): Retrigger popover --- integration/tests/vue/components.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integration/tests/vue/components.test.ts b/integration/tests/vue/components.test.ts index 088a8823da6..4f68a04a531 100644 --- a/integration/tests/vue/components.test.ts +++ b/integration/tests/vue/components.test.ts @@ -72,6 +72,10 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te await u.page.getByRole('menuitem', { name: /Custom action/i }).click(); await expect(u.page.getByText('Is action clicked: true')).toBeVisible(); + // Trigger the popover again + await u.po.userButton.toggleTrigger(); + await u.po.userButton.waitForPopover(); + // Click custom action and check for custom page availbility await u.page.getByRole('menuitem', { name: /Custom page/i }).click(); await u.po.userProfile.waitForUserProfileModal(); From 1b2a9a35001ae71dcb1d1fc3f29134730b89e4b1 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 3 Dec 2024 13:40:28 -0800 Subject: [PATCH 05/15] test(vue): Fix slots --- .../templates/vue-vite/src/components/CustomUserButton.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/integration/templates/vue-vite/src/components/CustomUserButton.vue b/integration/templates/vue-vite/src/components/CustomUserButton.vue index a398a2a7c15..143f2a614a7 100644 --- a/integration/templates/vue-vite/src/components/CustomUserButton.vue +++ b/integration/templates/vue-vite/src/components/CustomUserButton.vue @@ -33,7 +33,9 @@ const isActionClicked = ref(false); -
Icon
+

Custom Terms Page

This is the custom terms page

From 73a98dafb0b5322de6ab81f8d9baf8f6f9f7461a Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Wed, 4 Dec 2024 08:53:10 -0800 Subject: [PATCH 06/15] chore: add changeset --- .changeset/four-beans-argue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/four-beans-argue.md diff --git a/.changeset/four-beans-argue.md b/.changeset/four-beans-argue.md new file mode 100644 index 00000000000..efba861967d --- /dev/null +++ b/.changeset/four-beans-argue.md @@ -0,0 +1,5 @@ +--- +"@clerk/vue": patch +--- + +Add support for custom pages and links From 1e1448e64381f2f31e76a7ced29b52cd4e53dd33 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 4 Dec 2024 09:28:27 -0800 Subject: [PATCH 07/15] test(vue): Add tests for user and organization profile custom pages and links --- .../src/components/CustomUserButton.vue | 18 ++++-- integration/templates/vue-vite/src/router.ts | 13 ++++- .../custom-pages/OrganizationProfile.vue | 29 ++++++++++ .../src/views/custom-pages/UserProfile.vue | 29 ++++++++++ integration/tests/vue/components.test.ts | 55 +++++++++++++++++++ 5 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 integration/templates/vue-vite/src/views/custom-pages/OrganizationProfile.vue create mode 100644 integration/templates/vue-vite/src/views/custom-pages/UserProfile.vue diff --git a/integration/templates/vue-vite/src/components/CustomUserButton.vue b/integration/templates/vue-vite/src/components/CustomUserButton.vue index 143f2a614a7..3da06750280 100644 --- a/integration/templates/vue-vite/src/components/CustomUserButton.vue +++ b/integration/templates/vue-vite/src/components/CustomUserButton.vue @@ -18,7 +18,10 @@ const isActionClicked = ref(false);
Icon
- + @@ -32,14 +35,17 @@ const isActionClicked = ref(false); - + -
-

Custom Terms Page

-

This is the custom terms page

-
+
+

Custom Terms Page

+

This is the custom terms page

+
Is action clicked: {{ isActionClicked }}
diff --git a/integration/templates/vue-vite/src/router.ts b/integration/templates/vue-vite/src/router.ts index 245fa6427f0..d1058868be5 100644 --- a/integration/templates/vue-vite/src/router.ts +++ b/integration/templates/vue-vite/src/router.ts @@ -26,6 +26,16 @@ const routes = [ path: '/unstyled', component: () => import('./views/Unstyled.vue'), }, + { + name: 'CustomUserProfile', + path: '/custom-pages/user-profile', + component: () => import('./views/custom-pages/UserProfile.vue'), + }, + { + name: 'CustomOrganizationProfile', + path: '/custom-pages/organization-profile', + component: () => import('./views/custom-pages/CustomOrganizationProfile.vue'), + }, ]; const router = createRouter({ @@ -35,6 +45,7 @@ const router = createRouter({ router.beforeEach(async (to, _, next) => { const { isSignedIn, isLoaded } = useAuth(); + const authenticatedPages = ['Profile', 'Admin', 'CustomUserProfile', 'CustomOrganizationProfile']; if (!isLoaded.value) { await waitForClerkJsLoaded(isLoaded); @@ -42,7 +53,7 @@ router.beforeEach(async (to, _, next) => { if (isSignedIn.value && to.name === 'Sign in') { next('/profile'); - } else if (!isSignedIn.value && ['Profile', 'Admin'].includes(to.name as string)) { + } else if (!isSignedIn.value && authenticatedPages.includes(to.name)) { next('/sign-in'); } else { next(); diff --git a/integration/templates/vue-vite/src/views/custom-pages/OrganizationProfile.vue b/integration/templates/vue-vite/src/views/custom-pages/OrganizationProfile.vue new file mode 100644 index 00000000000..58a06bd5915 --- /dev/null +++ b/integration/templates/vue-vite/src/views/custom-pages/OrganizationProfile.vue @@ -0,0 +1,29 @@ + + + diff --git a/integration/templates/vue-vite/src/views/custom-pages/UserProfile.vue b/integration/templates/vue-vite/src/views/custom-pages/UserProfile.vue new file mode 100644 index 00000000000..39787a68f1a --- /dev/null +++ b/integration/templates/vue-vite/src/views/custom-pages/UserProfile.vue @@ -0,0 +1,29 @@ + + + diff --git a/integration/tests/vue/components.test.ts b/integration/tests/vue/components.test.ts index 4f68a04a531..5845f4a8922 100644 --- a/integration/tests/vue/components.test.ts +++ b/integration/tests/vue/components.test.ts @@ -121,6 +121,61 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te await expect(u.page.getByText(`Hello, ${fakeUser.firstName}`)).toBeVisible(); }); + test('render user profile with custom pages and links', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/sign-in'); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + await u.page.goToRelative('/custom/user-profile'); + await u.po.userProfile.waitForMounted(); + + // Check if custom pages and links are visible + await expect(u.page.getByRole('button', { name: /Terms/i })).toBeVisible(); + await expect(u.page.getByRole('button', { name: /Homepage/i })).toBeVisible(); + + // Navigate to custom page + await u.page.getByRole('button', { name: /Terms/i }).click(); + await expect(u.page.getByRole('heading', { name: 'Custom Terms Page' })).toBeVisible(); + + // Check reordered default label. Security tab is now the last item. + await u.page.locator('.cl-navbarButton').last().click(); + await expect(u.page.getByRole('heading', { name: 'Security' })).toBeVisible(); + + // Click custom link and check navigation + await u.page.getByRole('button', { name: /Homepage/i }).click(); + await u.page.waitForAppUrl('/'); + }); + + test('render organization profile with custom pages and links', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/sign-in'); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + await u.page.goToRelative('/custom/organization-profile'); + await u.po.organizationSwitcher.waitForMounted(); + await u.po.organizationSwitcher.waitForAnOrganizationToSelected(); + + // Check if custom pages and links are visible + await expect(u.page.getByRole('button', { name: /Terms/i })).toBeVisible(); + await expect(u.page.getByRole('button', { name: /Homepage/i })).toBeVisible(); + + // Navigate to custom page + await u.page.getByRole('button', { name: /Terms/i }).click(); + await expect(u.page.getByRole('heading', { name: 'Custom Terms Page' })).toBeVisible(); + + // Check reordered default label. General tab is now the last item. + await u.page.locator('.cl-navbarButton').last().click(); + await expect(u.page.getByRole('heading', { name: 'General' })).toBeVisible(); + + // Click custom link and check navigation + await u.page.getByRole('button', { name: /Homepage/i }).click(); + await u.page.waitForAppUrl('/'); + }); + test('redirects to sign-in when unauthenticated', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.page.goToRelative('/profile'); From e3f4fbb6f8742cc6493b174fa3d0e212c57825b6 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 4 Dec 2024 10:00:41 -0800 Subject: [PATCH 08/15] test(vue): Fix incorrect filename --- integration/templates/vue-vite/src/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/templates/vue-vite/src/router.ts b/integration/templates/vue-vite/src/router.ts index d1058868be5..db90e9a319a 100644 --- a/integration/templates/vue-vite/src/router.ts +++ b/integration/templates/vue-vite/src/router.ts @@ -34,7 +34,7 @@ const routes = [ { name: 'CustomOrganizationProfile', path: '/custom-pages/organization-profile', - component: () => import('./views/custom-pages/CustomOrganizationProfile.vue'), + component: () => import('./views/custom-pages/OrganizationProfile.vue'), }, ]; From 52ad52996d81a20075d4a8fe0e7c06a0ba506f2a Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 4 Dec 2024 10:18:19 -0800 Subject: [PATCH 09/15] test(vue): Fix routes --- integration/tests/vue/components.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/tests/vue/components.test.ts b/integration/tests/vue/components.test.ts index 5845f4a8922..b0eedadd61d 100644 --- a/integration/tests/vue/components.test.ts +++ b/integration/tests/vue/components.test.ts @@ -128,7 +128,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); await u.po.expect.toBeSignedIn(); - await u.page.goToRelative('/custom/user-profile'); + await u.page.goToRelative('/custom-pages/user-profile'); await u.po.userProfile.waitForMounted(); // Check if custom pages and links are visible @@ -155,7 +155,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); await u.po.expect.toBeSignedIn(); - await u.page.goToRelative('/custom/organization-profile'); + await u.page.goToRelative('/custom-pages/organization-profile'); await u.po.organizationSwitcher.waitForMounted(); await u.po.organizationSwitcher.waitForAnOrganizationToSelected(); From caacf7bf9a139f91c62ccc10edce9b71c9495692 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Wed, 4 Dec 2024 10:23:57 -0800 Subject: [PATCH 10/15] test(vue): Fix extract characters in template --- .../vue-vite/src/views/custom-pages/OrganizationProfile.vue | 2 +- .../templates/vue-vite/src/views/custom-pages/UserProfile.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/templates/vue-vite/src/views/custom-pages/OrganizationProfile.vue b/integration/templates/vue-vite/src/views/custom-pages/OrganizationProfile.vue index 58a06bd5915..ec52fc33aef 100644 --- a/integration/templates/vue-vite/src/views/custom-pages/OrganizationProfile.vue +++ b/integration/templates/vue-vite/src/views/custom-pages/OrganizationProfile.vue @@ -20,7 +20,7 @@ import { OrganizationProfile } from '@clerk/vue'; label="Homepage" url="/" > - <