Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions frontend/taskdeck-web/src/locales/en/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,8 +475,14 @@ export default {
similarPast: {
heading: 'Similar past decisions',
empty: 'No comparable past decisions.',
// Shown inside the disclosure when there is nothing to list, so opening the
// control explains itself instead of revealing an empty region (#1940).
emptyDetail: 'Decisions on comparable proposals will be listed here.',
details: {
show: 'Show similar decisions',
// The empty-state label. It keeps the `show` wording and adds the count,
// so the closed control does not promise a list it does not have.
showEmpty: 'Show similar decisions (none found)',
hide: 'Hide similar decisions',
},
verdict: {
Expand Down
2 changes: 2 additions & 0 deletions frontend/taskdeck-web/src/locales/es/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,10 @@ export default {
similarPast: {
heading: 'Decisiones parecidas anteriores',
empty: 'No hay decisiones anteriores comparables.',
emptyDetail: 'Aquí se mostrarán las decisiones sobre propuestas comparables.',
details: {
show: 'Mostrar decisiones parecidas',
showEmpty: 'Mostrar decisiones parecidas (ninguna encontrada)',
hide: 'Ocultar decisiones parecidas',
},
verdict: {
Expand Down
2 changes: 2 additions & 0 deletions frontend/taskdeck-web/src/locales/it/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,10 @@ export default {
similarPast: {
heading: 'Decisioni simili passate',
empty: 'Nessuna decisione passata comparabile.',
emptyDetail: 'Qui compariranno le decisioni su proposte comparabili.',
details: {
show: 'Mostra decisioni simili',
showEmpty: 'Mostra decisioni simili (nessuna trovata)',
hide: 'Nascondi decisioni simili',
},
verdict: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import ReviewAuthorCard from '../../../../views/paper/review/ReviewAuthorCard.vue'
import type {
ConfidenceBreakdown,
ConfidenceValueSource,
} from '../../../../composables/usePaperReviewSelectors'

/**
* ReviewAuthorCard — the confidence SOURCE sentence must be readable before the
* disclosure opens (#1940, the retained residual of #2166).
*
* Why this one sentence matters more than it looks: on an Applied record the
* primary confidence-source badge in ReviewMain.vue is gated off, and
* PaperReviewView passes an empty `authorMeta` for the deterministic and
* not-reported sources. The sentence inside this collapsed region was then the
* only statement on the whole screen about where the confidence number came
* from — and it was invisible until the reviewer opened a control that gave no
* reason to be opened.
*
* The disclosure itself stays in every state (the view specs and the required
* E2E smoke drive it), so the fix hoists the sentence rather than moving the
* control.
*/

function breakdownOf(
source: ConfidenceValueSource,
components: Array<{ key: string; value: number }> = [],
note?: string,
): ConfidenceBreakdown {
return { overall: components.length > 0 ? 0.9 : null, components, threshold: null, source, note }
}

function mountCard(breakdown: ConfidenceBreakdown, authorMeta = '') {
return mount(ReviewAuthorCard, {
attachTo: document.body,
props: {
authorName: 'Taskdeck',
authorMeta,
proposedDate: '2026-08-22',
proposedTime: '18:00',
proposedNum: '001',
breakdown,
},
})
}

const sourceLine = '[data-testid="paper-review-author-confidence-source"]'

describe('ReviewAuthorCard', () => {
describe('confidence source, stated before the disclosure opens', () => {
// The two sources the backend actually emits with an empty components
// array. Their wording is unchanged by #1940 — only its position is.
const CASES: Array<[string, ConfidenceValueSource, string]> = [
['deterministic extraction', 'deterministic', 'Deterministic extraction · no model confidence'],
['a model that reported nothing', 'not-reported', 'No model confidence reported'],
['a derived average', 'derived', 'No model confidence reported'],
]

it.each(CASES)('names %s at first paint, outside the collapsed region', (_label, source, copy) => {
const wrapper = mountCard(breakdownOf(source))
const line = wrapper.get(sourceLine)
const details = wrapper.get('[data-testid="paper-review-confidence-details"]')

expect(line.text()).toBe(copy)
expect(line.isVisible()).toBe(true)
expect(details.find(sourceLine).exists()).toBe(false)
expect(details.isVisible()).toBe(false)

wrapper.unmount()
})

it('says it exactly once, before and after the region opens', async () => {
const wrapper = mountCard(breakdownOf('deterministic'))
const copy = 'Deterministic extraction · no model confidence'

expect(wrapper.text().split(copy).length - 1).toBe(1)

await wrapper.get('[data-testid="paper-review-confidence-disclosure"]').trigger('click')

expect(wrapper.text().split(copy).length - 1).toBe(1)
expect(wrapper.get('[data-testid="paper-review-confidence-details"]').text()).not.toContain(copy)

wrapper.unmount()
})

it('drops the sentence entirely once there are per-component bars to read', async () => {
const wrapper = mountCard(
breakdownOf('model-reported', [
{ key: 'Operation 1: create card', value: 0.92 },
{ key: 'Operation 2: update card', value: 0.4 },
]),
'0.90 model-reported average',
)
const details = wrapper.get('[data-testid="paper-review-confidence-details"]')

expect(wrapper.find(sourceLine).exists()).toBe(false)
expect(details.isVisible()).toBe(false)

await wrapper.get('[data-testid="paper-review-confidence-disclosure"]').trigger('click')

expect(details.isVisible()).toBe(true)
expect(details.get('.paper-review-author__bd-heading').text()).toBe(
'Model-reported item confidence',
)
expect(details.findAll('.paper-review-author__bar-key').map((n) => n.text())).toEqual([
'Operation 1: create card',
'Operation 2: update card',
])

wrapper.unmount()
})

// #1940: `model-reported` with an empty components array made the heading
// announce "Model-reported item confidence" directly above a body saying
// no model confidence was reported. The backend never emits that pair —
// PaperReviewView.spec.ts builds it — but the heading is derived from what
// is actually on screen now, so the contradiction cannot be constructed.
it('derives the heading from the bars it has, not from the claimed source', async () => {
const wrapper = mountCard(breakdownOf('model-reported'))
const details = wrapper.get('[data-testid="paper-review-confidence-details"]')

expect(wrapper.get(sourceLine).text()).toBe('No model confidence reported')

await wrapper.get('[data-testid="paper-review-confidence-disclosure"]').trigger('click')

const heading = details.get('.paper-review-author__bd-heading').text()
expect(heading).toBe('Confidence source')
expect(heading).not.toBe('Model-reported item confidence')
expect(details.findAll('.paper-review-author__bar')).toHaveLength(0)

wrapper.unmount()
})

it('keeps the model note behind the disclosure', async () => {
const wrapper = mountCard(
breakdownOf('model-reported', [{ key: 'Operation 1: create card', value: 0.96 }], 'Reported by the model for the proposed operation.'),
)
const details = wrapper.get('[data-testid="paper-review-confidence-details"]')

expect(details.isVisible()).toBe(false)

await wrapper.get('[data-testid="paper-review-confidence-disclosure"]').trigger('click')

expect(details.text()).toContain('Reported by the model for the proposed operation.')

wrapper.unmount()
})
})

it('keeps the disclosure present and correctly paired while collapsed', () => {
const wrapper = mountCard(breakdownOf('deterministic'))
const button = wrapper.get('[data-testid="paper-review-confidence-disclosure"]')
const details = wrapper.get('[data-testid="paper-review-confidence-details"]')

expect(button.element.tagName).toBe('BUTTON')
expect(button.attributes('type')).toBe('button')
expect(button.attributes('aria-expanded')).toBe('false')
expect(button.attributes('aria-controls')).toBe(details.attributes('id'))
expect(details.attributes('aria-labelledby')).toBe(button.attributes('id'))
expect(details.attributes('role')).toBe('region')

wrapper.unmount()
})

// Matches ReviewProvenance.vue: `v-show` alone leaves the collapsed region in
// the accessibility tree for anything that reads the DOM rather than the
// computed style.
it('binds `hidden` to the collapsed state, like the provenance card', async () => {
const wrapper = mountCard(breakdownOf('model-reported', [{ key: 'Operation 1', value: 0.9 }]))
const button = wrapper.get('[data-testid="paper-review-confidence-disclosure"]')
const details = wrapper.get('[data-testid="paper-review-confidence-details"]')

expect(details.attributes('hidden')).toBeDefined()

await button.trigger('click')
expect(details.attributes('hidden')).toBeUndefined()

await button.trigger('click')
expect(details.attributes('hidden')).toBeDefined()

wrapper.unmount()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import ReviewSimilarPast from '../../../../views/paper/review/ReviewSimilarPast.vue'
import type { SimilarPastRow } from '../../../../composables/usePaperReviewSelectors'

/**
* ReviewSimilarPast — the card must tell the truth BEFORE the disclosure opens
* (#1940, the retained residual of #2166).
*
* The card shipped with the whole empty state locked inside a collapsed
* region: a reviewer looking at a proposal with no comparable history saw only
* "Show similar decisions" and had to open it to learn there was nothing to
* show. The fix keeps the disclosure in every state — PaperReviewView.spec.ts,
* PaperReviewView.language.spec.ts and the required E2E smoke all drive that
* button on an empty fixture — and hoists the fact above it.
*
* The empty sentence lives in exactly one place. Duplicating it above and
* inside the region would make the open state read as two separate findings.
*/

const ROWS: SimilarPastRow[] = [
{ serial: '#PAST-1', title: 'A prior comparable decision', verdict: 'applied', date: '2026-08-20' },
{ serial: '#PAST-2', title: 'A prior rejected decision', verdict: 'rejected', date: '2026-08-19' },
]

const EMPTY_SENTENCE = 'No comparable past decisions.'

function mountCard(rows: SimilarPastRow[]) {
return mount(ReviewSimilarPast, {
attachTo: document.body,
props: {
rows,
applyRate:
rows.length === 0
? { applied: 0, total: 0, ratio: 0 }
: { applied: 1, total: 2, ratio: 0.5 },
},
})
}

/** How many times a sentence appears in the rendered text of the whole card. */
function occurrences(haystack: string, needle: string): number {
return haystack.split(needle).length - 1
}

describe('ReviewSimilarPast', () => {
describe('with no comparable decisions', () => {
it('states the emptiness at first paint, above the still-collapsed disclosure', () => {
const wrapper = mountCard([])
const empty = wrapper.get('[data-testid="paper-review-similar-past-empty"]')
const details = wrapper.get('[data-testid="paper-review-similar-past-details"]')

expect(empty.text()).toBe(EMPTY_SENTENCE)
expect(empty.isVisible()).toBe(true)
// The hoisted line is a sibling of the region, not a child of it: a
// child would still be invisible while the region is collapsed.
expect(details.find('[data-testid="paper-review-similar-past-empty"]').exists()).toBe(false)
expect(details.isVisible()).toBe(false)

wrapper.unmount()
})

it('says so on the disclosure label too, so the closed control is not a promise', () => {
const wrapper = mountCard([])
const button = wrapper.get('[data-testid="paper-review-similar-past-disclosure"]')

expect(button.text()).toContain('Show similar decisions')
expect(button.text()).toContain('none found')

wrapper.unmount()
})

it('keeps the disclosure present and correctly paired while collapsed', () => {
const wrapper = mountCard([])
const button = wrapper.get('[data-testid="paper-review-similar-past-disclosure"]')
const details = wrapper.get('[data-testid="paper-review-similar-past-details"]')

expect(button.element.tagName).toBe('BUTTON')
expect(button.attributes('type')).toBe('button')
expect(button.attributes('aria-expanded')).toBe('false')
expect(button.attributes('aria-controls')).toBe(details.attributes('id'))
expect(details.attributes('aria-labelledby')).toBe(button.attributes('id'))
expect(details.attributes('role')).toBe('region')

wrapper.unmount()
})

it('never says the same thing twice', async () => {
const wrapper = mountCard([])
const details = wrapper.get('[data-testid="paper-review-similar-past-details"]')

expect(occurrences(wrapper.text(), EMPTY_SENTENCE)).toBe(1)
expect(details.text()).not.toContain(EMPTY_SENTENCE)

await wrapper.get('[data-testid="paper-review-similar-past-disclosure"]').trigger('click')

expect(occurrences(wrapper.text(), EMPTY_SENTENCE)).toBe(1)
expect(details.text()).not.toContain(EMPTY_SENTENCE)

wrapper.unmount()
})

it('opens onto an explanation rather than onto nothing', async () => {
const wrapper = mountCard([])
const button = wrapper.get('[data-testid="paper-review-similar-past-disclosure"]')
const details = wrapper.get('[data-testid="paper-review-similar-past-details"]')

await button.trigger('click')

expect(button.attributes('aria-expanded')).toBe('true')
expect(details.isVisible()).toBe(true)
// A region that opens to a zero-height void reads as a broken control,
// and the required E2E asserts this region is *visible* once opened.
expect(details.get('[data-testid="paper-review-similar-past-empty-detail"]').text()).toBe(
'Decisions on comparable proposals will be listed here.',
)
expect(wrapper.find('.paper-review-past__rate').exists()).toBe(false)

wrapper.unmount()
})
})

describe('with comparable decisions', () => {
it('keeps the rows and the apply-rate footer behind the disclosure', async () => {
const wrapper = mountCard(ROWS)
const button = wrapper.get('[data-testid="paper-review-similar-past-disclosure"]')
const details = wrapper.get('[data-testid="paper-review-similar-past-details"]')
const rate = wrapper.get('.paper-review-past__rate')

expect(wrapper.find('[data-testid="paper-review-similar-past-empty"]').exists()).toBe(false)
expect(wrapper.text()).not.toContain(EMPTY_SENTENCE)
expect(button.text()).toContain('Show similar decisions')
expect(button.text()).not.toContain('none found')
expect(details.isVisible()).toBe(false)
expect(rate.isVisible()).toBe(false)

await button.trigger('click')

expect(details.isVisible()).toBe(true)
expect(details.text()).toContain('A prior comparable decision')
expect(details.text()).toContain('A prior rejected decision')
expect(rate.isVisible()).toBe(true)
expect(rate.text()).toContain('1 of 2 (50%)')
expect(
wrapper.find('[data-testid="paper-review-similar-past-empty-detail"]').exists(),
).toBe(false)

wrapper.unmount()
})
})

// Matches ReviewProvenance.vue: `v-show` alone leaves the collapsed region in
// the accessibility tree for anything that reads the DOM rather than the
// computed style.
it('binds `hidden` to the collapsed state, like the provenance card', async () => {
const wrapper = mountCard(ROWS)
const button = wrapper.get('[data-testid="paper-review-similar-past-disclosure"]')
const details = wrapper.get('[data-testid="paper-review-similar-past-details"]')

expect(details.attributes('hidden')).toBeDefined()

await button.trigger('click')
expect(details.attributes('hidden')).toBeUndefined()

await button.trigger('click')
expect(details.attributes('hidden')).toBeDefined()

wrapper.unmount()
})
})
Loading
Loading