test(13-02): add red store, health-flow, reconnect, and no-probe tests

- SettingsCloudTab.health.test.js: Test/Reconnect/Disconnect per-connection actions,
  explicit confirmation before credential removal, Google Drive broader scope consent
  copy (D-12/D-13/D-15/D-16/D-17/T-13-09)
- cloudConnections.test.js: connectionHealth state map, setConnectionHealth translation,
  degraded vs requires_reauth distinction, pendingHealthRetest after failure, reconnect
  preserves cached items, auto-test after connect, no-probe-on-navigation (D-12..D-15)
- CloudFolderRenderedFlow.test.js: warning+reconnect banner alongside cached items,
  requires_reauth renders actionable prompt, folder-navigate never triggers health probe
  (D-12/D-13/D-14/CONN-02/T-13-06)
This commit is contained in:
curo1305
2026-06-22 18:13:39 +02:00
parent 514925bd4c
commit 8923ed5b3c
3 changed files with 813 additions and 0 deletions
@@ -0,0 +1,477 @@
/**
* Phase 13 Plan 02 Task 2 — SettingsCloudTab health/reconnect/disconnect tests.
*
* RED tests: these define the only acceptable connection health, reconnect, disconnect,
* and Google Drive consent copy behavior for Phase 13. They MUST FAIL against the
* current SettingsCloudTab implementation.
*
* Coverage:
* D-12: Full diagnostics plus explicit Test and Reconnect controls in Settings.
* D-13: Automatic health retest after connect/reconnect; explicit Test action allowed.
* D-15: Transient outage warns without deleting credentials or cached metadata.
* D-16: Explicit Disconnect requires confirmation and removes credentials/metadata.
* D-17: Google Drive consent copy must explicitly mention broader storage access.
* T-13-09: Reconnect/consent UX requires explicit broader Google scope explanation.
*
* Security constraints:
* - credentials_enc must never appear in test fixtures or expected HTML.
* - Disconnect confirmation must be required before credential removal.
* - Reconnect must not create a new connection row (must update the existing row).
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
// ── Store mock ────────────────────────────────────────────────────────────────
const mockTestConnection = vi.fn()
const mockReconnect = vi.fn()
const mockDisconnect = vi.fn()
const mockFetchConnections = vi.fn()
vi.mock('../../../stores/cloudConnections.js', () => ({
useCloudConnectionsStore: () => ({
connections: [],
loading: false,
error: null,
fetchConnections: mockFetchConnections,
disconnect: mockDisconnect,
disconnectAll: vi.fn(),
rename: vi.fn(),
testConnection: mockTestConnection,
reconnect: mockReconnect,
defaultDisplayName: (c) => c.display_name ?? c.provider,
}),
}))
vi.mock('../../../api/client.js', () => ({
connectWebDav: vi.fn(),
updateWebDavCredentials: vi.fn(),
listCloudConnections: vi.fn(),
disconnectCloud: vi.fn(),
renameCloudConnection: vi.fn(),
initiateOAuth: vi.fn(),
testCloudConnection: vi.fn(),
reconnectCloudConnection: vi.fn(),
}))
import SettingsCloudTab from '../SettingsCloudTab.vue'
// ── Connection health fixtures ────────────────────────────────────────────────
const ACTIVE_GDRIVE = {
id: 'conn-gdrive-1',
provider: 'google_drive',
display_name: 'Work Drive',
status: 'ACTIVE',
health: { state: 'healthy', checked_at: '2026-06-22T10:00:00Z', error_code: null },
connected_at: '2026-06-01T00:00:00Z',
}
const REQUIRES_REAUTH_GDRIVE = {
id: 'conn-gdrive-reauth',
provider: 'google_drive',
display_name: 'Stale Drive',
status: 'REQUIRES_REAUTH',
health: {
state: 'requires_reauth',
checked_at: '2026-06-22T09:00:00Z',
error_code: 'token_expired',
error_message: 'Access token has expired. Please reconnect.',
},
connected_at: '2026-05-01T00:00:00Z',
}
const TRANSIENT_OUTAGE_ONEDRIVE = {
id: 'conn-onedrive-1',
provider: 'onedrive',
display_name: 'Personal OneDrive',
status: 'DEGRADED',
health: {
state: 'degraded',
checked_at: '2026-06-22T11:00:00Z',
error_code: 'provider_timeout',
error_message: 'OneDrive temporarily unreachable.',
},
connected_at: '2026-04-01T00:00:00Z',
}
function makeGlobal(connections) {
return {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
provide: { connections },
}
}
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
// ── D-12: Full diagnostics in Settings ────────────────────────────────────────
describe('SettingsCloudTab_shows_full_health_diagnostics', () => {
it('renders a Test connection button for active connections', () => {
/**
* D-12: Full diagnostics plus explicit Test action in Settings.
* An active connection must expose a "Test connection" button.
*
* RED: current SettingsCloudTab has no Test button per-connection.
*/
vi.doMock('../../../stores/cloudConnections.js', () => ({
useCloudConnectionsStore: () => ({
connections: [ACTIVE_GDRIVE],
loading: false,
error: null,
fetchConnections: mockFetchConnections,
disconnect: mockDisconnect,
disconnectAll: vi.fn(),
rename: vi.fn(),
testConnection: mockTestConnection,
reconnect: mockReconnect,
defaultDisplayName: (c) => c.display_name ?? c.provider,
}),
}))
// Mount with ACTIVE_GDRIVE connection inline — the store mock returns empty
// (module mock applies globally), so we verify the structural requirement:
// the component must declare testConnection interaction.
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
// At minimum, a "Test" button pattern must be declared — fails until implemented
const html = wrapper.html()
// Either a visible Test button or a data-test="test-connection" element must exist
const hasTestUi = html.includes('Test') || wrapper.find('[data-test="test-connection"]').exists()
expect(hasTestUi).toBe(true)
})
it('renders a Reconnect button for connections in REQUIRES_REAUTH status', () => {
/**
* D-12: Explicit Reconnect control must appear when a connection requires
* re-authentication. The button must not appear for healthy connections.
*
* RED: current SettingsCloudTab has no Reconnect button per-connection.
*/
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
// Must have reconnect affordance somewhere (button or link)
const html = wrapper.html()
const hasReconnectUi = html.includes('Reconnect')
|| wrapper.find('[data-test="reconnect-connection"]').exists()
expect(hasReconnectUi).toBe(true)
})
it('shows error message for REQUIRES_REAUTH connection — not a generic status', () => {
/**
* D-12: Actionable error details must be visible for expired/revoked/invalid
* credentials. A generic "Error" badge is insufficient.
*
* RED: current SettingsCloudTab shows no per-connection error detail.
*/
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
// Health error state must be represented in the UI beyond just a badge
// The component must have a [data-test="connection-health-detail"] or similar
const hasHealthDetail = wrapper.find('[data-test="connection-health"]').exists()
|| wrapper.find('[data-test="health-error"]').exists()
|| wrapper.find('[data-test="connection-health-detail"]').exists()
expect(hasHealthDetail).toBe(true)
})
})
// ── D-13: Explicit Test action in Settings ────────────────────────────────────
describe('SettingsCloudTab_test_connection_action', () => {
it('clicking Test connection calls testConnection with the connection id', async () => {
/**
* D-13: Allow an explicit Test action in Settings.
* Clicking the Test button must call the store's testConnection method
* with the specific connection's ID.
*
* RED: no testConnection method or call exists in the current component.
*/
mockTestConnection.mockResolvedValue({ state: 'healthy' })
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
const testBtn = wrapper.find('[data-test="test-connection"]')
if (testBtn.exists()) {
await testBtn.trigger('click')
await flushPromises()
expect(mockTestConnection).toHaveBeenCalled()
} else {
// RED: button does not exist yet
expect(wrapper.find('[data-test="test-connection"]').exists()).toBe(true)
}
})
})
// ── D-15: Transient outage — preserve credentials, show warning ───────────────
describe('SettingsCloudTab_transient_outage_warning', () => {
it('DEGRADED connection shows an actionable warning without a Disconnect prompt', () => {
/**
* D-15: A timeout or temporarily unreachable provider is a warning state only.
* The Settings UI must show an actionable warning (retry/reconnect) and must
* NOT immediately prompt for disconnect or credential deletion.
*
* RED: current component does not distinguish degraded from reauth state.
*/
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
// A connection health warning indicator must exist
const hasWarning = wrapper.find('[data-test="health-warning"]').exists()
|| wrapper.find('[data-test="connection-warning"]').exists()
expect(hasWarning).toBe(true)
})
it('DEGRADED connection does not auto-trigger disconnect confirmation', () => {
/**
* D-15: A transient failure must never trigger automatic disconnect.
* The disconnect confirm dialog must NOT appear automatically.
*
* RED: no DEGRADED state distinction in current component.
*/
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
// Disconnect confirm must NOT appear on mount (only on explicit user action)
expect(wrapper.find('[data-test="disconnect-confirm-dialog"]').exists()).toBe(false)
})
})
// ── D-16: Explicit Disconnect requires confirmation ───────────────────────────
describe('SettingsCloudTab_disconnect_requires_confirmation', () => {
it('clicking Disconnect opens a confirmation dialog before removing credentials', async () => {
/**
* D-16: Explicit user-initiated Disconnect requires confirmation.
* Credentials and cloud metadata must not be removed without user confirmation.
*
* RED: no explicit disconnect confirmation flow exists in the current component.
*/
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
// Find any disconnect button
const disconnectBtn = wrapper.find('[data-test="disconnect-connection"]')
?? wrapper.findAll('button').find(b => b.text().toLowerCase().includes('disconnect'))
if (disconnectBtn && disconnectBtn.exists()) {
await disconnectBtn.trigger('click')
await flushPromises()
// Store disconnect must NOT be called immediately — confirmation required
expect(mockDisconnect).not.toHaveBeenCalled()
} else {
// RED: No Disconnect button exists yet
const hasDisconnect = wrapper.find('[data-test="disconnect-connection"]').exists()
expect(hasDisconnect).toBe(true)
}
})
it('Disconnect confirmation clearly states provider files are untouched', () => {
/**
* D-16: Disconnect removes credentials and DocuVault cloud metadata,
* but leaves all provider files untouched. The confirmation copy must say so.
*
* RED: no disconnect confirmation dialog with this copy exists.
*/
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
// The confirmation text must describe what is removed vs preserved
// Since dialog is not open by default, check that when a confirm element appears
// it has the correct structural intent. For now, the data-test must exist.
const confirmEl = wrapper.find('[data-test="disconnect-confirm-dialog"]')
|| wrapper.find('[data-test="confirm-disconnect-copy"]')
// RED: confirm element does not exist until implemented
expect(
wrapper.find('[data-test="disconnect-confirm-dialog"]').exists()
|| wrapper.find('[data-test="confirm-disconnect-copy"]').exists()
).toBe(true)
})
})
// ── D-17 / T-13-09: Google Drive broader scope consent copy ──────────────────
describe('SettingsCloudTab_google_drive_consent_copy', () => {
it('Google Drive connect/reconnect copy mentions broader storage access scope', () => {
/**
* D-17 and T-13-09: Phase 13 requests broader Google Drive access beyond
* drive.file scope. The consent copy shown to users must explicitly mention
* that the app will access files throughout their Google Drive, not just
* files created by the app.
*
* RED: current SettingsCloudTab shows generic "Google Drive" connect text
* without any scope explanation.
*/
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
const html = wrapper.html()
// Must contain scope information in some form:
// - "all files", "full Drive access", "entire Drive", "all your Google Drive files"
// - or a data-test="gdrive-scope-notice" element
const hasScopeNote = html.includes('all files')
|| html.includes('full Drive')
|| html.includes('entire Drive')
|| html.includes('all your Google Drive')
|| html.includes('broader')
|| wrapper.find('[data-test="gdrive-scope-notice"]').exists()
// RED: no scope notice exists in the current component
expect(hasScopeNote).toBe(true)
})
it('Google Drive reconnect dialog also includes broader scope explanation', () => {
/**
* T-13-09: The expanded Phase 13 scope must not ship silently.
* Even the reconnect dialog must explain the broader access being requested.
*
* RED: no reconnect dialog with scope explanation exists.
*/
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
// A data-test attribute covering the gdrive scope disclosure must exist
const hasScopeEl = wrapper.find('[data-test="gdrive-scope-notice"]').exists()
|| wrapper.find('[data-test="gdrive-consent-copy"]').exists()
// RED: no scope disclosure element exists yet
expect(hasScopeEl).toBe(true)
})
})
// ── D-12: Compact health status beside each connection ────────────────────────
describe('SettingsCloudTab_health_state_per_connection', () => {
it('each connection row shows a health status indicator', () => {
/**
* D-12: Connection health must appear beside each connection listing.
* Each row must have a health status element.
*
* RED: current component shows only "Connected" text, no per-connection health.
*/
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
// Structural check: health status elements must be declared
// Even with empty connections, the component must have health state structure
const hasHealthStructure = wrapper.find('[data-test="connection-health"]').exists()
|| wrapper.find('[data-test="health-status"]').exists()
// RED: no health status structure exists in the current component
expect(hasHealthStructure).toBe(true)
})
it('credentials_enc does not appear in any rendered output', () => {
/**
* Security invariant: the encrypted credentials must never appear in the
* component's rendered HTML output.
*
* This passes in the current component (it doesn't render credentials),
* but must remain passing throughout Phase 13 changes.
*/
const wrapper = mount(SettingsCloudTab, {
global: {
plugins: [createPinia()],
stubs: {
CloudCredentialModal: { template: '<div />', props: ['show', 'provider', 'existing'] },
AppIcon: true,
ConfirmBlock: true,
},
},
})
expect(wrapper.html()).not.toContain('credentials_enc')
expect(wrapper.html()).not.toContain('access_token')
expect(wrapper.html()).not.toContain('refresh_token')
})
})
@@ -236,3 +236,188 @@ describe('setBrowseState freshness mapping', () => {
expect(store.browseItems[0].id).toBe('item-a')
})
})
// ── Phase 13 Plan 02: health-state, auto-test, and no-probe-on-navigation ──────
// These tests MUST FAIL until Phase 13 store extensions are implemented.
describe('health_state_translation_D12', () => {
it('store exposes connectionHealth state for settings and browser display', () => {
/**
* D-12: Connection health appears in both Settings and the cloud browser.
* The store must provide a centralized health state so both surfaces
* consume the same server-translated values without duplicating translation logic.
*
* RED: current store has no connectionHealth or healthState field.
*/
const store = useCloudConnectionsStore()
// Store must expose a health state map or structured health field
// Either a computed connectionHealth getter or a healthState ref
expect(store.connectionHealth !== undefined || store.healthState !== undefined).toBe(true)
})
it('store.setConnectionHealth translates server requires_reauth to a UI actionable state', () => {
/**
* D-12 and CONN-02: The store must translate server health states once
* so both the browser compact status and Settings diagnostics use the same
* actionable vocabulary. `requires_reauth` must map to a UI-actionable state
* (not a raw HTTP status or API error string).
*
* RED: no setConnectionHealth or health translation exists in the current store.
*/
const store = useCloudConnectionsStore()
if (typeof store.setConnectionHealth === 'function') {
store.setConnectionHealth('conn-1', { state: 'requires_reauth', error_code: 'token_expired' })
const health = store.connectionHealth?.['conn-1'] ?? store.getConnectionHealth?.('conn-1')
expect(health).toBeDefined()
expect(health.state).toBe('requires_reauth')
} else {
// RED: method does not exist yet
expect(typeof store.setConnectionHealth).toBe('function')
}
})
it('store.setConnectionHealth translates server degraded to a transient warning state', () => {
/**
* D-15: Transient outage must be distinguished from requires_reauth.
* The store must hold separate states for these two failure modes so
* the UI can show different affordances (retry vs reconnect).
*
* RED: no health state distinction exists.
*/
const store = useCloudConnectionsStore()
if (typeof store.setConnectionHealth === 'function') {
store.setConnectionHealth('conn-1', { state: 'degraded', error_code: 'provider_timeout' })
const health = store.connectionHealth?.['conn-1'] ?? store.getConnectionHealth?.('conn-1')
expect(health?.state).toBe('degraded')
// degraded must not equal requires_reauth
expect(health?.state).not.toBe('requires_reauth')
} else {
expect(typeof store.setConnectionHealth).toBe('function')
}
})
})
describe('no_probe_on_navigation_D13', () => {
it('ordinary folder navigation does NOT call testCloudConnection', async () => {
/**
* D-13: "Do not probe the provider on every folder navigation."
* When getCloudFoldersByConnectionId is called for browse, the store must
* NOT trigger a health probe (testCloudConnection) as a side effect.
*
* RED: no probe-suppression guard exists; getCloudFoldersByConnectionId
* could trigger a health probe as a side effect in a naive implementation.
*/
api.getCloudFoldersByConnectionId.mockResolvedValue({
items: [],
capabilities: null,
freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-22T00:00:00Z' },
})
const store = useCloudConnectionsStore()
store.connections = [{ id: 'conn-browse', provider: 'onedrive', display_name: 'My OneDrive' }]
// Simulate browse (what CloudFolderView calls on navigation)
if (typeof store.browse === 'function') {
await store.browse('conn-browse', '')
} else {
// Direct API call (current approach)
await api.getCloudFoldersByConnectionId('conn-browse', '')
}
// testCloudConnection must NOT have been called as a navigation side effect
expect(api.testCloudConnection ?? vi.fn()).not.toHaveBeenCalled()
})
it('health probe DOES trigger after credential-related failure (requires_reauth)', async () => {
/**
* D-13: Health retest is automatically triggered after credential-related
* failures. This SHOULD happen — it is the complement of the no-probe rule
* (probe only after failures, not on normal navigation).
*
* RED: no auto-probe-after-failure mechanism exists in the store.
*/
const store = useCloudConnectionsStore()
if (typeof store.handleHealthFailure === 'function') {
// After a reauth failure, the store should schedule/trigger a health retest
store.handleHealthFailure('conn-1', { state: 'requires_reauth', error_code: 'token_expired' })
// Must set a pending retest flag or have called testConnection
const needsRetest = store.pendingHealthRetest?.has?.('conn-1')
?? store.healthRetestPending?.['conn-1']
expect(needsRetest).toBe(true)
} else {
// RED: method does not exist yet
expect(typeof store.handleHealthFailure).toBe('function')
}
})
})
describe('reconnect_refreshes_current_folder_D14', () => {
it('store records that reconnect should refresh the current folder', () => {
/**
* D-14: A successful reconnect must immediately refresh the current folder.
* The store must record a pending folder refresh flag so CloudFolderView
* can detect and execute the refresh after reconnect.
*
* RED: no reconnect-triggered refresh flag exists in the current store.
*/
const store = useCloudConnectionsStore()
if (typeof store.markReconnectRefreshPending === 'function') {
store.markReconnectRefreshPending('conn-1')
expect(store.reconnectRefreshPending).toBe(true)
} else {
// RED: no such method
expect(typeof store.markReconnectRefreshPending).toBe('function')
}
})
it('reconnect preserves cached metadata as stale (does not clear browseItems)', () => {
/**
* D-14: A successful reconnect keeps cached metadata visible as stale.
* The store must NOT clear browseItems during reconnect — it must set
* freshness to 'stale' or 'reconnecting' without wiping the item list.
*
* RED: current selectConnection clears all browse state; reconnect must differ.
*/
const store = useCloudConnectionsStore()
const cachedItems = [
{ id: 'item-1', provider_item_id: 'pref-1', name: 'Old Folder', kind: 'folder' },
]
store.setBrowseState({ items: cachedItems, freshness: 'fresh' })
if (typeof store.markReconnecting === 'function') {
store.markReconnecting('conn-1')
// Items must remain visible during reconnect (stale but visible)
expect(store.browseItems).toHaveLength(1)
// Freshness changes to stale or reconnecting — but items are preserved
expect(['stale', 'reconnecting', 'warning'].includes(store.folderFreshness)).toBe(true)
} else {
// RED: no markReconnecting method
expect(typeof store.markReconnecting).toBe('function')
}
})
})
describe('auto_test_after_connect_D13', () => {
it('store exposes a pendingHealthTest flag or similar after a connect action', async () => {
/**
* D-13: Automatically test health after connect/reconnect.
* The store must have a mechanism to schedule or trigger a health test
* immediately after a new connection is established.
*
* RED: no auto-test-after-connect mechanism exists in the store.
*/
const store = useCloudConnectionsStore()
// After fetchConnections, newly added connections should trigger health check
api.listCloudConnections.mockResolvedValue({
items: [{ id: 'conn-new', provider: 'nextcloud', status: 'ACTIVE', health: null }],
})
await store.fetchConnections()
// The store should mark connections with null health for testing
const needsTest = store.connectionsNeedingHealthCheck
?? store.connections.filter(c => c.health === null || c.health?.state === undefined)
expect(needsTest).toBeDefined()
// At least one connection should be marked for health testing
if (Array.isArray(needsTest)) {
expect(needsTest.length).toBeGreaterThan(0)
}
})
})
@@ -412,3 +412,154 @@ describe('filenames_render_as_text_not_html', () => {
expect(html).not.toContain('<img src=x onerror=alert(1)>')
})
})
// ── Phase 13 Plan 02: health warning and reconnect state rendered-flow tests ──
// These tests MUST FAIL until Phase 13 health/reconnect UI is implemented.
describe('warning_state_shows_health_banner_while_keeping_items_D14', () => {
it('warning freshness shows a health/reconnect banner alongside cached items', async () => {
/**
* D-12 / D-14: On a warning freshness state, the browser must show BOTH:
* 1. The cached items (stale metadata visible while DocuVault revalidates)
* 2. A health/reconnect banner with compact status and Reconnect action
*
* RED: current StorageBrowser shows a freshness warning text but no actionable
* reconnect affordance within the cloud browser.
*/
api.getCloudFoldersByConnectionId.mockResolvedValue({
items: [ROOT_FOLDER_A, ROOT_FILE_A],
capabilities: null,
freshness: WARNING_FRESHNESS,
})
const wrapper = mount(CloudFolderView, {
global: { stubs: leafStubs },
})
await flushPromises()
const html = wrapper.html()
// Items must still be rendered (stale metadata visible)
expect(html).toContain('Alpha')
expect(html).toContain('report.pdf')
// A health/reconnect UI element must be present
const hasHealthBanner = wrapper.find('[data-test="cloud-health-banner"]').exists()
|| wrapper.find('[data-test="connection-warning"]').exists()
|| wrapper.find('[data-test="reconnect-action"]').exists()
// RED: no health banner with reconnect affordance in the current browser
expect(hasHealthBanner).toBe(true)
})
it('warning state renders a Reconnect button inside the browser, not just a badge', async () => {
/**
* D-12: Compact status plus Reconnect must appear in the cloud browser —
* not just in Settings. The browser must surface actionable reconnect.
*
* RED: current browser shows stale warning text but no Reconnect button.
*/
api.getCloudFoldersByConnectionId.mockResolvedValue({
items: [ROOT_FOLDER_A],
capabilities: null,
freshness: WARNING_FRESHNESS,
})
const wrapper = mount(CloudFolderView, {
global: { stubs: leafStubs },
})
await flushPromises()
// A reconnect action must be accessible within the browser component
const reconnectEl = wrapper.find('[data-test="reconnect-action"]')
|| wrapper.find('button[aria-label="Reconnect"]')
// RED: no reconnect button in browser view
expect(
wrapper.find('[data-test="reconnect-action"]').exists()
|| !!wrapper.findAll('button').find(b => b.text().toLowerCase().includes('reconnect'))
).toBe(true)
})
})
describe('requires_reauth_state_shows_reauthentication_prompt_D14', () => {
it('requires_reauth freshness state shows a reauthentication prompt', async () => {
/**
* D-12 / CONN-02: When a connection requires re-authentication, the browser
* must show a clear reauthentication prompt (not just a generic error).
*
* RED: current rendered flow has no requires_reauth specific UI.
*/
const REAUTH_FRESHNESS = {
refresh_state: 'requires_reauth',
last_refreshed_at: '2026-06-20T10:00:00Z',
error_code: 'token_expired',
error_message: 'Your Google Drive access has expired.',
}
api.getCloudFoldersByConnectionId.mockResolvedValue({
items: [ROOT_FOLDER_A],
capabilities: null,
freshness: REAUTH_FRESHNESS,
})
const wrapper = mount(CloudFolderView, {
global: { stubs: leafStubs },
})
await flushPromises()
// A reauthentication-specific UI element must be rendered
const hasReauthUi = wrapper.find('[data-test="requires-reauth"]').exists()
|| wrapper.find('[data-test="reauth-prompt"]').exists()
|| wrapper.html().toLowerCase().includes('reconnect')
// RED: no reauth-specific UI in the current browser
expect(hasReauthUi).toBe(true)
// Items must still be shown (D-14: cached metadata visible during reauth state)
expect(wrapper.html()).toContain('Alpha')
})
})
describe('no_health_probe_on_folder_navigate_D13', () => {
it('navigating into a subfolder does NOT trigger a separate health API call', async () => {
/**
* D-13: Ordinary folder navigation must never trigger a provider health probe.
* getCloudFoldersByConnectionId is for browse — no side-effect health calls.
*
* RED: a naive implementation could call testCloudConnection on every navigate.
*/
api.getCloudFoldersByConnectionId.mockResolvedValue({
items: [ROOT_FOLDER_A],
capabilities: null,
freshness: FRESH_FRESHNESS,
})
const wrapper = mount(CloudFolderView, {
global: { stubs: leafStubs },
})
await flushPromises()
// Navigate into a subfolder
const storageBrowser = wrapper.findComponent({ name: 'StorageBrowser' })
expect(storageBrowser.exists()).toBe(true)
api.getCloudFoldersByConnectionId.mockResolvedValue({
items: [ROOT_FILE_A],
capabilities: null,
freshness: FRESH_FRESHNESS,
})
await storageBrowser.vm.$emit('folder-navigate', ROOT_FOLDER_A)
await flushPromises()
// Browse API was called for the subfolder — that's expected
expect(api.getCloudFoldersByConnectionId).toHaveBeenCalledTimes(2)
// testCloudConnection (if it exists) must NOT have been called during navigation
const { testCloudConnection } = await import('../../api/client.js')
if (typeof testCloudConnection === 'function') {
expect(testCloudConnection).not.toHaveBeenCalled()
}
// The invariant is: browse calls = exactly N navigations; health calls = 0
})
})