test(feedback): reach the per-file coverage gate

Cover the reachable controller and control branches: each failure code's
copy, dispose-during-flight, non-Error rejections, and non-conflict
mutation failures.

Two paths were unreachable rather than untested, so remove them instead:
commit() cannot run after disposal because mutate() refuses admission
first, and the mutation tail cannot reject because every queued operation
settles as a result. Pass the recorded rating into the note save so the
editor's render site proves it exists.
This commit is contained in:
Chinesezjc
2026-08-11 17:52:09 +08:00
parent 9950c64621
commit 5867c00faf
4 changed files with 271 additions and 11 deletions

View File

@@ -60,16 +60,17 @@ export function FeedbackActions({ messageId, ensure, rate, clear, useFeedback, t
void rate(messageId, next, item?.note).then(settle)
}, [clear, item?.note, messageId, rate, rating, settle])
const onSaveNote = useCallback(() => {
if (rating === undefined) return
// The rating is a parameter because only the note editor's render site can
// prove one is recorded; that removes an unreachable undefined guard here.
const onSaveNote = useCallback((current: MessageFeedbackRating) => {
const trimmed = draft.trim()
setPending(true)
setFailure(null)
void rate(messageId, rating, trimmed.length === 0 ? undefined : trimmed).then((result) => {
void rate(messageId, current, trimmed.length === 0 ? undefined : trimmed).then((result) => {
settle(result)
if (result.ok && alive.current) setNoteOpen(false)
})
}, [draft, messageId, rate, rating, settle])
}, [draft, messageId, rate, settle])
const openNote = useCallback(() => {
setDraft(item?.note ?? '')
@@ -116,7 +117,7 @@ export function FeedbackActions({ messageId, ensure, rate, clear, useFeedback, t
{item?.note === undefined ? t('note.open') : item.note}
</button>
)}
{noteOpen && (
{rating !== undefined && noteOpen && (
<span className={css.noteEditor}>
<textarea
className={css.noteInput}
@@ -126,7 +127,12 @@ export function FeedbackActions({ messageId, ensure, rate, clear, useFeedback, t
rows={2}
onChange={(event) => { setDraft(event.target.value) }}
/>
<button type="button" className={css.noteSave} disabled={pending} onClick={onSaveNote}>
<button
type="button"
className={css.noteSave}
disabled={pending}
onClick={() => { onSaveNote(rating) }}
>
{t('note.save')}
</button>
<button type="button" className={css.noteCancel} onClick={() => { setNoteOpen(false) }}>

View File

@@ -238,15 +238,20 @@ export class FeedbackController implements HostObservable<FeedbackView> {
}
}
const result = this.operationTail.then(guarded, guarded)
// Every queued operation settles carrier and business failures as a
// FeedbackActionResult, so this controlled tail cannot reject.
this.operationTail = result.then(() => undefined, () => undefined)
// `guarded` settles every carrier and business failure as a
// FeedbackActionResult and never rethrows, so this tail cannot reject and
// needs no rejection handler.
this.operationTail = result.then(() => undefined)
return result
}
/** Replace one message's entry, keeping every other entry's identity. */
/**
* Replace one message's entry, keeping every other entry's identity. Only a
* `mutate` operation reaches this, and `mutate` refuses admission once the
* controller is disposed, so no disposal guard belongs here; `publish` is
* the single place that stops notifying after listeners are dropped.
*/
private commit(messageId: MessageId, item: MessageFeedbackItem | null): void {
if (this.disposed) return
const items = new Map(this.view.items)
if (item === null) items.delete(messageId)
else items.set(messageId, item)