Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VACMS-19386 Rate your experience, medallia and content footer updates #819

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 2 additions & 0 deletions additional.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ declare module '@department-of-veterans-affairs/component-library/Modal'
declare module '@department-of-veterans-affairs/component-library/Pagination'
declare module '@department-of-veterans-affairs/component-library/ProgressButton'
declare module '@department-of-veterans-affairs/component-library/PromoBanner'
declare module '@department-of-veterans-affairs/component-library/VaRadio'
declare module '@department-of-veterans-affairs/component-library/VaRadioOption'
declare module '@department-of-veterans-affairs/component-library/dist/react-bindings'
declare module 'mq-polyfill'
declare module 'debug'
Expand Down
87 changes: 71 additions & 16 deletions src/lib/utils/medallia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,101 @@ type KampyleOnsiteSdk = Window &
vaSurvey: string
}

const vagovstagingsurveys = {
'/search': 20,
'/contact-us/virtual-agent': 26,
const SURVEY_NUMBERS = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted this logic change to what is currently in production. Reminder that we don't have a way to test this until it gets to staging (known limitation with Medallia in environments).

DEFAULT_STAGING_SURVEY: 11,
DEFAULT_PROD_SURVEY: 17,
SEARCH_PROD: 21,
SEARCH_STAGING: 20,
CONTACT_US_VIRTUAL_AGENT_PROD: 25,
CONTACT_US_VIRTUAL_AGENT_STAGING: 26,
SCHOOL_ADMINISTRATORS_PROD: 17,
SCHOOL_ADMINISTRATORS_STAGING: 37,
HEALTH_CARE_PROD: 17,
HEALTH_CARE_STAGING: 41,
VISIT_SUMMARY_PROD: 17,
VISIT_SUMMARY_STAGING: 41,
}

const vagovprodsurveys = {
'/search': 21,
'/contact-us/virtual-agent': 25,
}

