Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion frontend/src/components/PageHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
14 changes: 4 additions & 10 deletions frontend/src/pages/UnverifiedEmail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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) {
Expand Down
16 changes: 15 additions & 1 deletion frontend/src/pages/team/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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)
},
Expand All @@ -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
Expand Down Expand Up @@ -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 = []

Expand Down Expand Up @@ -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 })
Expand Down
21 changes: 21 additions & 0 deletions test/unit/frontend/components/PageHeader.spec.js
Original file line number Diff line number Diff line change
@@ -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')
})
})
27 changes: 4 additions & 23 deletions test/unit/frontend/pages/UnverifiedEmail.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,14 @@ 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() }
}
})

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
}))
Expand Down Expand Up @@ -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({})
Expand All @@ -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)
Expand All @@ -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()
})
})
83 changes: 83 additions & 0 deletions test/unit/frontend/pages/team/TeamPage.spec.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
Loading