2026-08-03 10:04:41 +08:00
|
|
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
|
import { FeedbackDialog } from './FeedbackDialog'
|
|
|
|
|
|
import {
|
|
|
|
|
|
CIRCLECI_HOME_URL,
|
|
|
|
|
|
REFACTORING_HOME_URL,
|
|
|
|
|
|
TOLARIA_GITHUB_CONTRIBUTING_URL,
|
|
|
|
|
|
TOLARIA_GITHUB_DISCUSSIONS_URL,
|
|
|
|
|
|
TOLARIA_GITHUB_ISSUES_URL,
|
|
|
|
|
|
TOLARIA_GITHUB_PULL_REQUESTS_URL,
|
|
|
|
|
|
TOLARIA_PRODUCT_BOARD_URL,
|
|
|
|
|
|
UNBLOCKED_HOME_URL,
|
|
|
|
|
|
} from '../constants/feedback'
|
|
|
|
|
|
import { APP_COMMAND_EVENT_NAME, APP_COMMAND_IDS } from '../hooks/appCommandDispatcher'
|
|
|
|
|
|
import { rememberFeedbackDialogOpener } from '../lib/feedbackDialogOpener'
|
|
|
|
|
|
|
|
|
|
|
|
vi.mock('../utils/url', () => ({
|
|
|
|
|
|
openExternalUrl: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
const { openExternalUrl } = await import('../utils/url') as typeof import('../utils/url') & {
|
|
|
|
|
|
openExternalUrl: ReturnType<typeof vi.fn>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
describe('FeedbackDialog', () => {
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
vi.clearAllMocks()
|
|
|
|
|
|
Object.defineProperty(window.navigator, 'clipboard', {
|
|
|
|
|
|
configurable: true,
|
|
|
|
|
|
value: {
|
|
|
|
|
|
writeText: vi.fn().mockResolvedValue(undefined),
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('renders the contribution paths when open', () => {
|
|
|
|
|
|
render(<FeedbackDialog open={true} onClose={vi.fn()} buildNumber="b281" releaseChannel="alpha" />)
|
|
|
|
|
|
expect(screen.getByTestId('feedback-dialog')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Contribute to HoloLake Era')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Pick the path that fits what you want to do! Any type of help is appreciated')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Newsletter')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Sponsors')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Feature requests')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Discussions')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Contribute code')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Report a bug')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText(/Refactoring is my newsletter and community/i)).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText(/HoloLake Era is supported by a panel of tools/i)).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByRole('button', { name: 'Open CircleCI' })).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByRole('button', { name: 'Open Unblocked' })).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Search on the board first, upvote existing ideas, and create new posts when genuinely new!')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Use Discussions for questions, conversations, show & tell, and community context.')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Small, focused PRs are welcome. Check the board first so you build the right things!')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('Explain how to reproduce, what you expected, vs what happened. Attach the diagnostics please!')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.queryByText(/Sanitized and optional/i)).not.toBeInTheDocument()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('localizes the contribution dialog', () => {
|
|
|
|
|
|
render(<FeedbackDialog open={true} onClose={vi.fn()} buildNumber="b281" locale="zh-CN" releaseChannel="alpha" />)
|
|
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('参与 HoloLake Era 贡献')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText('功能请求')).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByRole('button', { name: '打开产品看板' })).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByRole('button', { name: '复制已清理的诊断信息' })).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.queryByText('Contribute to HoloLake Era')).not.toBeInTheDocument()
|
|
|
|
|
|
expect(screen.queryByText('Feature requests')).not.toBeInTheDocument()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('focuses the primary CTA when opened', async () => {
|
|
|
|
|
|
render(<FeedbackDialog open={true} onClose={vi.fn()} buildNumber="b281" releaseChannel={null} />)
|
|
|
|
|
|
const cta = screen.getByRole('button', { name: 'Check out Refactoring' })
|
|
|
|
|
|
await waitFor(() => expect(cta).toHaveFocus())
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('opens the expected contribution links without closing the modal', async () => {
|
|
|
|
|
|
const onClose = vi.fn()
|
|
|
|
|
|
render(<FeedbackDialog open={true} onClose={onClose} buildNumber="b281" releaseChannel={null} />)
|
|
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Check out Refactoring' }))
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Open CircleCI' }))
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Open Unblocked' }))
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Open Product Board' }))
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Open Discussions' }))
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Open Pull Requests' }))
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Open Contributing Guide' }))
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Open GitHub Issues' }))
|
|
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(openExternalUrl).toHaveBeenNthCalledWith(1, REFACTORING_HOME_URL))
|
2026-08-04 23:11:25 +08:00
|
|
|
|
expect(openExternalUrl).toHaveBeenNthCalledWith(2, CIRCLECI_HOME_URL)
|
|
|
|
|
|
expect(openExternalUrl).toHaveBeenNthCalledWith(3, UNBLOCKED_HOME_URL)
|
|
|
|
|
|
expect(openExternalUrl).toHaveBeenNthCalledWith(4, TOLARIA_PRODUCT_BOARD_URL)
|
|
|
|
|
|
expect(openExternalUrl).toHaveBeenNthCalledWith(5, TOLARIA_GITHUB_DISCUSSIONS_URL)
|
|
|
|
|
|
expect(openExternalUrl).toHaveBeenNthCalledWith(6, TOLARIA_GITHUB_PULL_REQUESTS_URL)
|
|
|
|
|
|
expect(openExternalUrl).toHaveBeenNthCalledWith(7, TOLARIA_GITHUB_CONTRIBUTING_URL)
|
|
|
|
|
|
expect(openExternalUrl).toHaveBeenNthCalledWith(8, TOLARIA_GITHUB_ISSUES_URL)
|
2026-08-03 10:04:41 +08:00
|
|
|
|
expect(onClose).not.toHaveBeenCalled()
|
|
|
|
|
|
expect(screen.getByTestId('feedback-dialog')).toBeInTheDocument()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('copies a sanitized diagnostic bundle', async () => {
|
|
|
|
|
|
const writeText = vi.fn().mockResolvedValue(undefined)
|
|
|
|
|
|
Object.defineProperty(window.navigator, 'clipboard', {
|
|
|
|
|
|
configurable: true,
|
|
|
|
|
|
value: { writeText },
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
render(<FeedbackDialog open={true} onClose={vi.fn()} buildNumber="b281" releaseChannel="alpha" />)
|
|
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Copy sanitized diagnostics' }))
|
|
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(writeText).toHaveBeenCalledTimes(1))
|
|
|
|
|
|
expect(writeText.mock.calls[0]?.[0]).toContain('HoloLake Era sanitized diagnostics')
|
|
|
|
|
|
expect(writeText.mock.calls[0]?.[0]).toContain('Build: b281')
|
|
|
|
|
|
expect(writeText.mock.calls[0]?.[0]).toContain('Release channel: alpha')
|
|
|
|
|
|
expect(screen.getByText('Diagnostics copied.')).toBeInTheDocument()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('shows a fallback message when a contribution link cannot be opened', async () => {
|
|
|
|
|
|
openExternalUrl.mockRejectedValueOnce(new Error('blocked'))
|
|
|
|
|
|
|
|
|
|
|
|
render(<FeedbackDialog open={true} onClose={vi.fn()} buildNumber="b281" releaseChannel={null} />)
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Open Product Board' }))
|
|
|
|
|
|
|
|
|
|
|
|
expect(await screen.findByText(/couldn’t open Product Board automatically/i)).toBeInTheDocument()
|
|
|
|
|
|
expect(screen.getByText(TOLARIA_PRODUCT_BOARD_URL)).toBeInTheDocument()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('closes when pressing Escape', () => {
|
|
|
|
|
|
const onClose = vi.fn()
|
|
|
|
|
|
render(<FeedbackDialog open={true} onClose={onClose} buildNumber="b281" releaseChannel={null} />)
|
|
|
|
|
|
fireEvent.keyDown(document, { key: 'Escape' })
|
|
|
|
|
|
expect(onClose).toHaveBeenCalledOnce()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('closes when clicking the top-right Close control', () => {
|
|
|
|
|
|
const onClose = vi.fn()
|
|
|
|
|
|
render(<FeedbackDialog open={true} onClose={onClose} buildNumber="b281" releaseChannel={null} />)
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
|
|
|
|
|
expect(onClose).toHaveBeenCalledOnce()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
it('reopens the command palette after closing when launched from it', () => {
|
|
|
|
|
|
vi.useFakeTimers()
|
|
|
|
|
|
|
|
|
|
|
|
const opener = document.createElement('input')
|
|
|
|
|
|
opener.setAttribute('placeholder', 'Type a command...')
|
|
|
|
|
|
document.body.appendChild(opener)
|
|
|
|
|
|
rememberFeedbackDialogOpener(opener)
|
|
|
|
|
|
|
|
|
|
|
|
const onClose = vi.fn()
|
|
|
|
|
|
const handleReopen = vi.fn()
|
|
|
|
|
|
window.addEventListener(APP_COMMAND_EVENT_NAME, handleReopen)
|
|
|
|
|
|
|
|
|
|
|
|
const { rerender } = render(
|
|
|
|
|
|
<FeedbackDialog open={false} onClose={onClose} buildNumber="b281" releaseChannel={null} />,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
rerender(<FeedbackDialog open={true} onClose={onClose} buildNumber="b281" releaseChannel={null} />)
|
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
|
|
|
|
|
vi.advanceTimersByTime(100)
|
|
|
|
|
|
|
|
|
|
|
|
expect(onClose).toHaveBeenCalledOnce()
|
|
|
|
|
|
expect(handleReopen).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
expect(handleReopen.mock.calls[0]?.[0]).toMatchObject({
|
|
|
|
|
|
detail: APP_COMMAND_IDS.viewCommandPalette,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
window.removeEventListener(APP_COMMAND_EVENT_NAME, handleReopen)
|
|
|
|
|
|
opener.remove()
|
|
|
|
|
|
vi.useRealTimers()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|