Files
supabase/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.test.ts
T
CharisandJoshen Lim 8bdfe03fe7 refactor(studio): drop notebook type widening now that the API supports it (#49272)
## Summary

- Regenerates `packages/api-types` for the content endpoints now that
the Platform API's `notebook` content type has landed (list/get/upsert
`type` enums, plus `UpsertContentBody`'s notebook cell shape with
`_id`/`y_series`). Unrelated schema drift from the same regen
(Warehouse, SSO, notification exceptions, etc.) is excluded — only the
content-endpoint hunks are applied.
- Removes every local widening cast added while the API support was
pending (`content-query.ts`, `content-infinite-query.ts`,
`notebook-query.ts`, `notebook-upsert-mutation.ts`,
`sql-folders-query.ts`).
- What remains is scoped and renamed to match: draft ids
(`generateDraftId`/`isDraftId`), used only for cells created client-side
in the editor before their first save, dropped before they'd ever reach
the backend as a fake `_id`.

## Test plan

- [x] `pnpm typecheck` — clean
- [x] `pnpm --filter studio test` — full suite passes (518 files / 5471
tests)
- [x] `pnpm --filter studio run lint:ratchet` — no new warnings
- [x] `pnpm format` / prettier — clean

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

* **Bug Fixes**
* Improved notebook cell tracking during editing, reordering, insertion,
and deletion.
* Preserved existing cell identifiers while removing temporary draft
identifiers before saving.
* Improved chart configuration for selecting and displaying multiple
Y-axis series.
  * Strengthened notebook validation and content persistence behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-08-20 13:06:41 +08:00