function trimSlash(url: string): string {
if (url.length === 0) return
return url.charAt(url.length - 1) === '/' ? url.slice(0, url.length - 1) : url
const medalliaSurveys = {
urls: {
'/search': {
production: SURVEY_NUMBERS.SEARCH_PROD,
staging: SURVEY_NUMBERS.SEARCH_STAGING,
},
'/contact-us/virtual-agent': {
production: SURVEY_NUMBERS.CONTACT_US_VIRTUAL_AGENT_PROD,
staging: SURVEY_NUMBERS.CONTACT_US_VIRTUAL_AGENT_STAGING,
},
'/school-administrators': {
production: SURVEY_NUMBERS.SCHOOL_ADMINISTRATORS_PROD,
staging: SURVEY_NUMBERS.SCHOOL_ADMINISTRATORS_STAGING,
},
},
urlsWithSubPaths: {
'/health-care': {
production: SURVEY_NUMBERS.HEALTH_CARE_PROD,
staging: SURVEY_NUMBERS.HEALTH_CARE_STAGING,
},
'/my-health/medical-records/summaries-and-notes/visit-summary': {
production: SURVEY_NUMBERS.VISIT_SUMMARY_PROD,
staging: SURVEY_NUMBERS.VISIT_SUMMARY_STAGING,
},
'/resources': {
production: SURVEY_NUMBERS.DEFAULT_PROD_SURVEY,
staging: SURVEY_NUMBERS.DEFAULT_STAGING_SURVEY,
},
},
}

export function getSurveyNumber(url: string, isProduction = false): number {
const pathUrl = trimSlash(url.toString())
if (isProduction) {
return vagovprodsurveys[pathUrl] ? vagovprodsurveys[pathUrl] : 17
} else {
return vagovstagingsurveys[pathUrl] ? vagovstagingsurveys[pathUrl] : 11
const defaultSurvey = isProduction
? SURVEY_NUMBERS.DEFAULT_PROD_SURVEY
: SURVEY_NUMBERS.DEFAULT_STAGING_SURVEY

const buildEnv = isProduction ? 'production' : 'staging'

if (typeof url !== 'string' || url === null) {
return defaultSurvey
}

// Check for exact path match
if (url in medalliaSurveys.urls) {
const surveyInfo = medalliaSurveys.urls[url]

return surveyInfo[buildEnv] || defaultSurvey
}

// Check for subpath match
for (const [subpath, surveyInfo] of Object.entries(
medalliaSurveys.urlsWithSubPaths
)) {
if (url.startsWith(subpath)) {
return surveyInfo[buildEnv] || defaultSurvey
}
}

return defaultSurvey
}

export function loadForm(formNumber: number): boolean {
const KAMPYLE_ONSITE_SDK = (window as KampyleOnsiteSdk).KAMPYLE_ONSITE_SDK

if (KAMPYLE_ONSITE_SDK) {
return KAMPYLE_ONSITE_SDK.loadForm(formNumber)
}
}

export function showForm(formNumber: number): boolean {
const KAMPYLE_ONSITE_SDK = (window as KampyleOnsiteSdk).KAMPYLE_ONSITE_SDK

if (KAMPYLE_ONSITE_SDK) {
return KAMPYLE_ONSITE_SDK.showForm(formNumber)
}
}

export function onMedalliaLoaded(callback: () => void): void {
const KAMPYLE_ONSITE_SDK = (window as KampyleOnsiteSdk).KAMPYLE_ONSITE_SDK

if (KAMPYLE_ONSITE_SDK) {
callback()
} else {
Expand Down
124 changes: 48 additions & 76 deletions src/templates/common/contentFooter/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import React, { useEffect } from 'react'
import React from 'react'
import { parseDate, getDateParts } from '@/lib/utils/date'
import { MedalliaAssets } from '@/templates/common/medallia'
import { getSurveyNumber, showForm } from '@/lib/utils/medallia'
import { BUILD_TYPES } from '@/lib/constants/environment'

// "Back to top", "Last updated" date and Medallia feedback button

type ContentFooterProps = {
lastUpdated?: string | number
// responsiveLayout should be passed by the "landing_page" content type only
responsiveLayout?: string
}

function formatDate(date: Date, format: 'display' | 'machine'): string {
const dateParts = getDateParts(date)

if (format === 'display') {
const month = dateParts.month?.name
const day = dateParts.day?.numeric
Expand All @@ -33,91 +38,58 @@ function formatDate(date: Date, format: 'display' | 'machine'): string {
}
}

function FeedbackButton() {
useEffect(() => {
const vaButton = document.getElementById('mdFormButton') as HTMLElement & {
shadowRoot?: ShadowRoot
}
if (vaButton && vaButton.shadowRoot) {
const shadowRoot = vaButton.shadowRoot

// Create a new observer
const observer = new MutationObserver(() => {
const button = shadowRoot.querySelector(
'.usa-button'
) as HTMLButtonElement
if (button) {
button.classList.add('feedback-button')
observer.disconnect() // Stop observing once we've found the button and added the class
}
})

// Start observing the shadow root with the configured parameters
observer.observe(shadowRoot, { childList: true, subtree: true })
}
}, [])
return (
<>
<MedalliaAssets />
<button
className="feedback-button usa-button"
id="mdFormButton"
onClick={() => {
const isProduction =
process.env.NEXT_PUBLIC_BUILD_TYPE === BUILD_TYPES.PROD
const surveyNumber = getSurveyNumber(
window.location.pathname,
isProduction
)
showForm(surveyNumber)
}}
>
Feedback
</button>
</>
)
}

export function ContentFooter({ lastUpdated }: ContentFooterProps) {
export function ContentFooter({
lastUpdated,
responsiveLayout,
}: ContentFooterProps) {
let displayDate, machineDate
let wrapperClasses = ''
const date = parseDate(lastUpdated)

if (date) {
displayDate = formatDate(date, 'display')
machineDate = formatDate(date, 'machine')
}

if (responsiveLayout === 'desktop') {
wrapperClasses = ' vads-u-display--none medium-screen:vads-u-display--block'
} else if (responsiveLayout === 'mobile') {
wrapperClasses = ' medium-screen:vads-u-display--none'
}

return (
<>
<div className="last-updated vads-u-padding-x--1 desktop-lg:vads-u-padding-x--0">
{displayDate && (
<div className="mobile-lg:vads-u-display--flex above-footer-elements-container">
<div className="vads-u-flex--auto">
<span className="vads-u-text-align--justify">
<p>
<>
Last updated:{' '}
<time dateTime={machineDate}>{displayDate}</time>
</>
</p>
</span>
</div>
<div className="vads-u-flex--1 vads-u-text-align--right">
<span className="vads-u-text-align--right">
<FeedbackButton />
</span>
</div>
</div>
)}
{!displayDate && (
<div className="vads-u-display--flex above-footer-elements-container">
<div className="vads-u-flex--1 vads-u-text-align--right">
<span className="vads-u-text-align--right">
<FeedbackButton />
</span>
</div>
<div
className={`last-updated usa-content vads-u-padding-x--1 desktop-lg:vads-u-padding-x--0${wrapperClasses}`}
>
<div className="mobile-lg:vads-u-display--flex above-footer-elements-container">
{displayDate && machineDate && (
<div className="vads-u-flex--auto">
<span className="vads-u-text-align--justify">
<p>
Last updated:&nbsp;
<time dateTime={machineDate}>{displayDate}</time>
</p>
</span>
</div>
)}
<div className="vads-u-flex--1 vads-u-text-align--right">
<MedalliaAssets />
<va-button
label="Give feedback"
id="mdFormButton"
onClick={() => {
const isProduction =
process.env.NEXT_PUBLIC_BUILD_TYPE === BUILD_TYPES.PROD
const surveyNumber = getSurveyNumber(
window.location.pathname,
isProduction
)
showForm(surveyNumber)
}}
text="Feedback"
/>
</div>
</div>
</>
</div>
)
}
61 changes: 9 additions & 52 deletions src/templates/components/rateYourExperience/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,22 @@
import { render } from '@testing-library/react'
import { RateYourExperience } from './index'
import userEvent from '@testing-library/user-event'
import { waitFor } from '@testing-library/react'
import { prettyDOM } from '@testing-library/react'
import { screen } from '@testing-library/dom'

describe('<RateYourExperience>', () => {
test('renders <RateYourExperience />', () => {
const { queryByText } = render(<RateYourExperience />)

const good = document.querySelector('#rate-your-experience--good')
const bad = document.querySelector('#rate-your-experience--bad')

expect(
queryByText(/How do you rate your experience on this page?/)
).toBeInTheDocument()
expect(good).toBeInTheDocument()
expect(bad).toBeInTheDocument()
})

test('shows error message when submitted without selection', async () => {
const user = userEvent.setup()
render(<RateYourExperience />)

const submitButton = document.querySelector(
'#rate-your-experience--rating-submit'
)
const errorMessage = document.querySelector(
'#rate-your-experience--error-message'
)
expect(errorMessage).toBeInTheDocument()
expect(errorMessage).toHaveClass('vads-u-display--none')

user.click(submitButton)
await waitFor(() =>
expect(errorMessage).not.toHaveClass('vads-u-display--none')
)
})

test('shows thank-you message when submitted with selection', async () => {
const user = userEvent.setup()
render(<RateYourExperience />)

const goodRatingInput = document.querySelector(
'#rate-your-experience--good'
const good = document.querySelector('#Good')
const bad = document.querySelector('#Bad')
const radioHeader = document.querySelector(
'[label="How do you rate your experience on this page?'
)
const submitButton = document.querySelector(
'#rate-your-experience--rating-submit'
'va-button[text="Submit feedback"]'
)
const thankYouMessage = document.querySelector(
'#rate-your-experience--thank-you-message'
)
expect(thankYouMessage).toBeInTheDocument()
expect(thankYouMessage).toHaveClass('vads-u-display--none')

user.click(goodRatingInput)
await waitFor(() => expect(goodRatingInput).toBeChecked())

user.click(submitButton)
await waitFor(() =>
expect(thankYouMessage).not.toHaveClass('vads-u-display--none')
)
expect(radioHeader).toBeInTheDocument()
expect(good).toBeInTheDocument()
expect(bad).toBeInTheDocument()
expect(submitButton).toBeInTheDocument()
})
})
Loading
Loading