Files
supabase/apps/studio/components/interfaces/Support/SupportStatus.utils.test.ts
Charis 8b8569bd1a feat(studio): status page client data layer + support form (#50984)
## Summary

* Adds the client query layer for `/api/status-page`
(`statusPageQueryOptions` in `data/platform/status-page-query.ts`) and a
shared normalization module (`lib/status-page/status-page.utils.ts`)
that maps the endpoint response into region- and project-creation-aware
`StatusItem`s — later PRs in this stack (assistant tool,
project-creation admonition, global banner) build on this same module.
* Wires the support form's status pill and incident admonition to the
new data behind the `incidentIoStatusPage` ConfigCat flag.
`useSupportStatus` branches between the new endpoint and the existing
`useIncidentStatusQuery`/`processIncidentData` path, both mapped to the
same `SupportStatus` shape.
* `IncidentAdmonition` becomes purely presentational; the status link
now points at the `pageUrl` returned by the endpoint instead of a
hardcoded URL.

Part of
[FE-4057](https://linear.app/supabase/issue/FE-4057/frontend-bannerbot-reconfigured)
— see Linear for full design context.

## Test plan

- [X] `pnpm --filter studio run typecheck`
- [X] `pnpm --filter studio run lint:ratchet`
- [X] `pnpm knip --workspace apps/studio`
- [X] `pnpm test:prettier`
- [X] `pnpm --filter studio exec vitest run` (touched files) — 70 tests
passing, including a flag-on case for `SupportFormPage`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Support pages now display current incident and maintenance
information, including relevant status descriptions and links to the
status page.
* Status details can reflect items affecting a user’s region or
project-creation services. Upcoming maintenance is excluded from active
alerts.
* **Bug Fixes**
* Status labels and alerts now account for loading, errors, incidents,
and maintenance consistently across support pages.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 07:53:28 -04:00

176 lines
5.6 KiB
TypeScript

import { describe, expect, test } from 'vitest'
import {
getStatusPillLabel,
getSupportAdmonitionCopy,
getSupportStatusLabel,
prioritizeActiveItems,
type SupportStatus,
} from './SupportStatus.utils'
import type { StatusItem } from '@/lib/status-page/status-page.utils'
function incident(overrides: Partial<Extract<StatusItem, { kind: 'incident' }>> = {}): StatusItem {
return {
kind: 'incident',
id: 'inc-1',
name: 'Elevated errors',
url: 'https://status.supabase.com/incidents/inc-1',
lastUpdateAt: '2026-01-01T00:00:00Z',
lastUpdateMessage: null,
components: [],
scope: { type: 'global' },
projectCreationScope: null,
isProjectCreationOnly: false,
showBanner: true,
status: 'investigating',
impact: 'full_outage',
...overrides,
}
}
function maintenance(
overrides: Partial<Extract<StatusItem, { kind: 'maintenance' }>> = {}
): StatusItem {
return {
kind: 'maintenance',
id: 'maint-1',
name: 'Database maintenance',
url: 'https://status.supabase.com/incidents/maint-1',
lastUpdateAt: '2026-01-01T00:00:00Z',
lastUpdateMessage: null,
components: [],
scope: { type: 'global' },
projectCreationScope: null,
isProjectCreationOnly: false,
showBanner: true,
startedAt: '2026-01-01T00:00:00Z',
scheduledEndAt: null,
...overrides,
}
}
function upcomingMaintenance(
overrides: Partial<Extract<StatusItem, { kind: 'upcoming_maintenance' }>> = {}
): StatusItem {
return {
kind: 'upcoming_maintenance',
id: 'upcoming-1',
name: 'Upcoming maintenance',
url: 'https://status.supabase.com/incidents/upcoming-1',
lastUpdateAt: '2026-01-01T00:00:00Z',
lastUpdateMessage: null,
components: [],
scope: { type: 'global' },
projectCreationScope: null,
isProjectCreationOnly: false,
showBanner: true,
startsAt: '2026-02-01T00:00:00Z',
endsAt: '2026-02-01T02:00:00Z',
leadDays: 7,
...overrides,
}
}
describe('prioritizeActiveItems', () => {
test('ranks incidents above maintenance by impact', () => {
const items = [
maintenance({ id: 'm', lastUpdateAt: '2026-01-01T00:00:00Z' }),
incident({
id: 'degraded',
impact: 'degraded_performance',
lastUpdateAt: '2026-01-01T00:00:00Z',
}),
incident({ id: 'full', impact: 'full_outage', lastUpdateAt: '2026-01-01T00:00:00Z' }),
]
expect(prioritizeActiveItems(items).map((item) => item.id)).toEqual(['full', 'degraded', 'm'])
})
test('breaks ties by lastUpdateAt descending', () => {
const items = [
incident({ id: 'older', impact: 'full_outage', lastUpdateAt: '2026-01-01T00:00:00Z' }),
incident({ id: 'newer', impact: 'full_outage', lastUpdateAt: '2026-01-02T00:00:00Z' }),
]
expect(prioritizeActiveItems(items).map((item) => item.id)).toEqual(['newer', 'older'])
})
test('ignores upcoming maintenance entirely', () => {
const items = [upcomingMaintenance(), incident()]
expect(prioritizeActiveItems(items).map((item) => item.id)).toEqual(['inc-1'])
})
test('includes items regardless of showBanner', () => {
const items = [incident({ showBanner: false })]
expect(prioritizeActiveItems(items)).toHaveLength(1)
})
})
describe('getSupportAdmonitionCopy', () => {
test('returns null when there is nothing active', () => {
expect(getSupportAdmonitionCopy([upcomingMaintenance()])).toBeNull()
})
test('singular title for one active item', () => {
const copy = getSupportAdmonitionCopy([incident({ name: 'Elevated errors' })])
expect(copy?.title).toBe('Elevated errors')
})
test('plural title for two active items', () => {
const copy = getSupportAdmonitionCopy([
incident({ id: 'a', name: 'Elevated errors', impact: 'full_outage' }),
incident({ id: 'b', name: 'Slow queries', impact: 'degraded_performance' }),
])
expect(copy?.title).toBe('Elevated errors and 1 other issue')
})
test('plural title uses plural "issues" for three or more', () => {
const copy = getSupportAdmonitionCopy([
incident({ id: 'a', impact: 'full_outage' }),
incident({ id: 'b', impact: 'degraded_performance' }),
incident({ id: 'c', impact: 'degraded_performance' }),
])
expect(copy?.title).toBe('Elevated errors and 2 other issues')
})
test('describes an in-progress maintenance', () => {
const copy = getSupportAdmonitionCopy([maintenance()])
expect(copy?.description).toContain('Scheduled maintenance is in progress')
})
})
describe('getStatusPillLabel', () => {
test('all systems operational when nothing is active', () => {
expect(getStatusPillLabel([upcomingMaintenance()])).toBe('All systems operational')
})
test('active incident ongoing when any incident is active', () => {
expect(getStatusPillLabel([incident(), maintenance()])).toBe('Active incident ongoing')
})
test('scheduled maintenance when only maintenance is active', () => {
expect(getStatusPillLabel([maintenance()])).toBe('Scheduled maintenance')
})
})
describe('getSupportStatusLabel', () => {
test('pending', () => {
expect(getSupportStatusLabel({ status: 'pending' })).toBe('Checking status')
})
test('error', () => {
expect(getSupportStatusLabel({ status: 'error' })).toBe('Failed to check status')
})
test('success uses the computed pill label', () => {
const status: SupportStatus = {
status: 'success',
hasActiveIncidents: true,
pillLabel: 'Active incident ongoing',
admonition: null,
pageUrl: 'https://status.supabase.com/',
}
expect(getSupportStatusLabel(status)).toBe('Active incident ongoing')
})
})