@igstack/app-catalog-frontend-core 6.9.0 → 6.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.d.ts +0 -0
  2. package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.d.ts +0 -0
  3. package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.d.ts +0 -0
  4. package/dist/esm/__tests__/modules/appCatalog/ui/detail/composeComments.test.d.ts +1 -0
  5. package/dist/esm/__tests__/ui/detail/CommunityInput.test.d.ts +1 -0
  6. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +8 -0
  7. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +1 -0
  8. package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
  9. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +237 -218
  10. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
  11. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.d.ts +17 -0
  12. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js +187 -0
  13. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js.map +1 -0
  14. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.d.ts +9 -0
  15. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js +91 -0
  16. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js.map +1 -0
  17. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.d.ts +17 -0
  18. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js +328 -0
  19. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js.map +1 -0
  20. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.d.ts +15 -0
  21. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js +26 -0
  22. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js.map +1 -0
  23. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.d.ts +8 -19
  24. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +49 -37
  25. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -1
  26. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.d.ts +30 -0
  27. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js +50 -0
  28. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js.map +1 -0
  29. package/dist/esm/modules/appCatalog/ui/detail/communityError.d.ts +6 -0
  30. package/dist/esm/modules/appCatalog/ui/detail/communityError.js +8 -0
  31. package/dist/esm/modules/appCatalog/ui/detail/communityError.js.map +1 -0
  32. package/dist/esm/modules/appCatalog/ui/detail/composeComments.d.ts +27 -0
  33. package/dist/esm/modules/appCatalog/ui/detail/composeComments.js +66 -0
  34. package/dist/esm/modules/appCatalog/ui/detail/composeComments.js.map +1 -0
  35. package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.d.ts +11 -0
  36. package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.js +15 -0
  37. package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.js.map +1 -0
  38. package/dist/esm/modules/extensions/features/resourceDetail.d.ts +38 -0
  39. package/dist/esm/modules/extensions/features/resourceDetail.js +4 -0
  40. package/dist/esm/modules/extensions/features/resourceDetail.js.map +1 -1
  41. package/dist/esm/modules/extensions/index.d.ts +1 -1
  42. package/dist/esm/modules/extensions/index.js.map +1 -1
  43. package/dist/index.css +501 -49
  44. package/package.json +4 -4
  45. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +5 -2
  46. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.enter.test.tsx +5 -2
  47. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +9 -10
  48. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.behindThisCard.test.tsx +402 -0
  49. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +8 -4
  50. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +5 -2
  51. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.knowMore.test.tsx +175 -0
  52. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +5 -2
  53. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +5 -2
  54. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +5 -2
  55. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.tsx +281 -0
  56. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.tsx +179 -0
  57. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.tsx +423 -0
  58. package/src/__tests__/modules/appCatalog/ui/detail/composeComments.test.ts +170 -0
  59. package/src/__tests__/ui/detail/CommunityInput.test.tsx +39 -0
  60. package/src/index.css +501 -49
  61. package/src/modules/appCatalog/ui/detail/AppDetails.tsx +292 -289
  62. package/src/modules/appCatalog/ui/detail/CommentsFeed.tsx +254 -0
  63. package/src/modules/appCatalog/ui/detail/CommentsHelp.tsx +102 -0
  64. package/src/modules/appCatalog/ui/detail/CommunityInput.tsx +419 -0
  65. package/src/modules/appCatalog/ui/detail/CommunitySection.tsx +41 -0
  66. package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +77 -71
  67. package/src/modules/appCatalog/ui/detail/communityDraft.ts +80 -0
  68. package/src/modules/appCatalog/ui/detail/communityError.ts +10 -0
  69. package/src/modules/appCatalog/ui/detail/composeComments.ts +103 -0
  70. package/src/modules/appCatalog/ui/detail/usePendingCount.ts +26 -0
  71. package/src/modules/extensions/features/resourceDetail.ts +28 -0
  72. package/src/modules/extensions/index.ts +2 -0
  73. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.d.ts +0 -13
  74. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js +0 -433
  75. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js.map +0 -1
  76. package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.tsx +0 -181
  77. package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.tsx +0 -131
  78. package/src/modules/appCatalog/ui/detail/FeedbackSection.tsx +0 -605
  79. /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{FeedbackSection.emptyState.test.d.ts → AppDetails.behindThisCard.test.d.ts} +0 -0
  80. /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{FeedbackSection.reviewStatus.test.d.ts → AppDetails.knowMore.test.d.ts} +0 -0
