From 1fa80c7b73f3e439bdc9ba04c9a4009f04433d54 Mon Sep 17 00:00:00 2001 From: Tyler Nullmeier Date: Thu, 18 Sep 2025 13:38:52 -0500 Subject: [PATCH 01/23] First attempt at adding chat --- src/components/HelpMenu.tsx | 173 +++++++++++++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 3 deletions(-) diff --git a/src/components/HelpMenu.tsx b/src/components/HelpMenu.tsx index 46e0bfefb..afd7245c5 100644 --- a/src/components/HelpMenu.tsx +++ b/src/components/HelpMenu.tsx @@ -105,8 +105,161 @@ export interface HelpMenuProps { children?: React.ReactNode; } +declare global { + interface Window { + embeddedservice_bootstrap?: any; + } +} +export interface BusinessHours { + startTime: number; + endTime: number; +} + +export interface ChatContext extends BusinessHours { + openChat: () => void; +} + +function useScript(src: string): boolean { + const [ready, setReady] = React.useState(false); + const scriptRef = React.useRef(null); + + React.useEffect(() => { + // Already in the DOM? No need to add it again. + if (document.querySelector(`script[src="${src}"]`)) { + setReady(true); + return; + } + + const script = document.createElement('script'); + script.src = src; + script.async = true; + script.onload = () => setReady(true); + script.onerror = () => console.warn(`Failed to load ${src}`); + document.head.appendChild(script); + scriptRef.current = script; + + return () => { + scriptRef.current?.remove(); + }; + }, [src]); + + return ready; +} + +export function useWebMessagingDeployment() { + const [chatContext, setChatContext] = React.useState(null); + const orgId = '00DU0000000Kwch'; + const app = 'Web_Messaging_Deployment'; + const deployment = 'ESWWebMessagingDeployme1716235390398'; + const scrt2URL = 'https://openstax.my.salesforce-scrt.com'; + const scriptUrl = + `https://openstax.my.site.com/${deployment}/assets/js/bootstrap.min.js`; + + const scriptLoaded = useScript(scriptUrl); + + React.useEffect(() => { + if (!scriptLoaded || typeof window === 'undefined') return; + + let cancelled = false; + + const openChat = () => { + const svc = window.embeddedservice_bootstrap + svc.utilAPI.launchChat() + .catch((err: Error) => { + console.error(err) + }) + } + + // Get the business hours from salesforce + const fetchHours = async () => { + try { + const res = await fetch( + `${scrt2URL}/embeddedservice/v1/businesshours?orgId=${orgId}&esConfigName=${app}` + ); + const { businessHoursInfo }: { + businessHoursInfo: { + isActive: boolean + businessHours: BusinessHours[] + } + } = await res.json(); + + if (cancelled) return; + + const today = (new Date()).toISOString().slice(0, 10); + const todaysHours = businessHoursInfo.businessHours.find( + (h: BusinessHours) => today === (new Date(h.startTime)).toISOString().slice(0, 10) + ); + const newChatContext = !businessHoursInfo.isActive || todaysHours === undefined + ? null + : { ...todaysHours, openChat } + setChatContext(newChatContext); + } catch (e) { + if (!cancelled) { + console.error('Error fetching business hours', e); + setChatContext(null); + } + } + }; + + const handleBusinessHours = () => { + void fetchHours(); + }; + + // Don't try to set business hours until we know the chat is ready + window.addEventListener('onEmbeddedMessagingReady', handleBusinessHours); + window.addEventListener('onEmbeddedMessagingBusinessHoursStarted', handleBusinessHours); + window.addEventListener('onEmbeddedMessagingBusinessHoursEnded', handleBusinessHours); + + try { + // `embeddedservice_bootstrap` is injected by the script we just added + const svc = window.embeddedservice_bootstrap; + svc.settings.language = 'en_US'; + svc.settings.hideChatButtonOnLoad = true; + svc.init(orgId, app, `https://openstax.my.site.com/${deployment}`, { scrt2URL }); + } catch (e) { + console.error('Error initializing Embedded Messaging', e); + } + + return () => { + cancelled = true; + window.removeEventListener('onEmbeddedMessagingReady', handleBusinessHours); + window.removeEventListener('onEmbeddedMessagingBusinessHoursStarted', handleBusinessHours); + window.removeEventListener('onEmbeddedMessagingBusinessHoursEnded', handleBusinessHours); + }; + }, [scriptLoaded, orgId, app, scrt2URL]); + + return { chatContext }; +} + +const formatBusinessHoursRange = (startTime: number, endTime: number) => { + // Ensure we are working with a real Date instance + const startDate = new Date(startTime); + const endDate = new Date(endTime); + + // Bail if the timestamps are not valid numbers + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) return ''; + + try { + const baseOptions: Parameters[1] = { + hour: 'numeric', hour12: true + }; + const start = new Intl.DateTimeFormat(undefined, baseOptions).format(startDate); + const end = new Intl.DateTimeFormat(undefined, { + ...baseOptions, timeZoneName: 'short' + }).format(endDate); + // Ex: 9 AM - 5 PM CDT + return `${start} - ${end}`; + } catch (e) { + console.warn( + 'Intl.DateTimeFormat not available, falling back to simple hours.', e + ); + return `${startDate.getHours()} - ${endDate.getHours()}`; + } +} + export const HelpMenu: React.FC = ({ contactFormParams, children }) => { const [showIframe, setShowIframe] = React.useState(); + const { chatContext } = useWebMessagingDeployment(); const contactFormUrl = React.useMemo(() => { const formUrl = 'https://openstax.org/embedded/contact'; @@ -118,6 +271,12 @@ export const HelpMenu: React.FC = ({ contactFormParams, children return `${formUrl}?${params}`; }, [contactFormParams]); + const hoursRange = React.useMemo( + () => chatContext == null ? '' : formatBusinessHoursRange( + chatContext.startTime, chatContext.endTime + ), [chatContext] + ); + React.useEffect(() => { const closeIt = ({data}: MessageEvent) => { if (data === 'CONTACT_FORM_SUBMITTED') { @@ -132,9 +291,17 @@ export const HelpMenu: React.FC = ({ contactFormParams, children return ( <> - setShowIframe(contactFormUrl)}> - Report an issue - + {chatContext !== null + ? ( + chatContext.openChat()}> + Chat With Us ({hoursRange}) + + ) : ( + setShowIframe(contactFormUrl)}> + Report an issue + + ) + } {children} From 98523d810b6fc6b1cfe88ef2954d7d4cb074324c Mon Sep 17 00:00:00 2001 From: Tyler Nullmeier Date: Thu, 18 Sep 2025 17:38:15 -0500 Subject: [PATCH 02/23] Move stuff; polish things --- src/components/HelpMenu.spec.tsx | 30 ---- src/components/HelpMenu/HelpMenu.spec.tsx | 57 ++++++ .../{ => HelpMenu}/HelpMenu.stories.tsx | 6 +- src/components/HelpMenu/constants.ts | 22 +++ src/components/HelpMenu/hooks.ts | 149 ++++++++++++++++ .../{HelpMenu.tsx => HelpMenu/index.tsx} | 162 +++--------------- 6 files changed, 255 insertions(+), 171 deletions(-) delete mode 100644 src/components/HelpMenu.spec.tsx create mode 100644 src/components/HelpMenu/HelpMenu.spec.tsx rename src/components/{ => HelpMenu}/HelpMenu.stories.tsx (80%) create mode 100644 src/components/HelpMenu/constants.ts create mode 100644 src/components/HelpMenu/hooks.ts rename src/components/{HelpMenu.tsx => HelpMenu/index.tsx} (53%) diff --git a/src/components/HelpMenu.spec.tsx b/src/components/HelpMenu.spec.tsx deleted file mode 100644 index db2d6cc2b..000000000 --- a/src/components/HelpMenu.spec.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { render } from '@testing-library/react'; -import { BodyPortalSlotsContext } from './BodyPortalSlotsContext'; -import { HelpMenu, HelpMenuItem } from './HelpMenu'; -import { NavBar } from './NavBar'; - -describe('HelpMenu', () => { - let root: HTMLElement; - - beforeEach(() => { - root = document.createElement('main'); - root.id = 'root'; - document.body.append(root); - }); - - it('matches snapshot', () => { - render( - - - - window.alert('Ran HelpMenu callback function')}> - Test Callback - - - - - ); - - expect(document.body).toMatchSnapshot(); - }); -}); diff --git a/src/components/HelpMenu/HelpMenu.spec.tsx b/src/components/HelpMenu/HelpMenu.spec.tsx new file mode 100644 index 000000000..b4e944fe8 --- /dev/null +++ b/src/components/HelpMenu/HelpMenu.spec.tsx @@ -0,0 +1,57 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { BodyPortalSlotsContext } from '../BodyPortalSlotsContext'; +import { HelpMenu, HelpMenuItem } from '.'; +import { NavBar } from '../NavBar'; + +describe('HelpMenu', () => { + let root: HTMLElement; + + beforeEach(() => { + root = document.createElement('main'); + root.id = 'root'; + document.body.append(root); + }); + + it('matches snapshot', () => { + render( + + + + window.alert('Ran HelpMenu callback function')}> + Test Callback + + + + + ); + + expect(document.body).toMatchSnapshot(); + }); + + it('shows loading icon while SDK is loading', () => { + render(); + expect(screen.getByRole('button')).toBeDisabled(); + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); + }); + + it('replaces button when SDK is ready', async () => { + // mock the global embeddedservice_bootstrap object + const mockSvc = { + init: jest.fn(), + utilAPI: { launchChat: jest.fn() }, + }; + (window as any).embeddedservice_bootstrap = mockSvc; + const addEventListenerCalls: Parameters[] = []; + window.addEventListener = jest.fn().mockImplementation((...args: Parameters) => { + addEventListenerCalls.push(args); + }); + + render(); + const btn = await screen.findByRole('button', { name: /chat with us/i }); + expect(btn).not.toBeDisabled(); + + fireEvent.click(btn); + expect(mockSvc.utilAPI.launchChat).toHaveBeenCalled(); + }); +}); + diff --git a/src/components/HelpMenu.stories.tsx b/src/components/HelpMenu/HelpMenu.stories.tsx similarity index 80% rename from src/components/HelpMenu.stories.tsx rename to src/components/HelpMenu/HelpMenu.stories.tsx index 4c7dd2cc3..df0c26919 100644 --- a/src/components/HelpMenu.stories.tsx +++ b/src/components/HelpMenu/HelpMenu.stories.tsx @@ -1,7 +1,7 @@ import { createGlobalStyle } from 'styled-components'; -import { BodyPortalSlotsContext } from './BodyPortalSlotsContext'; -import { HelpMenu, HelpMenuItem } from './HelpMenu'; -import { NavBar } from './NavBar'; +import { BodyPortalSlotsContext } from '../BodyPortalSlotsContext'; +import { HelpMenu, HelpMenuItem } from '.'; +import { NavBar } from '../NavBar'; const BodyPortalGlobalStyle = createGlobalStyle` [data-portal-slot="nav"] { diff --git a/src/components/HelpMenu/constants.ts b/src/components/HelpMenu/constants.ts new file mode 100644 index 000000000..2d54583ef --- /dev/null +++ b/src/components/HelpMenu/constants.ts @@ -0,0 +1,22 @@ +export const orgId = '00DU0000000Kwch'; +export const app = 'Web_Messaging_Deployment'; +export const deployment = 'ESWWebMessagingDeployme1716235390398'; +export const deploymentURL = `https://openstax.my.site.com/${deployment}`; +export const scrt2URL = 'https://openstax.my.salesforce-scrt.com'; +export const scriptUrl = `${deploymentURL}/assets/js/bootstrap.min.js`; +export const businessHoursURL = + `${scrt2URL}/embeddedservice/v1/businesshours?orgId=${orgId}&esConfigName=${app}`; +export const chatEmbedDefaults = { + orgId, + app, + deploymentURL, + scrt2URL, + scriptUrl, + businessHoursURL, +} as const; + +export const embeddedChatEvents = { + READY: 'onEmbeddedMessagingReady', + BUSINESS_HOURS_STARTED: 'onEmbeddedMessagingBusinessHoursStarted', + BUSINESS_HOURS_ENDED: 'onEmbeddedMessagingBusinessHoursEnded', +} as const; \ No newline at end of file diff --git a/src/components/HelpMenu/hooks.ts b/src/components/HelpMenu/hooks.ts new file mode 100644 index 000000000..af093b860 --- /dev/null +++ b/src/components/HelpMenu/hooks.ts @@ -0,0 +1,149 @@ +import React from "react"; +import { embeddedChatEvents } from "./constants"; + +export interface WindowWithEmbed extends Window { + embeddedservice_bootstrap?: any; +} + +export interface ChatEmbedServiceConfiguration { + orgId: string, + app: string, + deploymentURL: string, + scrt2URL: string, + scriptUrl: string, + businessHoursURL: string, +} + +export interface BusinessHours { + startTime: number; + endTime: number; +} + +export interface BusinessHoursResponse { + businessHoursInfo: { + isActive: boolean; + businessHours: BusinessHours[]; + }; +} + +export interface ChatEmbed extends BusinessHours { + openChat: () => void; +} + +export const useScript = (src: string) => { + const [ready, setReady] = React.useState(false); + const [error, setError] = React.useState(null); + + const scriptRef = React.useRef(null); + + React.useEffect(() => { + // Already in the DOM? No need to add it again. + if (document.querySelector(`script[src="${src}"]`)) { + setReady(true); + return; + } + + const script = document.createElement('script'); + script.src = src; + script.async = true; + script.onload = () => setReady(true); + script.onerror = () => setError(new Error(`Failed to load ${src}`)); + document.head.appendChild(script); + scriptRef.current = script; + }, [src]); + + return { ready, error }; +}; + +export const getBusinessHours = async ( + businessHoursURL: string, + signal?: AbortSignal +) => { + const res = await fetch(businessHoursURL, { signal }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return (await res.json()) as BusinessHoursResponse; +}; + +export const getChatEmbed = async (businessHoursURL: string, signal?: AbortSignal) => { + const { businessHoursInfo } = await getBusinessHours(businessHoursURL, signal); + + const today = new Date().toISOString().slice(0, 10); + const todaysHours = businessHoursInfo.businessHours.find( + (h: BusinessHours) => today === new Date(h.startTime).toISOString().slice(0, 10) + ); + + const openChat = async () => { + const svc = (window as WindowWithEmbed).embeddedservice_bootstrap; + if (!svc) return undefined; + return await svc.utilAPI.launchChat(); + } + + return !businessHoursInfo.isActive || todaysHours === undefined + ? null + : { ...todaysHours, openChat }; +}; + +export const useEmbeddedChatService = ({ + orgId, + app, + deploymentURL, + scrt2URL, + scriptUrl, + businessHoursURL, +}: ChatEmbedServiceConfiguration) => { + const [chatEmbed, setChatEmbed] = React.useState(null); + const [loading, setLoading] = React.useState(false); + const [fetchError, setFetchError] = React.useState(null); + + const { ready: scriptLoaded, error: scriptError } = useScript(scriptUrl); + const controllerRef = React.useRef(null); + + const updateChatEmbed = React.useCallback(({ signal }: AbortController) => { + return () => { + setLoading(true); + setFetchError(null); + + return getChatEmbed(businessHoursURL, signal) + .then(setChatEmbed) + .catch((err) => { + if ((err as any).name !== "AbortError") { + setFetchError(err); + setChatEmbed(null); + } + }) + .finally(() => setLoading(false)); + } + }, [businessHoursURL]); + + React.useEffect(() => { + if (!scriptLoaded || typeof window === 'undefined') return; + + controllerRef.current = new AbortController(); + const update = updateChatEmbed(controllerRef.current); + + // Don't try to set business hours until we know the chat is ready + window.addEventListener(embeddedChatEvents.READY, update); + window.addEventListener(embeddedChatEvents.BUSINESS_HOURS_STARTED, update); + window.addEventListener(embeddedChatEvents.BUSINESS_HOURS_ENDED, update); + + try { + // `embeddedservice_bootstrap` is injected by the script we just added + const svc = (window as WindowWithEmbed).embeddedservice_bootstrap; + svc.settings.language = 'en_US'; + svc.settings.hideChatButtonOnLoad = true; + svc.init(orgId, app, deploymentURL, { scrt2URL }); + } catch (e) { + console.error('Error initializing Embedded Messaging', e); + } + + return () => { + controllerRef.current?.abort(); + controllerRef.current = null; + window.removeEventListener(embeddedChatEvents.READY, update); + window.removeEventListener(embeddedChatEvents.BUSINESS_HOURS_STARTED, update); + window.removeEventListener(embeddedChatEvents.BUSINESS_HOURS_ENDED, update); + }; + }, [scriptLoaded, updateChatEmbed, orgId, app, deploymentURL, scrt2URL]); + + return { chatEmbed, loading, error: scriptError ?? fetchError }; +} diff --git a/src/components/HelpMenu.tsx b/src/components/HelpMenu/index.tsx similarity index 53% rename from src/components/HelpMenu.tsx rename to src/components/HelpMenu/index.tsx index afd7245c5..b2f7446d2 100644 --- a/src/components/HelpMenu.tsx +++ b/src/components/HelpMenu/index.tsx @@ -1,8 +1,10 @@ import React from 'react'; -import { NavBarMenuButton, NavBarMenuItem } from './NavBarMenuButtons'; -import { colors } from '../theme'; +import { NavBarMenuButton, NavBarMenuItem } from '../NavBarMenuButtons'; +import { colors } from '../../theme'; import styled from 'styled-components'; -import { BodyPortal } from './BodyPortal'; +import { BodyPortal } from '../BodyPortal'; +import { ChatEmbedServiceConfiguration, useEmbeddedChatService } from './hooks'; +import { chatEmbedDefaults } from './constants' export const HelpMenuButton = styled(NavBarMenuButton)` color: ${colors.palette.gray}; @@ -102,136 +104,11 @@ export const NewTabIcon = () => ( export interface HelpMenuProps { contactFormParams: { key: string; value: string }[]; + chatEmbedParams?: Partial; children?: React.ReactNode; } -declare global { - interface Window { - embeddedservice_bootstrap?: any; - } -} -export interface BusinessHours { - startTime: number; - endTime: number; -} - -export interface ChatContext extends BusinessHours { - openChat: () => void; -} - -function useScript(src: string): boolean { - const [ready, setReady] = React.useState(false); - const scriptRef = React.useRef(null); - - React.useEffect(() => { - // Already in the DOM? No need to add it again. - if (document.querySelector(`script[src="${src}"]`)) { - setReady(true); - return; - } - - const script = document.createElement('script'); - script.src = src; - script.async = true; - script.onload = () => setReady(true); - script.onerror = () => console.warn(`Failed to load ${src}`); - document.head.appendChild(script); - scriptRef.current = script; - - return () => { - scriptRef.current?.remove(); - }; - }, [src]); - - return ready; -} - -export function useWebMessagingDeployment() { - const [chatContext, setChatContext] = React.useState(null); - const orgId = '00DU0000000Kwch'; - const app = 'Web_Messaging_Deployment'; - const deployment = 'ESWWebMessagingDeployme1716235390398'; - const scrt2URL = 'https://openstax.my.salesforce-scrt.com'; - const scriptUrl = - `https://openstax.my.site.com/${deployment}/assets/js/bootstrap.min.js`; - - const scriptLoaded = useScript(scriptUrl); - - React.useEffect(() => { - if (!scriptLoaded || typeof window === 'undefined') return; - - let cancelled = false; - - const openChat = () => { - const svc = window.embeddedservice_bootstrap - svc.utilAPI.launchChat() - .catch((err: Error) => { - console.error(err) - }) - } - - // Get the business hours from salesforce - const fetchHours = async () => { - try { - const res = await fetch( - `${scrt2URL}/embeddedservice/v1/businesshours?orgId=${orgId}&esConfigName=${app}` - ); - const { businessHoursInfo }: { - businessHoursInfo: { - isActive: boolean - businessHours: BusinessHours[] - } - } = await res.json(); - - if (cancelled) return; - - const today = (new Date()).toISOString().slice(0, 10); - const todaysHours = businessHoursInfo.businessHours.find( - (h: BusinessHours) => today === (new Date(h.startTime)).toISOString().slice(0, 10) - ); - const newChatContext = !businessHoursInfo.isActive || todaysHours === undefined - ? null - : { ...todaysHours, openChat } - setChatContext(newChatContext); - } catch (e) { - if (!cancelled) { - console.error('Error fetching business hours', e); - setChatContext(null); - } - } - }; - - const handleBusinessHours = () => { - void fetchHours(); - }; - - // Don't try to set business hours until we know the chat is ready - window.addEventListener('onEmbeddedMessagingReady', handleBusinessHours); - window.addEventListener('onEmbeddedMessagingBusinessHoursStarted', handleBusinessHours); - window.addEventListener('onEmbeddedMessagingBusinessHoursEnded', handleBusinessHours); - - try { - // `embeddedservice_bootstrap` is injected by the script we just added - const svc = window.embeddedservice_bootstrap; - svc.settings.language = 'en_US'; - svc.settings.hideChatButtonOnLoad = true; - svc.init(orgId, app, `https://openstax.my.site.com/${deployment}`, { scrt2URL }); - } catch (e) { - console.error('Error initializing Embedded Messaging', e); - } - - return () => { - cancelled = true; - window.removeEventListener('onEmbeddedMessagingReady', handleBusinessHours); - window.removeEventListener('onEmbeddedMessagingBusinessHoursStarted', handleBusinessHours); - window.removeEventListener('onEmbeddedMessagingBusinessHoursEnded', handleBusinessHours); - }; - }, [scriptLoaded, orgId, app, scrt2URL]); - - return { chatContext }; -} - -const formatBusinessHoursRange = (startTime: number, endTime: number) => { +export const formatBusinessHoursRange = (startTime: number, endTime: number) => { // Ensure we are working with a real Date instance const startDate = new Date(startTime); const endDate = new Date(endTime); @@ -253,13 +130,17 @@ const formatBusinessHoursRange = (startTime: number, endTime: number) => { console.warn( 'Intl.DateTimeFormat not available, falling back to simple hours.', e ); + // Ex: 9 - 17 return `${startDate.getHours()} - ${endDate.getHours()}`; } -} +}; -export const HelpMenu: React.FC = ({ contactFormParams, children }) => { +export const HelpMenu: React.FC = ({ contactFormParams, chatEmbedParams, children }) => { const [showIframe, setShowIframe] = React.useState(); - const { chatContext } = useWebMessagingDeployment(); + const chatConfig = React.useMemo(() => ({ + ...chatEmbedDefaults, ...chatEmbedParams + }), [chatEmbedParams]); + const { chatEmbed, error: chatEmbedError } = useEmbeddedChatService(chatConfig); const contactFormUrl = React.useMemo(() => { const formUrl = 'https://openstax.org/embedded/contact'; @@ -272,9 +153,12 @@ export const HelpMenu: React.FC = ({ contactFormParams, children }, [contactFormParams]); const hoursRange = React.useMemo( - () => chatContext == null ? '' : formatBusinessHoursRange( - chatContext.startTime, chatContext.endTime - ), [chatContext] + () => ( + chatEmbed?.startTime && chatEmbed?.endTime + ? formatBusinessHoursRange(chatEmbed.startTime, chatEmbed.endTime) + : null + ), + [chatEmbed?.startTime, chatEmbed?.endTime] ); React.useEffect(() => { @@ -288,12 +172,14 @@ export const HelpMenu: React.FC = ({ contactFormParams, children return () => window.removeEventListener('message', closeIt, false); }, []); + if (chatEmbedError) console.error(chatEmbedError); + return ( <> - {chatContext !== null + {chatEmbed !== null && chatEmbedError === null ? ( - chatContext.openChat()}> + chatEmbed.openChat()}> Chat With Us ({hoursRange}) ) : ( From e1ac610afbba6841d22a4bfa34d87e553d140440 Mon Sep 17 00:00:00 2001 From: Tyler Nullmeier Date: Mon, 22 Sep 2025 16:49:54 -0500 Subject: [PATCH 03/23] Update tests --- package.json | 3 +- src/components/HelpMenu/HelpMenu.spec.tsx | 57 --- .../__snapshots__/index.spec.tsx.snap} | 114 +++++- src/components/HelpMenu/hooks.spec.tsx | 330 ++++++++++++++++++ src/components/HelpMenu/hooks.ts | 15 +- src/components/HelpMenu/index.spec.tsx | 209 +++++++++++ yarn.lock | 15 + 7 files changed, 679 insertions(+), 64 deletions(-) delete mode 100644 src/components/HelpMenu/HelpMenu.spec.tsx rename src/components/{__snapshots__/HelpMenu.spec.tsx.snap => HelpMenu/__snapshots__/index.spec.tsx.snap} (60%) create mode 100644 src/components/HelpMenu/hooks.spec.tsx create mode 100644 src/components/HelpMenu/index.spec.tsx diff --git a/package.json b/package.json index 5effbe04e..6728a400a 100644 --- a/package.json +++ b/package.json @@ -33,13 +33,13 @@ "styled-components": "*" }, "devDependencies": { - "npm-run-all": "^4.1.5", "@ladle/react": "^2.1.2", "@openstax/ts-utils": "^1.27.6", "@playwright/test": "^1.25.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^12.0.0", + "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.2", "@types/jest": "^28.1.4", "@types/node": "^18.7.5", @@ -59,6 +59,7 @@ "jest-environment-node": "^29.6.2", "microbundle": "^0.15.1", "node-fetch": "<3.0.0", + "npm-run-all": "^4.1.5", "react": "^17.0.2", "react-dom": "^17.0.2", "react-is": "^16.8.0", diff --git a/src/components/HelpMenu/HelpMenu.spec.tsx b/src/components/HelpMenu/HelpMenu.spec.tsx deleted file mode 100644 index b4e944fe8..000000000 --- a/src/components/HelpMenu/HelpMenu.spec.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { BodyPortalSlotsContext } from '../BodyPortalSlotsContext'; -import { HelpMenu, HelpMenuItem } from '.'; -import { NavBar } from '../NavBar'; - -describe('HelpMenu', () => { - let root: HTMLElement; - - beforeEach(() => { - root = document.createElement('main'); - root.id = 'root'; - document.body.append(root); - }); - - it('matches snapshot', () => { - render( - - - - window.alert('Ran HelpMenu callback function')}> - Test Callback - - - - - ); - - expect(document.body).toMatchSnapshot(); - }); - - it('shows loading icon while SDK is loading', () => { - render(); - expect(screen.getByRole('button')).toBeDisabled(); - expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); - }); - - it('replaces button when SDK is ready', async () => { - // mock the global embeddedservice_bootstrap object - const mockSvc = { - init: jest.fn(), - utilAPI: { launchChat: jest.fn() }, - }; - (window as any).embeddedservice_bootstrap = mockSvc; - const addEventListenerCalls: Parameters[] = []; - window.addEventListener = jest.fn().mockImplementation((...args: Parameters) => { - addEventListenerCalls.push(args); - }); - - render(); - const btn = await screen.findByRole('button', { name: /chat with us/i }); - expect(btn).not.toBeDisabled(); - - fireEvent.click(btn); - expect(mockSvc.utilAPI.launchChat).toHaveBeenCalled(); - }); -}); - diff --git a/src/components/__snapshots__/HelpMenu.spec.tsx.snap b/src/components/HelpMenu/__snapshots__/index.spec.tsx.snap similarity index 60% rename from src/components/__snapshots__/HelpMenu.spec.tsx.snap rename to src/components/HelpMenu/__snapshots__/index.spec.tsx.snap index 1cb9b0f48..2548563cf 100644 --- a/src/components/__snapshots__/HelpMenu.spec.tsx.snap +++ b/src/components/HelpMenu/__snapshots__/index.spec.tsx.snap @@ -3,6 +3,7 @@ exports[`HelpMenu matches snapshot 1`] = `
-
+