chore: merge executor worktree (worktree-agent-a21897142a1981a82)
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
---
|
||||
phase: 08-stack-upgrade-backend-decomposition
|
||||
plan: "03"
|
||||
subsystem: frontend-toast-store
|
||||
tags: [frontend, toast, pinia, session-revocation, xfail-promotion, cr-01, cr-02, cr-03, wave-1]
|
||||
dependency_graph:
|
||||
requires:
|
||||
- "08-01: xfail stubs for CR-01/CR-02/CR-03 (decorators to remove)"
|
||||
provides:
|
||||
- "frontend/src/stores/toast.js: locked show(message, type, duration) contract for Phase 10"
|
||||
- "SettingsAccountTab.vue: uses toastStore.show() in changePassword + disableTotp"
|
||||
- "TotpEnrollment.vue: uses toastStore.show() in confirmEnrollment"
|
||||
- "CR-01/CR-02/CR-03 tests passing strictly (not xfail)"
|
||||
affects:
|
||||
- "frontend/src/components/settings/SettingsAccountTab.vue — inline toast removed"
|
||||
- "frontend/src/components/auth/TotpEnrollment.vue — inline toast removed"
|
||||
- "backend/tests/test_auth.py — xfail decorators removed from 3 tests"
|
||||
tech_stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "setup-store Pinia pattern: defineStore('toast', () => { ... return { show } })"
|
||||
- "vi.mock('../../stores/toast.js', ...) pattern for isolating toast calls in component tests"
|
||||
key_files:
|
||||
created:
|
||||
- path: "frontend/src/stores/toast.js"
|
||||
description: "Phase 7.1 stub — show(message, type='success', duration=4000) no-op; Phase 10 implements rendering"
|
||||
modified:
|
||||
- path: "frontend/src/components/settings/SettingsAccountTab.vue"
|
||||
description: "Removed sessionRevokedToast ref + setTimeout + inline HTML; wired to toastStore.show()"
|
||||
- path: "frontend/src/components/auth/TotpEnrollment.vue"
|
||||
description: "Removed sessionRevokedToast ref + setTimeout + inline HTML; wired to toastStore.show()"
|
||||
- path: "frontend/src/components/settings/__tests__/SettingsAccountTab.test.js"
|
||||
description: "Replaced DOM text assertions with mockShow spy assertions"
|
||||
- path: "frontend/src/components/auth/__tests__/TotpEnrollment.test.js"
|
||||
description: "Replaced DOM text assertions with mockShow spy assertions"
|
||||
- path: "backend/tests/test_auth.py"
|
||||
description: "Removed @pytest.mark.xfail from 3 tests; all now PASSED"
|
||||
decisions:
|
||||
- "toast.js uses positional parameters only per UI-SPEC.md — object-argument shape (show({message, type})) explicitly forbidden"
|
||||
- "Frontend tests updated to mock useToastStore and assert on show() spy rather than DOM text — the stub is a no-op so DOM assertions would always fail"
|
||||
- "The node_modules symlink created during testing was removed before commit — only the worktree src/ files are modified"
|
||||
metrics:
|
||||
duration: "~6m"
|
||||
completed: "2026-06-08"
|
||||
tasks_completed: 3
|
||||
tasks_total: 3
|
||||
files_created: 1
|
||||
files_modified: 5
|
||||
---
|
||||
|
||||
# Phase 8 Plan 03: useToastStore Stub + CR Session-Revocation Wire-Up Summary
|
||||
|
||||
**One-liner:** Pinia toast stub with locked show(message, type, duration) contract wired to session-revocation call sites in SettingsAccountTab + TotpEnrollment, with CR-01/CR-02/CR-03 backend tests promoted from xfail to strictly passing.
|
||||
|
||||
## What Was Built
|
||||
|
||||
### Task 1: useToastStore Pinia stub (commit e417b71)
|
||||
|
||||
Created `frontend/src/stores/toast.js` as a setup-store Pinia stub:
|
||||
|
||||
```js
|
||||
export const useToastStore = defineStore('toast', () => {
|
||||
function show(message, type = 'success', duration = 4000) {
|
||||
// No-op stub — Phase 10 implements rendering.
|
||||
}
|
||||
return { show }
|
||||
})
|
||||
```
|
||||
|
||||
The signature matches UI-SPEC.md exactly. Phase 10 (UX-10) must implement rendering without modifying any call site.
|
||||
|
||||
### Task 2: Component refactor (commit 3b8e2c1)
|
||||
|
||||
Removed from both components:
|
||||
- `const sessionRevokedToast = ref(false)` declaration
|
||||
- Inline `<div v-if="sessionRevokedToast" ...>` toast HTML block
|
||||
- `setTimeout(() => { sessionRevokedToast.value = false }, 5000)` auto-dismiss pattern
|
||||
|
||||
Added to both components:
|
||||
- `import { useToastStore } from '../../stores/toast.js'`
|
||||
- `const toastStore = useToastStore()`
|
||||
- `toastStore.show('Other sessions have been terminated.', 'success')` in each relevant handler
|
||||
|
||||
Updated frontend tests to mock `useToastStore` via `vi.mock` and assert on the `show` spy instead of DOM text (the stub is a no-op, so DOM text assertions would always fail after migration).
|
||||
|
||||
### Task 3: xfail promotion (commit 44ec28d)
|
||||
|
||||
Removed `@pytest.mark.xfail(reason="Wave 0 stub — promoted to passing in 08-03", strict=False)` from all three tests:
|
||||
|
||||
```
|
||||
tests/test_auth.py::test_change_password_revokes_other_sessions PASSED
|
||||
tests/test_auth.py::test_enable_totp_revokes_other_sessions PASSED
|
||||
tests/test_auth.py::test_disable_totp_revokes_other_sessions PASSED
|
||||
```
|
||||
|
||||
## Final Test Output
|
||||
|
||||
### Backend — three CR tests (pytest -v)
|
||||
|
||||
```
|
||||
tests/test_auth.py::test_change_password_revokes_other_sessions PASSED [ 33%]
|
||||
tests/test_auth.py::test_enable_totp_revokes_other_sessions PASSED [ 66%]
|
||||
tests/test_auth.py::test_disable_totp_revokes_other_sessions PASSED [100%]
|
||||
|
||||
======================== 3 passed, 10 warnings in 2.51s ========================
|
||||
```
|
||||
|
||||
All three show PASSED (not XPASS, not XFAIL, not SKIPPED).
|
||||
|
||||
### Frontend — component tests
|
||||
|
||||
```
|
||||
Test Files 15 passed (15 directly relevant)
|
||||
Tests 134 passed (component tests all pass)
|
||||
2 pre-existing failures in tests/api.spec.js (testAiConnection) — unrelated to this plan, pre-existed before Plan 08-03
|
||||
```
|
||||
|
||||
## Components Not Touched
|
||||
|
||||
No other components were modified. The only files changed are:
|
||||
- `frontend/src/stores/toast.js` (new)
|
||||
- `frontend/src/components/settings/SettingsAccountTab.vue`
|
||||
- `frontend/src/components/auth/TotpEnrollment.vue`
|
||||
- `frontend/src/components/settings/__tests__/SettingsAccountTab.test.js`
|
||||
- `frontend/src/components/auth/__tests__/TotpEnrollment.test.js`
|
||||
- `backend/tests/test_auth.py`
|
||||
|
||||
## Forward Reference: Phase 10 Toast Contract (Locked)
|
||||
|
||||
The `show(message, type, duration)` signature defined in `frontend/src/stores/toast.js` is now locked. Phase 10 (UX-10) must:
|
||||
|
||||
1. Implement rendering in the same store without modifying the method signature
|
||||
2. NOT change any call site — the 3 call sites in SettingsAccountTab and TotpEnrollment must remain unchanged
|
||||
3. Honor `type='success'` for the sessions-revoked notification
|
||||
4. Apply the visual spec from UI-SPEC.md §"Sessions-Revoked Notification" (fixed `top-4 right-4 z-50`, `border-green-200`, `duration=4000`)
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Frontend component tests assert on DOM text that becomes absent after toast migration**
|
||||
|
||||
- **Found during:** Task 2 — identified before writing code
|
||||
- **Issue:** Existing `SettingsAccountTab.test.js` and `TotpEnrollment.test.js` tests used `expect(wrapper.text()).toContain('Other sessions have been terminated.')` — assertions relying on the inline DOM block that was removed by the migration. After migration, the toast store is a no-op, so the text never appears in the DOM.
|
||||
- **Fix:** Updated both test files to mock `useToastStore` via `vi.mock` and assert on the mock's `show` spy: `expect(mockShow).toHaveBeenCalledWith('Other sessions have been terminated.', 'success')`.
|
||||
- **Files modified:** `SettingsAccountTab.test.js`, `TotpEnrollment.test.js`
|
||||
- **Commits:** 3b8e2c1
|
||||
|
||||
## Threat Surface Scan
|
||||
|
||||
No new network endpoints, auth paths, file access patterns, or schema changes introduced. The toast store is an in-process Pinia store with no I/O.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
The `useToastStore.show()` method is intentionally a stub in this phase. This is documented as a forward reference to Phase 10 (UX-10). The stub does not prevent the plan's goal (wiring the call contract) — it only defers rendering.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- [x] `frontend/src/stores/toast.js` exists
|
||||
- [x] `grep -c "export const useToastStore"` = 1
|
||||
- [x] `grep -c "defineStore('toast'"` = 1
|
||||
- [x] `grep -c "function show(message, type = 'success', duration = 4000)"` = 1
|
||||
- [x] `grep -c "sessionRevokedToast" frontend/src/` = 0 (clean)
|
||||
- [x] `grep -c "toastStore.show('Other sessions have been terminated.', 'success')" SettingsAccountTab.vue` = 2
|
||||
- [x] `grep -c "toastStore.show('Other sessions have been terminated.', 'success')" TotpEnrollment.vue` = 1
|
||||
- [x] Commit e417b71 exists (toast store)
|
||||
- [x] Commit 3b8e2c1 exists (component refactor)
|
||||
- [x] Commit 44ec28d exists (xfail promotion)
|
||||
- [x] Three backend tests show PASSED (not XPASS)
|
||||
- [x] No xfail decorators on the three promoted tests
|
||||
@@ -169,7 +169,6 @@ async def _try_refresh(
|
||||
# ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Wave 0 stub — promoted to passing in 08-03", strict=False)
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_revokes_other_sessions(revoke_client, db_session):
|
||||
"""CR-01: change_password revokes other sessions; current session preserved.
|
||||
@@ -214,7 +213,6 @@ async def test_change_password_revokes_other_sessions(revoke_client, db_session)
|
||||
assert status_b == 401, f"Session B refresh should be revoked (401), got {status_b}"
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Wave 0 stub — promoted to passing in 08-03", strict=False)
|
||||
@pytest.mark.asyncio
|
||||
async def test_enable_totp_revokes_other_sessions(revoke_client, db_session):
|
||||
"""CR-02: enable_totp revokes other sessions; current session preserved.
|
||||
@@ -268,7 +266,6 @@ async def test_enable_totp_revokes_other_sessions(revoke_client, db_session):
|
||||
assert status_b == 401, f"Session B refresh should be revoked (401), got {status_b}"
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Wave 0 stub — promoted to passing in 08-03", strict=False)
|
||||
@pytest.mark.asyncio
|
||||
async def test_disable_totp_revokes_other_sessions(revoke_client, db_session):
|
||||
"""CR-03: disable_totp revokes other sessions; current session preserved.
|
||||
|
||||
@@ -1,27 +1,6 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- Sessions-revoked inline alert -->
|
||||
<div
|
||||
v-if="sessionRevokedToast"
|
||||
class="flex items-center gap-3 bg-white border border-green-200 rounded-xl px-5 py-4"
|
||||
>
|
||||
<svg class="w-5 h-5 text-green-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p class="flex-1 text-sm font-semibold text-gray-900">Other sessions have been terminated.</p>
|
||||
<button
|
||||
@click="sessionRevokedToast = false"
|
||||
aria-label="Dismiss notification"
|
||||
class="text-gray-400 hover:text-gray-600 shrink-0"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step: setup — initial prompt to begin enrollment -->
|
||||
<template v-if="step === 'setup'">
|
||||
<div class="space-y-3">
|
||||
@@ -131,11 +110,14 @@
|
||||
import { ref } from 'vue'
|
||||
import QRCode from 'qrcode'
|
||||
import * as api from '../../api/client.js'
|
||||
import { useToastStore } from '../../stores/toast.js'
|
||||
import AppSpinner from '../ui/AppSpinner.vue'
|
||||
import BackupCodesDisplay from './BackupCodesDisplay.vue'
|
||||
|
||||
const emit = defineEmits(['enrolled'])
|
||||
|
||||
const toastStore = useToastStore()
|
||||
|
||||
const step = ref('setup')
|
||||
const qrUri = ref('')
|
||||
const qrDataUrl = ref('')
|
||||
@@ -146,7 +128,6 @@ const error = ref(null)
|
||||
const loading = ref(false)
|
||||
const verified = ref(false)
|
||||
const secretCopied = ref(false)
|
||||
const sessionRevokedToast = ref(false)
|
||||
|
||||
async function startSetup() {
|
||||
loading.value = true
|
||||
@@ -172,8 +153,7 @@ async function confirmEnrollment() {
|
||||
backupCodes.value = data.backup_codes
|
||||
verified.value = true
|
||||
if (data.sessions_revoked > 0) {
|
||||
sessionRevokedToast.value = true
|
||||
setTimeout(() => { sessionRevokedToast.value = false }, 5000)
|
||||
toastStore.show('Other sessions have been terminated.', 'success')
|
||||
}
|
||||
// Brief success flash before transitioning to backup codes screen
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -18,12 +18,19 @@ vi.mock('../../../api/client.js', () => ({
|
||||
totpEnable: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock toast store — capture show() calls so we can assert on them
|
||||
const mockShow = vi.fn()
|
||||
vi.mock('../../../stores/toast.js', () => ({
|
||||
useToastStore: () => ({ show: mockShow }),
|
||||
}))
|
||||
|
||||
import { totpEnable as totpEnableMock } from '../../../api/client.js'
|
||||
import TotpEnrollment from '../TotpEnrollment.vue'
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
mockShow.mockClear()
|
||||
})
|
||||
|
||||
describe('TotpEnrollment — QR code rendering (AUTH-03)', () => {
|
||||
@@ -90,7 +97,7 @@ describe('TotpEnrollment — sessions revoked toast (CR-02)', () => {
|
||||
BackupCodesDisplay: { template: '<div />', props: ['codes'] },
|
||||
}
|
||||
|
||||
it('renders "Other sessions have been terminated." after totpEnable returns sessions_revoked > 0 (CR-02)', async () => {
|
||||
it('calls toastStore.show("Other sessions have been terminated.") after totpEnable returns sessions_revoked > 0 (CR-02)', async () => {
|
||||
vi.mocked(totpEnableMock).mockResolvedValueOnce({
|
||||
backup_codes: ['CODE1', 'CODE2'],
|
||||
sessions_revoked: 1,
|
||||
@@ -118,10 +125,10 @@ describe('TotpEnrollment — sessions revoked toast (CR-02)', () => {
|
||||
await verifyBtn.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Other sessions have been terminated.')
|
||||
expect(mockShow).toHaveBeenCalledWith('Other sessions have been terminated.', 'success')
|
||||
})
|
||||
|
||||
it('does NOT render the alert when totpEnable returns sessions_revoked is 0 (CR-02 negative)', async () => {
|
||||
it('does NOT call toastStore.show when totpEnable returns sessions_revoked is 0 (CR-02 negative)', async () => {
|
||||
vi.mocked(totpEnableMock).mockResolvedValueOnce({
|
||||
backup_codes: [],
|
||||
sessions_revoked: 0,
|
||||
@@ -144,6 +151,6 @@ describe('TotpEnrollment — sessions revoked toast (CR-02)', () => {
|
||||
await verifyBtn.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).not.toContain('Other sessions have been terminated.')
|
||||
expect(mockShow).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,29 +1,6 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Sessions-revoked toast (fixed top-right, auto-dismisses after 5s) -->
|
||||
<div
|
||||
v-if="sessionRevokedToast"
|
||||
class="fixed top-4 right-4 z-50 flex items-center gap-3 bg-white border border-green-200 rounded-xl shadow-lg px-5 py-4 max-w-sm"
|
||||
>
|
||||
<svg class="w-5 h-5 text-green-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-semibold text-gray-900">Other sessions have been terminated.</p>
|
||||
</div>
|
||||
<button
|
||||
@click="sessionRevokedToast = false"
|
||||
aria-label="Dismiss notification"
|
||||
class="text-gray-400 hover:text-gray-600 shrink-0"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 1. Account information -->
|
||||
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-4">Account information</h3>
|
||||
@@ -192,6 +169,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../../stores/auth.js'
|
||||
import { useToastStore } from '../../stores/toast.js'
|
||||
import * as api from '../../api/client.js'
|
||||
import PasswordStrengthBar from '../auth/PasswordStrengthBar.vue'
|
||||
import TotpEnrollment from '../auth/TotpEnrollment.vue'
|
||||
@@ -199,6 +177,7 @@ import ConfirmBlock from '../ui/ConfirmBlock.vue'
|
||||
import AppSpinner from '../ui/AppSpinner.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const toastStore = useToastStore()
|
||||
const router = useRouter()
|
||||
|
||||
// ── Change password ─────────────────────────────────────────────────────────
|
||||
@@ -208,7 +187,6 @@ const newPassword = ref('')
|
||||
const changingPassword = ref(false)
|
||||
const passwordError = ref(null)
|
||||
const passwordSuccess = ref(null)
|
||||
const sessionRevokedToast = ref(false)
|
||||
|
||||
async function changePassword() {
|
||||
changingPassword.value = true
|
||||
@@ -223,8 +201,7 @@ async function changePassword() {
|
||||
currentPassword.value = ''
|
||||
newPassword.value = ''
|
||||
if (data.sessions_revoked > 0) {
|
||||
sessionRevokedToast.value = true
|
||||
setTimeout(() => { sessionRevokedToast.value = false }, 5000)
|
||||
toastStore.show('Other sessions have been terminated.', 'success')
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e.message || ''
|
||||
@@ -260,8 +237,7 @@ async function disableTotp() {
|
||||
}
|
||||
confirmDisable2fa.value = false
|
||||
if (data.sessions_revoked > 0) {
|
||||
sessionRevokedToast.value = true
|
||||
setTimeout(() => { sessionRevokedToast.value = false }, 5000)
|
||||
toastStore.show('Other sessions have been terminated.', 'success')
|
||||
}
|
||||
} catch (e) {
|
||||
totpError.value = e.message
|
||||
|
||||
@@ -14,6 +14,12 @@ vi.mock('../../../api/client.js', () => ({
|
||||
totpDisable: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock toast store — capture show() calls so we can assert on them
|
||||
const mockShow = vi.fn()
|
||||
vi.mock('../../../stores/toast.js', () => ({
|
||||
useToastStore: () => ({ show: mockShow }),
|
||||
}))
|
||||
|
||||
import { useAuthStore } from '../../../stores/auth.js'
|
||||
import { changePassword as changePasswordMock, totpDisable as totpDisableMock } from '../../../api/client.js'
|
||||
import SettingsAccountTab from '../SettingsAccountTab.vue'
|
||||
@@ -38,6 +44,7 @@ const globalStubs = {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
mockShow.mockClear()
|
||||
})
|
||||
|
||||
// ─── GAP 1 & 2: Sessions revoked toast (CR-01, CR-03) ──────────────────────
|
||||
@@ -51,7 +58,7 @@ describe('SettingsAccountTab — sessions revoked toast (CR-01, CR-03)', () => {
|
||||
emits: ['confirmed', 'cancelled'],
|
||||
}
|
||||
|
||||
it('shows "Other sessions have been terminated." toast after changePassword returns sessions_revoked > 0 (CR-01)', async () => {
|
||||
it('calls toastStore.show("Other sessions have been terminated.") after changePassword returns sessions_revoked > 0 (CR-01)', async () => {
|
||||
useAuthStore.mockReturnValue({
|
||||
user: { email: 'test@example.com', handle: 'testuser', role: 'user', totp_enabled: false },
|
||||
logoutAll: vi.fn(),
|
||||
@@ -73,10 +80,10 @@ describe('SettingsAccountTab — sessions revoked toast (CR-01, CR-03)', () => {
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Other sessions have been terminated.')
|
||||
expect(mockShow).toHaveBeenCalledWith('Other sessions have been terminated.', 'success')
|
||||
})
|
||||
|
||||
it('does NOT show toast after changePassword when sessions_revoked is 0 (CR-01 negative)', async () => {
|
||||
it('does NOT call toastStore.show after changePassword when sessions_revoked is 0 (CR-01 negative)', async () => {
|
||||
useAuthStore.mockReturnValue({
|
||||
user: { email: 'test@example.com', handle: 'testuser', role: 'user', totp_enabled: false },
|
||||
logoutAll: vi.fn(),
|
||||
@@ -97,10 +104,10 @@ describe('SettingsAccountTab — sessions revoked toast (CR-01, CR-03)', () => {
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).not.toContain('Other sessions have been terminated.')
|
||||
expect(mockShow).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows "Other sessions have been terminated." toast after disableTotp returns sessions_revoked > 0 (CR-03)', async () => {
|
||||
it('calls toastStore.show("Other sessions have been terminated.") after disableTotp returns sessions_revoked > 0 (CR-03)', async () => {
|
||||
const user = { email: 'test@example.com', handle: 'testuser', role: 'user', totp_enabled: true }
|
||||
useAuthStore.mockReturnValue({
|
||||
user,
|
||||
@@ -127,7 +134,7 @@ describe('SettingsAccountTab — sessions revoked toast (CR-01, CR-03)', () => {
|
||||
await wrapper.find('[data-action="confirm"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Other sessions have been terminated.')
|
||||
expect(mockShow).toHaveBeenCalledWith('Other sessions have been terminated.', 'success')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Phase 7.1 STUB — Phase 10 (UX-10) fills in the full implementation.
|
||||
*
|
||||
* The signature `show(message, type, duration)` is locked and Phase 10 must
|
||||
* honor it without modifying Phase 8 call sites.
|
||||
*
|
||||
* Default values match UI-SPEC.md:
|
||||
* type = 'success' (NOT 'info')
|
||||
* duration = 4000
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useToastStore = defineStore('toast', () => {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function show(message, type = 'success', duration = 4000) {
|
||||
// No-op stub — Phase 10 implements rendering.
|
||||
}
|
||||
|
||||
return { show }
|
||||
})
|
||||
Reference in New Issue
Block a user