@@ -0,0 +1,281 @@
1
+ import type { Claim } from '@igstack/app-catalog-backend-core'
2
+ import { fireEvent, render, screen, within } from '@testing-library/react'
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import '@testing-library/jest-dom/vitest'
5
+
6
+ // Only the list query matters here; mutations are inert.
7
+ const state = vi.hoisted(() => ({
8
+ items: [] as unknown[],
9
+ calls: [] as [string, unknown][],
10
+ }))
11
+
12
+ vi.mock('@tanstack/react-query', () => ({
13
+ useQuery: vi.fn(() => ({
14
+ data: { items: state.items, openCount: 0 },
15
+ isLoading: false,
16
+ isPending: false,
17
+ })),
18
+ useMutation: vi.fn((options?: { name?: string }) => ({
19
+ mutate: (vars: unknown) => state.calls.push([options?.name ?? '', vars]),
20
+ isPending: false,
21
+ })),
22
+ useQueryClient: vi.fn(() => ({ invalidateQueries: vi.fn() })),
23
+ }))
24
+ vi.mock('~/api/infra/trpc', () => ({
25
+ useTRPC: vi.fn(() => ({
26
+ feedback: {
27
+ list: {
28
+ queryOptions: () => ({ queryKey: ['feedback'] }),
29
+ queryKey: () => ['feedback'],
30
+ },
31
+ add: { mutationOptions: (o: object) => ({ ...o, name: 'add' }) },
32
+ edit: { mutationOptions: (o: object) => ({ ...o, name: 'edit' }) },
33
+ dismiss: { mutationOptions: (o: object) => ({ ...o, name: 'dismiss' }) },
34
+ },
35
+ })),
36
+ }))
37
+
38
+ const { CommunitySection } =
39
+ await import('~/modules/appCatalog/ui/detail/CommunitySection')
40
+
41
+ const note = {
42
+ id: 'c1',
43
+ authorName: 'Merry Mole',
44
+ body: 'This portal only covers one business unit.',
45
+ subject: null,
46
+ resourceSlug: 'example-app',
47
+ resourceName: 'Example App',
48
+ attachmentIds: [] as string[],
49
+ createdAt: '2026-10-10T10:00:00.000Z',
50
+ reviewedAt: null as string | null,
51
+ editedAt: null,
52
+ isMine: false,
53
+ canEditUntil: null as string | null,
54
+ status: null as string | null,
55
+ reviewerReply: null as string | null,
56
+ }
57
+
58
+ function mount(claims: Claim[] = []) {
59
+ render(
60
+ <CommunitySection
61
+ appSlug="example-app"
62
+ appName="Example App"
63
+ claims={claims}
64
+ />,
65
+ )
66
+ }
67
+
68
+ const items = () =>
69
+ screen.getByRole('list', { name: 'Comments' }).querySelectorAll(':scope > li')
70
+ const texts = () =>
71
+ [...items()].map((li) => li.querySelector('.comment__text')?.textContent)
72
+
73
+ beforeEach(() => {
74
+ state.items = []
75
+ state.calls = []
76
+ })
77
+
78
+ describe('CommunitySection — the Comments feed', () => {
79
+ it('has a Comments heading and none of the old sub-sections', () => {
80
+ state.items = [{ ...note }, { ...note, id: 'c2', status: 'applied' }]
81
+ mount([
82
+ { text: 'Archived.', addedAt: '2026-08-03', status: 'archived' },
83
+ { text: 'Fact.', addedAt: '2026-08-03', status: 'active' },
84
+ ])
85
+ expect(screen.getByRole('heading', { name: 'Comments' })).toBeVisible()
86
+ for (const name of [
87
+ /Notes in review/,
88
+ /Facts we rely on/,
89
+ /Answered/,
90
+ /Archived/,
91
+ ]) {
92
+ expect(screen.queryByRole('heading', { name })).toBeNull()
93
+ expect(screen.queryByText(name)).toBeNull()
94
+ }
95
+ })
96
+
97
+ it('shows a note once, with its claim’s badge, newest first', () => {
98
+ state.items = [
99
+ {
100
+ ...note,
101
+ id: 'old',
102
+ body: 'Older open note.',
103
+ createdAt: '2026-10-01T09:00:00Z',
104
+ },
105
+ {
106
+ ...note,
107
+ id: 'used',
108
+ body: 'Leads approve.',
109
+ status: 'applied',
110
+ createdAt: '2026-10-12T09:00:00Z',
111
+ },
112
+ ]
113
+ mount([
114
+ {
115
+ text: 'Team leads approve access.',
116
+ addedAt: '2026-10-13',
117
+ origin: 'feedback:used',
118
+ status: 'active',
119
+ },
120
+ { text: 'No longer true.', addedAt: '2026-10-14', status: 'archived' },
121
+ ])
122
+ expect(texts()).toEqual(['Leads approve.', 'Older open note.'])
123
+ const [first, second] = [...items()] as HTMLElement[]
124
+ expect(within(first!).getByText('Used as source')).toBeVisible()
125
+ expect(within(second!).getByText('Pending')).toBeVisible()
126
+ expect(screen.queryByText('Team leads approve access.')).toBeNull()
127
+ expect(screen.queryByText('No longer true.')).toBeNull()
128
+ })
129
+
130
+ it('puts who and when above the text, with the raw date in a title', () => {
131
+ state.items = [{ ...note }]
132
+ mount()
133
+ const meta = items()[0]!.querySelector('.comment__meta')!
134
+ expect(meta).toHaveTextContent(/^Merry Mole · /)
135
+ expect(within(meta as HTMLElement).getByTitle(note.createdAt)).toBeVisible()
136
+ expect(
137
+ meta.compareDocumentPosition(
138
+ items()[0]!.querySelector('.comment__text')!,
139
+ ) & Node.DOCUMENT_POSITION_FOLLOWING,
140
+ ).toBeTruthy()
141
+ })
142
+
143
+ it('shows a reviewed note’s reply under it, with no badge', () => {
144
+ state.items = [
145
+ {
146
+ ...note,
147
+ status: 'acknowledged',
148
+ reviewerReply: 'Not in the portal today.',
149
+ },
150
+ ]
151
+ mount()
152
+ expect(screen.getByText('↳ Not in the portal today.')).toBeVisible()
153
+ expect(screen.queryByText('Pending')).toBeNull()
154
+ expect(screen.queryByText('Used as source')).toBeNull()
155
+ })
156
+
157
+ it('shows a claim with no note in the list by its origin', () => {
158
+ mount([
159
+ {
160
+ text: 'Old boards are read-only.',
161
+ addedAt: '2026-10-15',
162
+ origin: 'Tools team',
163
+ status: 'review',
164
+ },
165
+ ])
166
+ expect(texts()).toEqual(['Old boards are read-only.'])
167
+ expect(items()[0]).toHaveTextContent(/^Tools team · .*Pending/)
168
+ })
169
+
170
+ it('renders nothing in the list when there is nothing said yet', () => {
171
+ mount()
172
+ expect(screen.queryByRole('list', { name: 'Comments' })).toBeNull()
173
+ })
174
+
175
+ it('shows a note’s screenshots under its text', () => {
176
+ state.items = [{ ...note, body: null, attachmentIds: ['img-1'] }]
177
+ mount()
178
+ expect(screen.getByText(/Screenshot only/)).toBeVisible()
179
+ expect(
180
+ screen.getByRole('img', { name: 'Attached screenshot' }),
181
+ ).toBeVisible()
182
+ })
183
+
184
+ it('says so when a note arrived with no words and no image', () => {
185
+ state.items = [{ ...note, body: null }]
186
+ mount()
187
+ expect(screen.getByText(/no detail given/)).toBeInTheDocument()
188
+ })
189
+
190
+ it('offers Dismiss on your own open note, and not once it is answered', () => {
191
+ state.items = [{ ...note, isMine: true }]
192
+ const { unmount } = render(
193
+ <CommunitySection appSlug="example-app" appName="Example App" />,
194
+ )
195
+ expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument()
196
+ expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull()
197
+ unmount()
198
+
199
+ state.items = [{ ...note, isMine: true, status: 'applied' }]
200
+ mount()
201
+ expect(screen.queryByRole('button', { name: 'Dismiss' })).toBeNull()
202
+ })
203
+
204
+ it('offers Edit within the hour on your own note', () => {
205
+ state.items = [
206
+ {
207
+ ...note,
208
+ isMine: true,
209
+ canEditUntil: new Date(Date.now() + 60_000).toISOString(),
210
+ },
211
+ ]
212
+ mount()
213
+ fireEvent.click(screen.getByRole('button', { name: 'Edit' }))
214
+ const box = screen.getByDisplayValue(
215
+ 'This portal only covers one business unit.',
216
+ )
217
+ expect(box).toHaveFocus()
218
+ fireEvent.change(box, { target: { value: ' Covers two units now. ' } })
219
+ fireEvent.click(screen.getByRole('button', { name: 'Save' }))
220
+ expect(state.calls).toEqual([
221
+ ['edit', { id: 'c1', body: 'Covers two units now.' }],
222
+ ])
223
+ })
224
+
225
+ it('withdraws your own open note through feedback.dismiss', () => {
226
+ state.items = [{ ...note, isMine: true }]
227
+ mount()
228
+ fireEvent.click(screen.getByRole('button', { name: 'Dismiss' }))
229
+ expect(state.calls).toEqual([['dismiss', { id: 'c1' }]])
230
+ })
231
+
232
+ it('renders claims with the same text without key warnings', () => {
233
+ const spy = vi.spyOn(console, 'error')
234
+ mount([
235
+ { text: 'Single sign-on.', addedAt: '2026-10-01', status: 'active' },
236
+ { text: 'Single sign-on.', addedAt: '2026-10-01', status: 'active' },
237
+ ])
238
+ expect(screen.getAllByText('Single sign-on.')).toHaveLength(2)
239
+ expect(spy).not.toHaveBeenCalledWith(expect.stringMatching(/same key/))
240
+ spy.mockRestore()
241
+ })
242
+
243
+ it('shows a skeleton with aria-busy while loading', async () => {
244
+ const { useQuery } = await import('@tanstack/react-query')
245
+ vi.mocked(useQuery).mockReturnValueOnce({
246
+ data: undefined,
247
+ error: null,
248
+ isPending: true,
249
+ } as never)
250
+ mount()
251
+ const status = screen.getByText('Loading comments…')
252
+ expect(status).toBeInTheDocument()
253
+ const skeleton = status.closest('.comments-skeleton')
254
+ expect(skeleton).toHaveAttribute('aria-busy', 'true')
255
+ })
256
+
257
+ it('replaces the skeleton with the feed once data arrives', async () => {
258
+ const { useQuery } = await import('@tanstack/react-query')
259
+ vi.mocked(useQuery).mockReturnValueOnce({
260
+ data: undefined,
261
+ error: null,
262
+ isPending: true,
263
+ } as never)
264
+ const { unmount } = render(
265
+ <CommunitySection appSlug="example-app" appName="Example App" />,
266
+ )
267
+ expect(screen.getByText('Loading comments…')).toBeInTheDocument()
268
+ unmount()
269
+
270
+ state.items = [{ ...note }]
271
+ mount()
272
+ expect(screen.queryByText('Loading comments…')).toBeNull()
273
+ expect(screen.getByRole('list', { name: 'Comments' })).toBeVisible()
274
+ })
275
+
276
+ it('shows an empty-state message when there are no entries', () => {
277
+ mount()
278
+ expect(screen.getByText(/No comments yet/)).toBeInTheDocument()
279
+ expect(screen.queryByRole('list', { name: 'Comments' })).toBeNull()
280
+ })
281
+ })
@@ -0,0 +1,179 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react'
2
+ import React from 'react'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import '@testing-library/jest-dom/vitest'
5
+ import type { AcPlugin } from '~/modules/extensions'
6
+ import { ExtensionsContext } from '~/modules/extensions'
7
+ import { CommunitySection } from '~/modules/appCatalog/ui/detail/CommunitySection'
8
+ import { useDismissable } from '~/modules/dismiss/DismissStack'
9
+
10
+ // Stub the query layer so the component mounts.
11
+ vi.mock('@tanstack/react-query', () => ({
12
+ useQuery: vi.fn(() => ({ data: { items: [], openCount: 0 } })),
13
+ useMutation: vi.fn(() => ({
14
+ mutate: vi.fn(),
15
+ isPending: false,
16
+ isError: false,
17
+ })),
18
+ useQueryClient: vi.fn(() => ({
19
+ invalidateQueries: vi.fn(),
20
+ })),
21
+ }))
22
+ vi.mock('~/api/infra/trpc', () => ({
23
+ useTRPC: vi.fn(() => ({
24
+ feedback: {
25
+ add: { mutationOptions: (o: unknown) => o },
26
+ edit: { mutationOptions: (o: unknown) => o },
27
+ dismiss: { mutationOptions: (o: unknown) => o },
28
+ list: {
29
+ queryOptions: () => ({ queryKey: ['feedback', 'list'] }),
30
+ queryKey: () => ['feedback', 'list'],
31
+ },
32
+ },
33
+ })),
34
+ }))
35
+
36
+ /** Stands in for the card: a dismiss layer around the section. */
37
+ function Card({
38
+ onClose,
39
+ children,
40
+ }: {
41
+ onClose: () => void
42
+ children: React.ReactNode
43
+ }) {
44
+ const ref = React.useRef<HTMLDivElement>(null)
45
+ useDismissable(ref, onClose)
46
+ return <div ref={ref}>{children}</div>
47
+ }
48
+
49
+ const onCardClose = vi.fn()
50
+ function mount(plugins: AcPlugin[] | undefined) {
51
+ onCardClose.mockReset()
52
+ return render(
53
+ <ExtensionsContext value={plugins}>
54
+ <Card onClose={onCardClose}>
55
+ <CommunitySection appSlug="test-app" appName="Test App" />
56
+ </Card>
57
+ </ExtensionsContext>,
58
+ )
59
+ }
60
+ const helpButton = () =>
61
+ screen.getByRole('button', { name: 'How comments work' })
62
+ const DEFAULT_NOTE =
63
+ 'Every note is reviewed. Facts that check out update the card and are marked Used as source.'
64
+
65
+ describe('CommunitySection — How comments work', () => {
66
+ it('toggles the popover with aria-expanded', () => {
67
+ mount(undefined)
68
+ const button = helpButton()
69
+ expect(button).toHaveAttribute('aria-expanded', 'false')
70
+ const popover = document.getElementById(
71
+ button.getAttribute('aria-controls')!,
72
+ )!
73
+ expect(popover).not.toBeVisible()
74
+
75
+ fireEvent.click(button)
76
+ expect(button).toHaveAttribute('aria-expanded', 'true')
77
+ expect(popover).toBeVisible()
78
+ expect(popover).toHaveTextContent('How comments work')
79
+ expect(popover).toHaveTextContent(
80
+ 'Paste a link — a wiki page, a doc or a ticket that explains something about this app.',
81
+ )
82
+ expect(popover).toHaveTextContent(
83
+ 'Fix something — "Access is approved by your team lead, not IT."',
84
+ )
85
+ expect(popover).toHaveTextContent(
86
+ 'Ask a question — "Can contractors get access?"',
87
+ )
88
+ expect(popover).toHaveTextContent(
89
+ 'Attach a screenshot — paste it, drop it or use the paperclip.',
90
+ )
91
+
92
+ fireEvent.click(button)
93
+ expect(button).toHaveAttribute('aria-expanded', 'false')
94
+ expect(popover).not.toBeVisible()
95
+ })
96
+
97
+ it('closes on Esc, returns focus to the button, and keeps the card open', () => {
98
+ mount(undefined)
99
+ fireEvent.click(helpButton())
100
+ // From anywhere in the card, an empty input included.
101
+ const field = screen.getByRole('textbox', {
102
+ name: 'Know more about Test App?',
103
+ })
104
+ field.focus()
105
+ fireEvent.keyDown(field, { key: 'Escape' })
106
+ expect(helpButton()).toHaveAttribute('aria-expanded', 'false')
107
+ expect(helpButton()).toHaveFocus()
108
+ expect(onCardClose).not.toHaveBeenCalled()
109
+
110
+ // Closed, it leaves Esc to the card.
111
+ fireEvent.keyDown(helpButton(), { key: 'Escape' })
112
+ expect(onCardClose).toHaveBeenCalledTimes(1)
113
+ })
114
+
115
+ it('closes on a click outside it', () => {
116
+ mount(undefined)
117
+ fireEvent.click(helpButton())
118
+ fireEvent.pointerDown(document.body)
119
+ expect(helpButton()).toHaveAttribute('aria-expanded', 'false')
120
+ })
121
+
122
+ it('stays open on a click inside it', () => {
123
+ mount(undefined)
124
+ fireEvent.click(helpButton())
125
+ fireEvent.pointerDown(screen.getByText(DEFAULT_NOTE))
126
+ expect(helpButton()).toHaveAttribute('aria-expanded', 'true')
127
+ })
128
+
129
+ it('says what happens to a note by default', () => {
130
+ mount(undefined)
131
+ fireEvent.click(helpButton())
132
+ expect(screen.getByText(DEFAULT_NOTE)).toBeVisible()
133
+ })
134
+
135
+ it('renders a plugin note instead of the default', () => {
136
+ const plugin: AcPlugin = {
137
+ name: 'test-note',
138
+ slots: {
139
+ resourceDetailFeedbackNote: ({ appSlug, appName }) => (
140
+ <>
141
+ Custom note for {appName} ({appSlug})
142
+ </>
143
+ ),
144
+ },
145
+ }
146
+ mount([plugin])
147
+ fireEvent.click(helpButton())
148
+ expect(
149
+ screen.getByText(/Custom note for Test App \(test-app\)/),
150
+ ).toBeVisible()
151
+ expect(screen.queryByText(DEFAULT_NOTE)).not.toBeInTheDocument()
152
+ })
153
+
154
+ it('has no note line under the input', () => {
155
+ mount(undefined)
156
+ const form = screen
157
+ .getByRole('textbox', { name: 'Know more about Test App?' })
158
+ .closest('form')!
159
+ expect(form).not.toHaveTextContent(/reviewed|maintainer/)
160
+ })
161
+
162
+ it('passes appSlug and appName to the plugin', () => {
163
+ let receivedProps: { appSlug: string; appName: string } | null = null
164
+ const plugin: AcPlugin = {
165
+ name: 'props-check',
166
+ slots: {
167
+ resourceDetailFeedbackNote: (props) => {
168
+ receivedProps = props
169
+ return <>Received</>
170
+ },
171
+ },
172
+ }
173
+ mount([plugin])
174
+ expect(receivedProps).toEqual({
175
+ appSlug: 'test-app',
176
+ appName: 'Test App',
177
+ })
178
+ })
179
+ })