Browse Source

test(feedback): cover the seed-failure and unmount paths of the Dislike control

creatixchu 2 tuần trước cách đây
mục cha
commit
700e8df91a

+ 3 - 0
packages/client/ui-message-feedback/tests/browser-plugin.client.spec.tsx

@@ -129,6 +129,9 @@ describe('ui-message-feedback browser plugin', () => {
     const face = b.entry()!.inject!(sid('s1'))
     expect(face.hooks.feedback.getSnapshot()).toMatchObject({ status: 'cold' })
     expect(face.ensure).toBeTypeOf('function')
+    expect(face.current(MSG)).toBeUndefined()
+    await face.ensure()
+    expect(face.current(MSG)).toMatchObject({ messageId: MSG, rating: 'positive' })
     expect(face.toggle).toBeTypeOf('function')
     expect(face.openDialog).toBeTypeOf('function')
     expect(face.acknowledge).toBeTypeOf('function')

+ 46 - 1
packages/client/ui-message-feedback/tests/message-feedback-actions.client.spec.tsx

@@ -43,6 +43,7 @@ function mount(options: {
   current?: MessageFeedbackItem | undefined
   /** The controller's committed item when it differs from the rendered view (a cold row). */
   committed?: MessageFeedbackItem
+  ensureResult?: MessageFeedbackActionResult
   toggleResult?: MessageFeedbackToggleResult
   status?: MessageFeedbackView['status']
 } = {}) {
@@ -51,7 +52,7 @@ function mount(options: {
     items: new Map(options.current === undefined ? [] : [[MSG, options.current]]),
     error: null,
   }
-  const ensure = vi.fn(() => Promise.resolve<MessageFeedbackActionResult>({ ok: true }))
+  const ensure = vi.fn(() => Promise.resolve<MessageFeedbackActionResult>(options.ensureResult ?? { ok: true }))
   // The controller owns record-vs-retract, so the double stands in for it:
   // matching the shown rating retracts, anything else records.
   const toggle = vi.fn((_id: MessageId, next: MessageFeedbackRating) =>
@@ -140,6 +141,15 @@ describe('MessageFeedbackActions', () => {
     expect(ui.toggle).not.toHaveBeenCalled()
   })
 
+  it('opens the dialog for a Dislike when the seeding read fails, leaving the put to decide', async () => {
+    const ui = mount({ ensureResult: { ok: false, error: { code: 'session-not-found', message: 'gone' } } })
+
+    fireEvent.click(ui.getByLabelText(zh['action.dislike']))
+
+    await waitFor(() => { expect(ui.openDialog).toHaveBeenCalledWith(MSG) })
+    expect(ui.toggle).not.toHaveBeenCalled()
+  })
+
   it('decides a Dislike from the committed item, so a cold row retracts a stored Dislike', async () => {
     const ui = mount({ committed: item({ rating: 'negative' }) })
 
@@ -222,6 +232,38 @@ describe('MessageFeedbackActions', () => {
     expect(errors).toEqual([])
   })
 
+  it('publishes no state after the row unmounts before the seeding read settles', async () => {
+    let release = (): void => {}
+    const gate = new Promise<MessageFeedbackActionResult>((resolve) => { release = () => { resolve({ ok: true }) } })
+    const view: MessageFeedbackView = { status: 'cold', items: new Map(), error: null }
+    const useFeedback = (<T,>(select: (v: MessageFeedbackView) => T): T =>
+      useSyncExternalStore(() => () => {}, () => select(view))) as never
+    const openDialog = vi.fn()
+    const props = {
+      messageId: MSG,
+      ensure: vi.fn(() => gate),
+      current: () => undefined,
+      toggle: vi.fn(),
+      openDialog,
+      acknowledge: vi.fn(),
+      useFeedback,
+      t,
+    } as unknown as Parameters<typeof MessageFeedbackActions>[0]
+    const ui = render(<MessageFeedbackActions {...props} />)
+    const errors: unknown[] = []
+    const onError = (event: ErrorEvent): void => { errors.push(event.error) }
+    window.addEventListener('error', onError)
+
+    fireEvent.click(ui.getByLabelText(zh['action.dislike']))
+    ui.unmount()
+    release()
+    await gate
+
+    window.removeEventListener('error', onError)
+    expect(errors).toEqual([])
+    expect(openDialog).not.toHaveBeenCalled()
+  })
+
   it('publishes no state after the row unmounts mid-retraction', async () => {
     let release = (): void => {}
     const gate = new Promise<MessageFeedbackToggleResult>((resolve) => {
@@ -248,6 +290,9 @@ describe('MessageFeedbackActions', () => {
     window.addEventListener('error', onError)
 
     fireEvent.click(ui.getByLabelText(zh['action.dislikeActive']))
+    // The retraction starts only after the seeding read settles; unmount
+    // while that retraction is in flight.
+    await waitFor(() => { expect(props.toggle).toHaveBeenCalledWith(MSG, 'negative') })
     ui.unmount()
     release()
     await gate