Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* Static-markup checks for the audit CleanupButton's visibility gate.
* Interactive two-stage dialog behaviour is covered by the pure-logic
* unit tests in `cleanup.helpers.test.ts` (phrase builder + comparator)
* plus the server-side audit-cleanup tests. This file only asserts the
* outer rule: the button hides itself when there is nothing to clean.
*/

import { describe, expect, it, mock } from 'bun:test'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderToStaticMarkup } from 'react-dom/server'
import type { CleanupCandidatesResponse } from '@/modules/api/audit.hooks'
import * as auditHooks from '@/modules/api/audit.hooks'

let candidatesOverride: CleanupCandidatesResponse | undefined

// Re-export the real module surface with only the two cleanup hooks
// stubbed. Preserves useTasks, useTaskDetail, useDispatches etc. for
// any downstream consumer (Audit.test.tsx runs in the same process
// and would otherwise resolve to a mock that's missing those exports).
mock.module('@/modules/api/audit.hooks', () => ({
...auditHooks,
useAuditCleanupCandidates: () => ({ data: candidatesOverride }),
useAuditCleanup: () => ({ mutate: () => {}, isPending: false }),
}))

const { CleanupButton } = await import('./CleanupButton')

function render(): string {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
return renderToStaticMarkup(
<QueryClientProvider client={client}>
<CleanupButton />
</QueryClientProvider>,
)
}

describe('CleanupButton visibility', () => {
it('renders nothing when candidates data is still loading', () => {
candidatesOverride = undefined
expect(render()).toBe('')
})

it('renders nothing when every range has zero sessions', () => {
candidatesOverride = {
ranges: [
{
olderThanDays: 15,
sessionCount: 0,
dispatchCount: 0,
bytesOnDisk: 0,
},
{
olderThanDays: 30,
sessionCount: 0,
dispatchCount: 0,
bytesOnDisk: 0,
},
{
olderThanDays: 90,
sessionCount: 0,
dispatchCount: 0,
bytesOnDisk: 0,
},
],
}
expect(render()).toBe('')
})

it('renders the Storage button when at least one range has sessions', () => {
candidatesOverride = {
ranges: [
{
olderThanDays: 15,
sessionCount: 3,
dispatchCount: 12,
bytesOnDisk: 1000,
},
{
olderThanDays: 30,
sessionCount: 0,
dispatchCount: 0,
bytesOnDisk: 0,
},
{
olderThanDays: 90,
sessionCount: 0,
dispatchCount: 0,
bytesOnDisk: 0,
},
],
}
const html = render()
expect(html).toContain('Storage')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* The "Storage" button in the audit header. Only renders when the
* candidates query returns at least one non-empty range (i.e. there
* IS data older than the smallest threshold). Owns the dialog open
* state so the header component doesn't have to.
*/

import { Trash2 } from 'lucide-react'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import type { CleanupCandidateStats } from '@/modules/api/audit.hooks'
import { useAuditCleanupCandidates } from '@/modules/api/audit.hooks'
import { CleanupDialog } from './CleanupDialog'

/**
* A range is worth showing to the user only when at least one session
* would be affected. Ranges with zero sessions are omitted entirely
* from the dialog (per Dani's spec).
*/
function nonEmptyRanges(
ranges: CleanupCandidateStats[] | undefined,
): CleanupCandidateStats[] {
return (ranges ?? []).filter((r) => r.sessionCount > 0)
}

export function CleanupButton() {
const [open, setOpen] = useState(false)
const candidates = useAuditCleanupCandidates()
const ranges = nonEmptyRanges(candidates.data?.ranges)

if (ranges.length === 0) return null

return (
<>
<Button
variant="ghost"
size="sm"
onClick={() => setOpen(true)}
className="h-8 gap-1.5 font-mono text-[11px] text-ink-2 uppercase tracking-[0.08em] hover:bg-card-tint"
>
<Trash2 className="size-3.5" />
Storage
</Button>
<CleanupDialog open={open} onOpenChange={setOpen} ranges={ranges} />
</>
)
}
Loading
Loading