From 802d4d1a49e7c4f18c469da92fc26c412ffb5eff Mon Sep 17 00:00:00 2001 From: cstns Date: Fri, 23 Oct 2026 10:29:50 +0300 Subject: [PATCH] Don't show the classic welcome tour on the Expert home The welcome tour and the getting started modal walk the classic dashboard, which the Expert home doesn't have. The team page now withdraws the tour when AI onboarding or the Expert is on, the Welcome Tour menu entry is hidden on the Expert home, and the AI connector dialog waits until AI onboarding is done. The email verification page always queues the tour and leaves the decision to the team page, since no team is loaded for an unverified user. Closes #8938 --- frontend/src/components/PageHeader.vue | 4 +- frontend/src/pages/UnverifiedEmail.vue | 14 +--- frontend/src/pages/team/index.vue | 16 +++- .../frontend/components/PageHeader.spec.js | 21 +++++ .../frontend/pages/UnverifiedEmail.spec.js | 27 +----- .../unit/frontend/pages/team/TeamPage.spec.js | 83 +++++++++++++++++++ 6 files changed, 130 insertions(+), 35 deletions(-) create mode 100644 test/unit/frontend/components/PageHeader.spec.js create mode 100644 test/unit/frontend/pages/team/TeamPage.spec.js diff --git a/frontend/src/components/PageHeader.vue b/frontend/src/components/PageHeader.vue index c4ae5a4518..fcbb5bb227 100644 --- a/frontend/src/components/PageHeader.vue +++ b/frontend/src/components/PageHeader.vue @@ -179,7 +179,9 @@ export default { label: 'Welcome Tour', icon: CursorArrowRaysIcon, tag: 'welcome-tour', - onclick: this.startWelcomeTour + onclick: this.startWelcomeTour, + // The tour walks the classic dashboard, so it has nothing to point at on the Expert home + hidden: !!this.featuresCheck.isExpertAssistantFeatureEnabled }, { label: 'Sign Out', diff --git a/frontend/src/pages/UnverifiedEmail.vue b/frontend/src/pages/UnverifiedEmail.vue index bcd60ee5e4..dccebbe25e 100644 --- a/frontend/src/pages/UnverifiedEmail.vue +++ b/frontend/src/pages/UnverifiedEmail.vue @@ -34,7 +34,6 @@ import userApi from '../api/user.js' import FFLayoutBox from '../layouts/Box.vue' import { useAccountAuthStore } from '@/stores/account-auth.js' -import { useAccountSettingsStore } from '@/stores/account-settings.js' import { useUxToursStore } from '@/stores/ux-tours.js' import { useUxStore } from '@/stores/ux.js' @@ -52,22 +51,17 @@ export default { } }, computed: { - ...mapState(useAccountAuthStore, ['user']), - ...mapState(useAccountSettingsStore, ['featuresCheck']) + ...mapState(useAccountAuthStore, ['user']) }, methods: { ...mapActions(useUxStore, ['setNewlyCreatedUser']), - ...mapActions(useUxToursStore, ['presentTour', 'withdrawTour']), + ...mapActions(useUxToursStore, ['presentTour']), async submitVerificationToken () { try { await userApi.verifyEmailToken(this.token) clearTimeout(this.resendTimeout) - if (this.featuresCheck?.isAiOnboardingFeatureEnabled) { - // clear any tour queued under the standard flow so it cannot fire later - this.withdrawTour() - } else { - this.presentTour() - } + // No team is loaded yet for an unverified user, so the team page decides whether the tour fits + this.presentTour() this.setNewlyCreatedUser() this.$router.go() } catch (err) { diff --git a/frontend/src/pages/team/index.vue b/frontend/src/pages/team/index.vue index 90d5e59d2d..e280a57ccd 100644 --- a/frontend/src/pages/team/index.vue +++ b/frontend/src/pages/team/index.vue @@ -33,6 +33,7 @@ import { useAccountStore } from '@/stores/account.js' import { useContextStore } from '@/stores/context.js' import { useProductExpertStore } from '@/stores/product-expert.js' import { useUxToursStore } from '@/stores/ux-tours.js' +import { useUxStore } from '@/stores/ux.js' export default { name: 'TeamPage', @@ -59,6 +60,7 @@ export default { ...mapState(useAccountAuthStore, ['user', 'isAdminUser']), ...mapState(useUxToursStore, ['shouldPresentTour', 'activeTour', 'shouldShowEducationModal', 'shouldAutoShowAiConnectorModal']), ...mapState(useProductExpertStore, ['shouldWakeUpAssistant']), + ...mapState(useUxStore, ['isOnboarding']), isVisitingAdmin: function () { return (this.teamMembership.role === Roles.Admin) }, @@ -77,6 +79,9 @@ export default { !this.shouldPresentTour && !this.activeTour && !this.shouldShowEducationModal && + // Every new user starts in the onboarding stage, but it only means something on a team + // that gets AI onboarding, where the dialog waits until onboarding is complete + !(this.featuresCheck.isAiOnboardingFeatureEnabled && this.isOnboarding) && this.featuresCheck.isAiFeatureEnabled && this.featuresCheck.isMcpThirdPartyFeatureEnabled && this.shouldAutoShowAiConnectorModal @@ -126,7 +131,7 @@ export default { }, methods: { ...mapActions(useProductExpertStore, ['wakeUpAssistant']), - ...mapActions(useUxToursStore, ['setWelcomeTour', 'openModal']), + ...mapActions(useUxToursStore, ['setWelcomeTour', 'withdrawTour', 'openModal']), checkRoute: async function (route) { const allowedRoutes = [] @@ -157,6 +162,15 @@ export default { this.$nextTick(() => useAiConnectorModal().open()) }, dispatchTour () { + // The welcome tour and the getting started modal walk the classic dashboard, which the + // Expert home doesn't have, and AI onboarding covers the same ground by doing + if (this.featuresCheck.isAiOnboardingFeatureEnabled || this.featuresCheck.isExpertAssistantFeatureEnabled) { + this.withdrawTour() + if (this.shouldWakeUpAssistant) { + this.wakeUpAssistant({ shouldHydrateMessages: true }) + } + return + } return this.setWelcomeTour(() => { if (this.shouldWakeUpAssistant) { this.wakeUpAssistant({ shouldHydrateMessages: true }) diff --git a/test/unit/frontend/components/PageHeader.spec.js b/test/unit/frontend/components/PageHeader.spec.js new file mode 100644 index 0000000000..c6290a226c --- /dev/null +++ b/test/unit/frontend/components/PageHeader.spec.js @@ -0,0 +1,21 @@ +import { describe, expect, test } from 'vitest' + +import PageHeader from '../../../../frontend/src/components/PageHeader.vue' + +// navigationOptions only reads a few fields, so it runs against a plain object +function menuTags (featuresCheck) { + const header = { user: { admin: false }, featuresCheck, $router: { push: () => {} } } + return PageHeader.computed.navigationOptions.call(header).map(option => option.tag) +} + +describe('PageHeader user menu', () => { + test('offers the welcome tour on the classic home', () => { + expect(menuTags({})).toContain('welcome-tour') + }) + + test('hides the welcome tour on the Expert home', () => { + const tags = menuTags({ isExpertAssistantFeatureEnabled: true }) + expect(tags).not.toContain('welcome-tour') + expect(tags).toContain('getting-started') + }) +}) diff --git a/test/unit/frontend/pages/UnverifiedEmail.spec.js b/test/unit/frontend/pages/UnverifiedEmail.spec.js index d604e52a76..8d19bb0d6d 100644 --- a/test/unit/frontend/pages/UnverifiedEmail.spec.js +++ b/test/unit/frontend/pages/UnverifiedEmail.spec.js @@ -4,8 +4,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest' const mocks = vi.hoisted(() => { return { authStore: { user: { username: 'alice' } }, - settingsStore: { featuresCheck: {} }, - toursStore: { presentTour: vi.fn(), withdrawTour: vi.fn() }, + toursStore: { presentTour: vi.fn() }, uxStore: { setNewlyCreatedUser: vi.fn() } } }) @@ -13,9 +12,6 @@ const mocks = vi.hoisted(() => { vi.mock('@/stores/account-auth.js', () => ({ useAccountAuthStore: () => mocks.authStore })) -vi.mock('@/stores/account-settings.js', () => ({ - useAccountSettingsStore: () => mocks.settingsStore -})) vi.mock('@/stores/ux-tours.js', () => ({ useUxToursStore: () => mocks.toursStore })) @@ -49,9 +45,7 @@ function mountPage () { describe('UnverifiedEmail', () => { beforeEach(() => { - mocks.settingsStore.featuresCheck = {} mocks.toursStore.presentTour.mockClear() - mocks.toursStore.withdrawTour.mockClear() mocks.uxStore.setNewlyCreatedUser.mockClear() userApi.verifyEmailToken.mockClear() userApi.verifyEmailToken.mockResolvedValue({}) @@ -64,26 +58,14 @@ describe('UnverifiedEmail', () => { await flushPromises() } - test('queues the welcome tour on the classic path', async () => { + // No team is loaded for an unverified user, so the team page decides whether the tour fits + test('queues the welcome tour once verified', async () => { const wrapper = mountPage() await verify(wrapper) expect(mocks.toursStore.presentTour).toHaveBeenCalledTimes(1) - expect(mocks.toursStore.withdrawTour).not.toHaveBeenCalled() - }) - - // The tour and the education modal it opens on close both explain concepts - // the onboarding conversation covers by doing. A tour queued on an earlier - // visit persists in localStorage, so it gets actively withdrawn too - test('withdraws the welcome tour when AI onboarding is enabled', async () => { - mocks.settingsStore.featuresCheck = { isAiOnboardingFeatureEnabled: true } - const wrapper = mountPage() - await verify(wrapper) - expect(mocks.toursStore.presentTour).not.toHaveBeenCalled() - expect(mocks.toursStore.withdrawTour).toHaveBeenCalledTimes(1) }) - test('still marks the user as newly created either way', async () => { - mocks.settingsStore.featuresCheck = { isAiOnboardingFeatureEnabled: true } + test('marks the user as newly created', async () => { const wrapper = mountPage() await verify(wrapper) expect(mocks.uxStore.setNewlyCreatedUser).toHaveBeenCalledTimes(1) @@ -96,7 +78,6 @@ describe('UnverifiedEmail', () => { const wrapper = mountPage() await verify(wrapper) expect(mocks.toursStore.presentTour).not.toHaveBeenCalled() - expect(mocks.toursStore.withdrawTour).not.toHaveBeenCalled() expect(mocks.uxStore.setNewlyCreatedUser).not.toHaveBeenCalled() }) }) diff --git a/test/unit/frontend/pages/team/TeamPage.spec.js b/test/unit/frontend/pages/team/TeamPage.spec.js new file mode 100644 index 0000000000..c99a3246ab --- /dev/null +++ b/test/unit/frontend/pages/team/TeamPage.spec.js @@ -0,0 +1,83 @@ +import { describe, expect, test, vi } from 'vitest' + +import TeamPage from '../../../../../frontend/src/pages/team/index.vue' + +// dispatchTour only reads a few fields and calls store actions, so it runs against a plain object +function teamPage ({ featuresCheck = {}, shouldWakeUpAssistant = false } = {}) { + return { + featuresCheck, + shouldWakeUpAssistant, + setWelcomeTour: vi.fn(), + withdrawTour: vi.fn(), + wakeUpAssistant: vi.fn(), + openModal: vi.fn() + } +} + +describe('Team page welcome tour', () => { + test('starts the welcome tour on the classic home', () => { + const page = teamPage() + TeamPage.methods.dispatchTour.call(page) + expect(page.setWelcomeTour).toHaveBeenCalledTimes(1) + expect(page.withdrawTour).not.toHaveBeenCalled() + }) + + test('opens the getting started modal when the classic tour ends', () => { + const page = teamPage() + TeamPage.methods.dispatchTour.call(page) + page.setWelcomeTour.mock.calls[0][0]() + expect(page.openModal).toHaveBeenCalledWith('education') + }) + + test('withdraws the tour on the Expert home without opening the getting started modal', () => { + const page = teamPage({ featuresCheck: { isExpertAssistantFeatureEnabled: true } }) + TeamPage.methods.dispatchTour.call(page) + expect(page.setWelcomeTour).not.toHaveBeenCalled() + expect(page.withdrawTour).toHaveBeenCalledTimes(1) + expect(page.openModal).not.toHaveBeenCalled() + expect(page.wakeUpAssistant).not.toHaveBeenCalled() + }) + + test('withdraws the tour when the team gets AI onboarding', () => { + const page = teamPage({ featuresCheck: { isAiOnboardingFeatureEnabled: true } }) + TeamPage.methods.dispatchTour.call(page) + expect(page.setWelcomeTour).not.toHaveBeenCalled() + expect(page.withdrawTour).toHaveBeenCalledTimes(1) + }) + + test('still wakes the Expert when it is due and the tour is skipped', () => { + const page = teamPage({ featuresCheck: { isExpertAssistantFeatureEnabled: true }, shouldWakeUpAssistant: true }) + TeamPage.methods.dispatchTour.call(page) + expect(page.wakeUpAssistant).toHaveBeenCalledWith({ shouldHydrateMessages: true }) + expect(page.openModal).not.toHaveBeenCalled() + }) +}) + +// canAutoShowAiConnector only reads a few fields, so it runs against a plain object +function connectorReady ({ aiOnboarding = false, isOnboarding = false } = {}) { + const page = { + team: { id: 'team-1' }, + shouldPresentTour: false, + activeTour: null, + shouldShowEducationModal: false, + shouldAutoShowAiConnectorModal: true, + isOnboarding, + featuresCheck: { isAiFeatureEnabled: true, isMcpThirdPartyFeatureEnabled: true, isAiOnboardingFeatureEnabled: aiOnboarding } + } + return TeamPage.computed.canAutoShowAiConnector.call(page) +} + +describe('Team page AI connector dialog', () => { + test('waits while a team with AI onboarding is still onboarding', () => { + expect(connectorReady({ aiOnboarding: true, isOnboarding: true })).toBe(false) + }) + + test('shows once AI onboarding is complete', () => { + expect(connectorReady({ aiOnboarding: true, isOnboarding: false })).toBe(true) + }) + + // Every new user starts in the onboarding stage, which means nothing without AI onboarding + test('is not held by the onboarding stage on a team without AI onboarding', () => { + expect(connectorReady({ aiOnboarding: false, isOnboarding: true })).toBe(true) + }) +})