183 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import dayjs from 'dayjs'
import { describe, expect, it } from 'vitest'
import {
formatNotebookDiffSummary,
formatTimeRange,
getCellLabel,
getCellMetadataLine,
getEntryKey,
summarizeNotebookDiff,
} from './NotebookPreview.utils'
import type { NotebookCellDiffEntry } from '@/data/content/notebooks/notebook-operations'
import type { AgentCell, CellWire } from '@/data/content/notebooks/notebook-schema'
import { isoDateTimeString } from '@/lib/iso-datetime'
const wireMarkdownCell = (id: string, text = 'hello'): CellWire => ({
_tag: 'markdown_cell',
_id: id,
text,
})
const agentMarkdownCell = (text = 'hello'): AgentCell => ({ _tag: 'markdown_cell', text })
const wireDatabaseCell = (id: string, title?: string, database_identifier?: string): CellWire => ({
_tag: 'database_cell',
_id: id,
title,
sql: 'select 1',
row_limit: 100,
database_identifier,
})
const wireLogCell = (id: string): CellWire => ({
_tag: 'log_cell',
_id: id,
sql: 'select 1',
time_range: { _tag: 'relative_time_range', unit: 'day', amount: 7 },
})
describe('getEntryKey', () => {
it('keys unchanged, removed, and moved entries off the cell id', () => {
expect(getEntryKey({ _tag: 'unchanged', cell: wireMarkdownCell('cell-1') })).toBe('cell-1')
expect(
getEntryKey({ _tag: 'removed', cell: wireMarkdownCell('cell-2'), operationIndex: 0 })
).toBe('cell-2')
expect(
getEntryKey({
_tag: 'moved',
cell: wireMarkdownCell('cell-3'),
fromIndex: 1,
operationIndex: 0,
})
).toBe('cell-3')
})
it('keys added and replaced entries off the operation index', () => {
expect(getEntryKey({ _tag: 'added', cell: agentMarkdownCell(), operationIndex: 2 })).toBe(
'op-2'
)
expect(
getEntryKey({
_tag: 'replaced',
before: wireMarkdownCell('cell-4'),
after: agentMarkdownCell(),
operationIndex: 3,
})
).toBe('op-3')
})
})
describe('getCellLabel', () => {
it('labels markdown cells', () => {
expect(getCellLabel(wireMarkdownCell('cell-1'))).toBe('Markdown cell')
})
it('labels query cells with their title', () => {
expect(getCellLabel(wireDatabaseCell('cell-1', 'Signups'))).toBe('Query: Signups')
})
it('falls back to "Untitled query" when a query cell has no title', () => {
expect(getCellLabel(wireDatabaseCell('cell-1'))).toBe('Query: Untitled query')
})
})
describe('formatTimeRange', () => {
it('formats a relative range, pluralizing the unit', () => {
expect(formatTimeRange({ _tag: 'relative_time_range', unit: 'day', amount: 7 })).toBe(
'Last 7 days'
)
})
it('does not pluralize a relative range with amount 1', () => {
expect(formatTimeRange({ _tag: 'relative_time_range', unit: 'hour', amount: 1 })).toBe(
'Last 1 hour'
)
})
it('formats an absolute range as a start → end pair', () => {
const start = isoDateTimeString('2026-01-01T13:00:00.000Z')!
const end = isoDateTimeString('2026-01-02T09:30:00.000Z')!
const formatted = formatTimeRange({ _tag: 'absolute_time_range', start, end })
// Bounds are asserted via dayjs rather than a hardcoded string so this doesn't depend on
// the test runner's local timezone (formatTimeRange formats in local time).
const expectedBound = (value: string) => dayjs(value).format('MMM D, YYYY h:mm A')
expect(formatted).toBe(`${expectedBound(start)} → ${expectedBound(end)}`)
})
})
describe('getCellMetadataLine', () => {
it('returns null for markdown cells', () => {
expect(getCellMetadataLine(wireMarkdownCell('cell-1'))).toBeNull()
})
it('returns null for a database cell with no database_identifier', () => {
expect(getCellMetadataLine(wireDatabaseCell('cell-1'))).toBeNull()
})
it('labels a database cell with a database_identifier', () => {
expect(getCellMetadataLine(wireDatabaseCell('cell-1', 'Signups', 'replica-3'))).toBe(
'Database: replica-3'
)
})
it('labels a log cell with its formatted time range', () => {
expect(getCellMetadataLine(wireLogCell('cell-1'))).toBe('Time range: Last 7 days')
})
})
describe('summarizeNotebookDiff / formatNotebookDiffSummary', () => {
it('formats create mode with pluralized cell count', () => {
const entries: NotebookCellDiffEntry[] = [
{ _tag: 'unchanged', cell: wireMarkdownCell('a') },
{ _tag: 'unchanged', cell: wireMarkdownCell('b') },
]
expect(formatNotebookDiffSummary(summarizeNotebookDiff(entries, 'create'))).toBe('2 cells')
})
it('formats create mode singular for a single cell', () => {
const entries: NotebookCellDiffEntry[] = [{ _tag: 'unchanged', cell: wireMarkdownCell('a') }]
expect(formatNotebookDiffSummary(summarizeNotebookDiff(entries, 'create'))).toBe('1 cell')
})
it('formats update mode with a mix of categories, ignoring unchanged', () => {
const entries: NotebookCellDiffEntry[] = [
{ _tag: 'unchanged', cell: wireMarkdownCell('a') },
{ _tag: 'added', cell: agentMarkdownCell(), operationIndex: 0 },
{ _tag: 'added', cell: agentMarkdownCell(), operationIndex: 1 },
{ _tag: 'removed', cell: wireMarkdownCell('d'), operationIndex: 2 },
{
_tag: 'replaced',
before: wireMarkdownCell('e'),
after: agentMarkdownCell(),
operationIndex: 3,
},
{ _tag: 'moved', cell: wireMarkdownCell('f'), fromIndex: 0, operationIndex: 4 },
]
expect(formatNotebookDiffSummary(summarizeNotebookDiff(entries, 'update'))).toBe('+2 −1 ~1 ↕1')
})
it('omits zero categories from the update summary', () => {
const entries: NotebookCellDiffEntry[] = [
{ _tag: 'unchanged', cell: wireMarkdownCell('a') },
{ _tag: 'added', cell: agentMarkdownCell(), operationIndex: 0 },
]
expect(formatNotebookDiffSummary(summarizeNotebookDiff(entries, 'update'))).toBe('+1')
})
it('falls back to "No changes" when every entry is unchanged', () => {
const entries: NotebookCellDiffEntry[] = [
{ _tag: 'unchanged', cell: wireMarkdownCell('a') },
{ _tag: 'unchanged', cell: wireMarkdownCell('b') },
]
expect(formatNotebookDiffSummary(summarizeNotebookDiff(entries, 'update'))).toBe('No changes')
})
})