Files
accounted/extensions/general/enable-banking/lib/__tests__/connection-state.test.ts
T
1ded1af8fe fix(enable-banking): one primary action per connection state on /settings/banking (#1727)
Restructure the banking settings page so every connection state has a
clear hierarchy:

- One "Dina bankkopplingar" group sorted by state precedence
  (pending_selection, pending, error, expired, expiring soon, active),
  replacing the three-way group split. State derivation, sorting and
  worst-state selection live in a pure, unit-tested helper
  (lib/connection-state.ts).
- Exactly one page-level .attn sentence for the worst state, or none;
  the BankSyncStatusChip is removed from this page (it linked to
  itself; it stays on /transactions and /import).
- Each row shows one primary action per state (Valj konton, Forsok
  igen, Fornya samtycke, Synka nu); everything else moves into a "..."
  menu, and details (accounts, IBAN, balances, initial historik) sit
  behind a collapsed disclosure. Expired rows never show balances.
- Expiring-soon active rows get a "Fornya samtycke" primary that
  reconnects without a psu-type override (the server reuses the stored
  psu_type); the explicit account-type choice stays in the menu.
- "Anslut ny bank" collapses behind one outline "Anslut en bank till"
  button whenever a non-revoked connection exists; the reuse-session
  group only shows while the connect-new surface is visible.
- Fresh connects to an already-connected bank are intercepted with a
  renew-instead dialog; "Anslut som ny" proceeds with force_new: true
  for the upcoming server-side 409 guard.
- In-flight 'pending' rows render as a spinner row ("Vantar pa banken")
  instead of being invisible.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 10:07:04 +02:00

150 lines
5.5 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
buildPageAttentionSentence,
getConnectionUiState,
selectPageAttention,
sortConnectionsByPrecedence,
EXPIRY_WARNING_DAYS,
STALE_SYNC_DAYS,
} from '../connection-state'
const NOW = new Date('2026-08-19T12:00:00Z').getTime()
const DAY_MS = 24 * 60 * 60 * 1000
function iso(offsetDays: number): string {
return new Date(NOW + offsetDays * DAY_MS).toISOString()
}
function conn(overrides: {
status?: string
consent_expires?: string | null
last_synced_at?: string | null
created_at?: string
bank_name?: string
}) {
return {
status: 'active',
consent_expires: iso(60),
last_synced_at: iso(-1),
created_at: iso(-30),
bank_name: 'SEB',
...overrides,
}
}
describe('getConnectionUiState', () => {
it('maps DB statuses straight through', () => {
expect(getConnectionUiState(conn({ status: 'pending_selection' }), NOW)).toBe('pending_selection')
expect(getConnectionUiState(conn({ status: 'pending' }), NOW)).toBe('pending')
expect(getConnectionUiState(conn({ status: 'error' }), NOW)).toBe('error')
expect(getConnectionUiState(conn({ status: 'expired' }), NOW)).toBe('expired')
})
it('classifies a healthy active row as active', () => {
expect(getConnectionUiState(conn({}), NOW)).toBe('active')
})
it('flags consent expiring within the warning window', () => {
expect(
getConnectionUiState(conn({ consent_expires: iso(EXPIRY_WARNING_DAYS - 1) }), NOW),
).toBe('expiring')
expect(
getConnectionUiState(conn({ consent_expires: iso(EXPIRY_WARNING_DAYS + 1) }), NOW),
).toBe('active')
})
it('expiring beats stale on the same row', () => {
expect(
getConnectionUiState(
conn({ consent_expires: iso(2), last_synced_at: iso(-10) }),
NOW,
),
).toBe('expiring')
})
it('flags stale and never-synced active rows', () => {
expect(
getConnectionUiState(conn({ last_synced_at: iso(-STALE_SYNC_DAYS) }), NOW),
).toBe('stale')
expect(
getConnectionUiState(conn({ last_synced_at: iso(-(STALE_SYNC_DAYS - 1)) }), NOW),
).toBe('active')
expect(getConnectionUiState(conn({ last_synced_at: null }), NOW)).toBe('never_synced')
})
it('handles a row with no consent date', () => {
expect(getConnectionUiState(conn({ consent_expires: null }), NOW)).toBe('active')
})
})
describe('sortConnectionsByPrecedence', () => {
it('orders pending_selection, pending, error, expired, expiring, active', () => {
const rows = [
conn({ bank_name: 'Healthy' }),
conn({ bank_name: 'Expiring', consent_expires: iso(2) }),
conn({ bank_name: 'Expired', status: 'expired' }),
conn({ bank_name: 'Errored', status: 'error' }),
conn({ bank_name: 'InFlight', status: 'pending' }),
conn({ bank_name: 'PickAccounts', status: 'pending_selection' }),
]
expect(sortConnectionsByPrecedence(rows, NOW).map((r) => r.bank_name)).toEqual([
'PickAccounts',
'InFlight',
'Errored',
'Expired',
'Expiring',
'Healthy',
])
})
it('breaks ties by newest created_at first and does not mutate the input', () => {
const older = conn({ bank_name: 'Older', created_at: iso(-100) })
const newer = conn({ bank_name: 'Newer', created_at: iso(-1) })
const rows = [older, newer]
const sorted = sortConnectionsByPrecedence(rows, NOW)
expect(sorted.map((r) => r.bank_name)).toEqual(['Newer', 'Older'])
expect(rows[0]).toBe(older)
})
})
describe('selectPageAttention', () => {
it('returns null when every connection is healthy or has no attention state', () => {
expect(selectPageAttention([conn({})], NOW)).toBeNull()
expect(selectPageAttention([conn({ status: 'pending_selection' })], NOW)).toBeNull()
expect(selectPageAttention([], NOW)).toBeNull()
})
it('picks the worst state: error beats expired beats expiring beats stale', () => {
const errored = conn({ bank_name: 'Errored', status: 'error' })
const expired = conn({ bank_name: 'Expired', status: 'expired' })
const expiring = conn({ bank_name: 'Expiring', consent_expires: iso(2) })
const stale = conn({ bank_name: 'Stale', last_synced_at: iso(-10) })
expect(selectPageAttention([stale, expiring, expired, errored], NOW)?.connection.bank_name).toBe('Errored')
expect(selectPageAttention([stale, expiring, expired], NOW)?.connection.bank_name).toBe('Expired')
expect(selectPageAttention([stale, expiring], NOW)?.connection.bank_name).toBe('Expiring')
expect(selectPageAttention([stale], NOW)?.state).toBe('stale')
})
})
describe('buildPageAttentionSentence', () => {
it('names the bank and the state', () => {
const attention = selectPageAttention([conn({ status: 'expired' })], NOW)!
expect(buildPageAttentionSentence(attention, NOW)).toBe(
'SEB: PSD2-samtycket har löpt ut. Förnya samtycket för att återuppta synkroniseringen.',
)
})
it('counts days for the expiring state with singular/plural', () => {
const one = selectPageAttention([conn({ consent_expires: iso(1) })], NOW)!
expect(buildPageAttentionSentence(one, NOW)).toContain('går ut om 1 dag.')
const five = selectPageAttention([conn({ consent_expires: iso(5) })], NOW)!
expect(buildPageAttentionSentence(five, NOW)).toContain('går ut om 5 dagar.')
})
it('counts days since sync for the stale state', () => {
const attention = selectPageAttention([conn({ last_synced_at: iso(-10) })], NOW)!
expect(buildPageAttentionSentence(attention, NOW)).toContain('ingen synkning på 10 dagar')
})
})