|
|
@@ -22,11 +22,24 @@ vi.mock('@/features/whats-new/Actions/sendWhatsNewFeedback', () => ({
|
|
|
sendWhatsNewFeedback: (input: unknown) => sendWhatsNewFeedback(input),
|
|
|
}))
|
|
|
|
|
|
+const loadMoreWhatsNew = vi.fn(async (_offset: unknown) => ({
|
|
|
+ success: true,
|
|
|
+ data: { entries: [] as WhatsNewEntry[], hasMore: false },
|
|
|
+}))
|
|
|
+vi.mock('@/features/whats-new/Actions/loadMoreWhatsNew', () => ({
|
|
|
+ loadMoreWhatsNew: (offset: unknown) => loadMoreWhatsNew(offset),
|
|
|
+}))
|
|
|
+
|
|
|
const toastError = vi.fn()
|
|
|
-vi.mock('sonner', () => ({ toast: { error: (message: string) => toastError(message) } }))
|
|
|
+const toastPlain = vi.fn()
|
|
|
+vi.mock('sonner', () => ({
|
|
|
+ toast: Object.assign((message: string) => toastPlain(message), {
|
|
|
+ error: (message: string) => toastError(message),
|
|
|
+ }),
|
|
|
+}))
|
|
|
|
|
|
const { WhatsNewProvider } = await import('@/features/whats-new/Components/WhatsNewProvider')
|
|
|
-const { WhatsNewButton } = await import('@/features/whats-new/Components/WhatsNewButton')
|
|
|
+const { WhatsNewAboutRows } = await import('@/features/whats-new/Components/WhatsNewAboutRows')
|
|
|
|
|
|
const ENTRIES: WhatsNewEntry[] = [
|
|
|
{
|
|
|
@@ -38,6 +51,7 @@ const ENTRIES: WhatsNewEntry[] = [
|
|
|
ctaLabel: 'Open the calendar',
|
|
|
ctaHref: '/calendar',
|
|
|
docsHref: '/docs/features/calendar',
|
|
|
+ videoUrl: 'https://youtu.be/abc123',
|
|
|
minVersion: null,
|
|
|
audience: 'all',
|
|
|
subject: null,
|
|
|
@@ -52,6 +66,7 @@ const ENTRIES: WhatsNewEntry[] = [
|
|
|
ctaLabel: null,
|
|
|
ctaHref: null,
|
|
|
docsHref: null,
|
|
|
+ videoUrl: null,
|
|
|
minVersion: null,
|
|
|
audience: 'all',
|
|
|
subject: null,
|
|
|
@@ -63,17 +78,21 @@ function show({
|
|
|
entries = ENTRIES,
|
|
|
unread = ['appointments'],
|
|
|
feedback = 'anonymous' as 'identified' | 'anonymous' | null,
|
|
|
+ tabHidden = false,
|
|
|
+ hasMore = false,
|
|
|
} = {}) {
|
|
|
return render(
|
|
|
<WhatsNewProvider
|
|
|
entries={entries}
|
|
|
+ hasMore={hasMore}
|
|
|
unreadIds={unread}
|
|
|
+ initialTabHidden={tabHidden}
|
|
|
timeZone="Europe/Oslo"
|
|
|
appVersion="v2.14.0"
|
|
|
releaseNotesUrl="https://example.test/releases"
|
|
|
feedback={feedback}
|
|
|
>
|
|
|
- <WhatsNewButton />
|
|
|
+ <WhatsNewAboutRows />
|
|
|
</WhatsNewProvider>
|
|
|
)
|
|
|
}
|
|
|
@@ -84,9 +103,10 @@ beforeEach(() => {
|
|
|
})
|
|
|
|
|
|
describe("What's new", () => {
|
|
|
- it('offers no button when there is nothing to read', () => {
|
|
|
+ it('offers nothing to press when there is nothing to read', () => {
|
|
|
show({ entries: [], unread: [] })
|
|
|
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
|
|
+ expect(screen.queryByRole('switch')).not.toBeInTheDocument()
|
|
|
})
|
|
|
|
|
|
it('says there is something unseen, then stops saying so once the panel was opened', async () => {
|
|
|
@@ -151,7 +171,7 @@ describe("What's new", () => {
|
|
|
show({ feedback: 'identified' })
|
|
|
await user.click(screen.getByRole('button', { name: /What's new/ }))
|
|
|
const first = screen.getByText('Appointments on the calendar').closest('article') as HTMLElement
|
|
|
- await user.click(within(first).getByRole('button', { name: 'Yes, this is useful' }))
|
|
|
+ await user.click(within(first).getByRole('button', { name: 'No, not what I expected' }))
|
|
|
expect(
|
|
|
await within(first).findByText(
|
|
|
'Sent to the Torqvoice team with your email address, so we can reply.'
|
|
|
@@ -159,6 +179,19 @@ describe("What's new", () => {
|
|
|
).toBeInTheDocument()
|
|
|
})
|
|
|
|
|
|
+ it('sends a thumb up and asks for nothing more', async () => {
|
|
|
+ const user = userEvent.setup()
|
|
|
+ show()
|
|
|
+ await user.click(screen.getByRole('button', { name: /What's new/ }))
|
|
|
+ const first = screen.getByText('Appointments on the calendar').closest('article') as HTMLElement
|
|
|
+ const up = within(first).getByRole('button', { name: 'Yes, this is useful' })
|
|
|
+ await user.click(up)
|
|
|
+
|
|
|
+ await waitFor(() => expect(up).toHaveAttribute('aria-pressed', 'true'))
|
|
|
+ expect(sendWhatsNewFeedback).toHaveBeenCalledWith({ entryId: 'appointments', vote: 'up' })
|
|
|
+ expect(within(first).queryByRole('textbox')).not.toBeInTheDocument()
|
|
|
+ })
|
|
|
+
|
|
|
it('takes the thumb back and says so when the vote could not be sent', async () => {
|
|
|
sendWhatsNewFeedback.mockResolvedValueOnce({ success: false })
|
|
|
const user = userEvent.setup()
|
|
|
@@ -224,3 +257,109 @@ describe("What's new, when things go wrong", () => {
|
|
|
expect(screen.queryByRole('link', { name: /Open the calendar/ })).not.toBeInTheDocument()
|
|
|
})
|
|
|
})
|
|
|
+
|
|
|
+describe("the What's new tab on the edge of the screen", () => {
|
|
|
+ it('can be sent away, says where to get it back, and stays away', async () => {
|
|
|
+ const user = userEvent.setup()
|
|
|
+ show()
|
|
|
+ await user.click(screen.getByRole('button', { name: 'Hide this tab' }))
|
|
|
+
|
|
|
+ expect(screen.queryByRole('button', { name: /^What's new/ })).not.toBeInTheDocument()
|
|
|
+ expect(toastPlain).toHaveBeenCalledWith(
|
|
|
+ 'Tab hidden. You can turn it back on in Settings, About.'
|
|
|
+ )
|
|
|
+ expect(document.cookie).toContain('whatsNewTab=hidden')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('is not drawn for somebody who hid it, but the news can still be opened from About', async () => {
|
|
|
+ const user = userEvent.setup()
|
|
|
+ show({ tabHidden: true })
|
|
|
+ expect(screen.queryByRole('button', { name: /^What's new/ })).not.toBeInTheDocument()
|
|
|
+
|
|
|
+ await user.click(screen.getByRole('button', { name: 'See what is new' }))
|
|
|
+ expect(await screen.findByRole('dialog', { name: "What's new" })).toBeInTheDocument()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('comes back when it is switched on again in About', async () => {
|
|
|
+ const user = userEvent.setup()
|
|
|
+ show({ tabHidden: true })
|
|
|
+ const toggle = screen.getByRole('switch', {
|
|
|
+ name: /Show the What's new tab at the side of the screen/,
|
|
|
+ })
|
|
|
+ expect(toggle).not.toBeChecked()
|
|
|
+ await user.click(toggle)
|
|
|
+
|
|
|
+ expect(screen.getByRole('button', { name: /^What's new/ })).toBeInTheDocument()
|
|
|
+ expect(document.cookie).toContain('whatsNewTab=shown')
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+describe("older highlights in the What's new panel", () => {
|
|
|
+ it('offers nothing more when the first page is all there is', async () => {
|
|
|
+ const user = userEvent.setup()
|
|
|
+ show()
|
|
|
+ await user.click(screen.getByRole('button', { name: /^What's new/ }))
|
|
|
+ expect(screen.queryByRole('button', { name: 'Show older updates' })).not.toBeInTheDocument()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('adds the next page under the ones already shown, and stops offering at the end', async () => {
|
|
|
+ loadMoreWhatsNew.mockResolvedValueOnce({
|
|
|
+ success: true,
|
|
|
+ data: {
|
|
|
+ entries: [{ ...ENTRIES[1], id: 'closures', title: 'Closed days and staff leave' }],
|
|
|
+ hasMore: false,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const user = userEvent.setup()
|
|
|
+ show({ hasMore: true })
|
|
|
+ await user.click(screen.getByRole('button', { name: /^What's new/ }))
|
|
|
+ await user.click(screen.getByRole('button', { name: 'Show older updates' }))
|
|
|
+
|
|
|
+ expect(loadMoreWhatsNew).toHaveBeenCalledWith(ENTRIES.length)
|
|
|
+ expect(await screen.findByText('Closed days and staff leave')).toBeInTheDocument()
|
|
|
+ expect(screen.getByText('Appointments on the calendar')).toBeInTheDocument()
|
|
|
+ expect(screen.queryByRole('button', { name: 'Show older updates' })).not.toBeInTheDocument()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('keeps the button after a page fails, and a second press brings the page', async () => {
|
|
|
+ loadMoreWhatsNew.mockRejectedValueOnce(new Error('offline'))
|
|
|
+ loadMoreWhatsNew.mockResolvedValueOnce({
|
|
|
+ success: true,
|
|
|
+ data: {
|
|
|
+ entries: [{ ...ENTRIES[1], id: 'closures', title: 'Closed days and staff leave' }],
|
|
|
+ hasMore: false,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const user = userEvent.setup()
|
|
|
+ show({ hasMore: true })
|
|
|
+ await user.click(screen.getByRole('button', { name: /^What's new/ }))
|
|
|
+ await user.click(screen.getByRole('button', { name: 'Show older updates' }))
|
|
|
+
|
|
|
+ const again = await screen.findByRole('button', { name: 'Show older updates' })
|
|
|
+ await waitFor(() => expect(again).toBeEnabled())
|
|
|
+ await user.click(again)
|
|
|
+ expect(await screen.findByText('Closed days and staff leave')).toBeInTheDocument()
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+describe("a video on a What's new entry", () => {
|
|
|
+ it('links to YouTube from the entry that has one, and only that one', async () => {
|
|
|
+ const user = userEvent.setup()
|
|
|
+ show()
|
|
|
+ await user.click(screen.getByRole('button', { name: /^What's new/ }))
|
|
|
+ const links = screen.getAllByRole('link', { name: 'Watch the video' })
|
|
|
+ expect(links).toHaveLength(1)
|
|
|
+ expect(links[0]).toHaveAttribute('href', 'https://youtu.be/abc123')
|
|
|
+ expect(links[0]).toHaveAttribute('target', '_blank')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('shows no video link for an address that is not YouTube', async () => {
|
|
|
+ const user = userEvent.setup()
|
|
|
+ show({
|
|
|
+ entries: [{ ...ENTRIES[0], videoUrl: 'https://youtube.com.evil.example/watch' }],
|
|
|
+ unread: [],
|
|
|
+ })
|
|
|
+ await user.click(screen.getByRole('button', { name: /^What's new/ }))
|
|
|
+ expect(screen.queryByRole('link', { name: 'Watch the video' })).not.toBeInTheDocument()
|
|
|
+ })
|
|
|
+})
|