Files
floci-ui/packages/frontend/src/components/DynamoDbTableExplorer.test.tsx
T
2026-09-30 17:43:42 -04:00

143 lines
7.5 KiB
TypeScript

import {QueryClient, QueryClientProvider} from '@tanstack/react-query'
import {render, screen, waitFor, within} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {MemoryRouter, useLocation} from 'react-router-dom'
import {expect, test, vi} from 'vitest'
import {DynamoDbTableExplorer} from './DynamoDbTableExplorer'
import type {CloudResource} from '@/types/resource'
const apiMocks = vi.hoisted(() => ({
deleteFlatItem: vi.fn(),
queryFlatItems: vi.fn(),
}))
vi.mock('@/api/cloudProxyClient', () => apiMocks)
const table: CloudResource = {
id: 'orders', name: 'orders', cloud: 'aws', service: 'nosql', type: 'dynamodb-table',
region: null, createdAt: null, metadata: {
keySchema: [{AttributeName: 'pk', KeyType: 'HASH'}, {AttributeName: 'sk', KeyType: 'RANGE'}],
attributeDefinitions: [
{AttributeName: 'pk', AttributeType: 'S'}, {AttributeName: 'sk', AttributeType: 'N'},
{AttributeName: 'status', AttributeType: 'S'}, {AttributeName: 'createdAt', AttributeType: 'S'},
],
globalSecondaryIndexes: [{IndexName: 'status-created', KeySchema: [
{AttributeName: 'status', KeyType: 'HASH'}, {AttributeName: 'createdAt', KeyType: 'RANGE'},
]}],
},
}
function Location() {
return <output data-testid="location">{useLocation().pathname}</output>
}
function renderExplorer() {
const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}})
return render(<MemoryRouter initialEntries={['/cloud-explorer/aws/nosql/orders/data']}><QueryClientProvider client={queryClient}><DynamoDbTableExplorer cloud="aws" resource={table} runtimeReachable/><Location/></QueryClientProvider></MemoryRouter>)
}
test('renders returned DynamoDB attributes as columns and bulk deletes selected items', async () => {
apiMocks.queryFlatItems.mockResolvedValue({items: [
{id: '{"pk":"order-1"}', collectionId: null, timestamp: null, body: {pk: 'order-1', status: 'new', total: 12, tags: ['priority', 'web']}, metadata: {key: {pk: 'order-1'}}},
{id: '{"pk":"order-2"}', collectionId: null, timestamp: null, body: {pk: 'order-2', active: true, total: 8}, metadata: {key: {pk: 'order-2'}}},
], nextCursor: null})
apiMocks.deleteFlatItem.mockResolvedValue(undefined)
const user = userEvent.setup()
renderExplorer()
expect(await screen.findByRole('columnheader', {name: 'pk'})).toBeInTheDocument()
expect(screen.getByRole('columnheader', {name: 'status'})).toBeInTheDocument()
expect(screen.getByRole('columnheader', {name: 'active'})).toBeInTheDocument()
expect(screen.queryByRole('columnheader', {name: 'Key'})).not.toBeInTheDocument()
expect(screen.getByText('priority, web')).toBeInTheDocument()
await user.click(screen.getByLabelText('Select all items'))
await user.click(screen.getByText('Actions'))
await user.click(screen.getByRole('button', {name: 'Delete item'}))
expect(screen.getByRole('alertdialog')).toHaveTextContent('Delete 2 items?')
expect(screen.getByText('Actions').closest('details')).not.toHaveAttribute('open')
await user.click(screen.getByRole('button', {name: 'Delete items'}))
await waitFor(() => {
expect(apiMocks.deleteFlatItem).toHaveBeenCalledWith('aws', 'nosql', 'orders', '{"pk":"order-1"}')
expect(apiMocks.deleteFlatItem).toHaveBeenCalledWith('aws', 'nosql', 'orders', '{"pk":"order-2"}')
})
expect(screen.getByTestId('location')).toHaveTextContent('/cloud-explorer/aws/nosql/orders/data')
})
test('delete menu action does not navigate to the item editor', async () => {
apiMocks.queryFlatItems.mockResolvedValue({items: [
{id: 'display-id', collectionId: null, timestamp: null, body: {pk: 'order-1'}, metadata: {key: {pk: 'order-1', sk: '2026-09-29'}}},
], nextCursor: null})
apiMocks.deleteFlatItem.mockResolvedValue(undefined)
const user = userEvent.setup()
renderExplorer()
await user.click(await screen.findByLabelText('Select item display-id'))
await user.click(screen.getByText('Actions'))
await user.click(screen.getByRole('button', {name: 'Delete item'}))
expect(screen.getByRole('alertdialog')).toHaveTextContent('Delete item?')
expect(screen.getByText('Actions').closest('details')).not.toHaveAttribute('open')
await user.click(within(screen.getByRole('alertdialog')).getByRole('button', {name: 'Delete item'}))
await waitFor(() => {
expect(apiMocks.deleteFlatItem).toHaveBeenCalledWith('aws', 'nosql', 'orders', '{"pk":"order-1","sk":"2026-09-29"}')
})
expect(screen.getByTestId('location')).toHaveTextContent('/cloud-explorer/aws/nosql/orders/data')
})
test('runs Scan with a typed server-side filter and loads the next cursor', async () => {
apiMocks.queryFlatItems.mockImplementation(async (_cloud, _service, _resource, query, cursor) => {
if (!query.filter) return {items: [], nextCursor: null, count: 0}
return cursor ? {
items: [{id: 'two', collectionId: null, timestamp: null, body: {pk: 'two'}, metadata: {key: {pk: 'two'}}}],
nextCursor: null, count: 1, scannedCount: 2, consumedCapacity: 0.25,
} : {
items: [{id: 'one', collectionId: null, timestamp: null, body: {pk: 'one'}, metadata: {key: {pk: 'one'}}}],
nextCursor: 'opaque-page-2', count: 1, scannedCount: 7, consumedCapacity: 0.5,
}
})
const user = userEvent.setup()
renderExplorer()
await screen.findByText('No records')
await user.type(screen.getByLabelText('Filter attribute'), 'total')
await user.selectOptions(screen.getByLabelText('Filter operator'), 'GE')
await user.selectOptions(screen.getByLabelText('Filter value type'), 'N')
await user.type(screen.getByLabelText('Filter value'), '10')
await user.click(screen.getByRole('button', {name: 'scan'}))
await waitFor(() => expect(apiMocks.queryFlatItems).toHaveBeenLastCalledWith(
'aws', 'nosql', 'orders', {operation: 'scan', filter: {attribute: 'total', operator: 'GE', type: 'N', value: '10'}}, undefined, 100, expect.any(AbortSignal),
))
await user.click(await screen.findByRole('button', {name: 'Load more'}))
await waitFor(() => expect(apiMocks.queryFlatItems).toHaveBeenLastCalledWith(
'aws', 'nosql', 'orders', expect.any(Object), 'opaque-page-2', 100, expect.any(AbortSignal),
))
expect(await screen.findByText('two')).toBeInTheDocument()
expect(screen.getByText('2 returned, 9 scanned, 0.75 capacity units')).toBeInTheDocument()
})
test('adapts Query key fields to a selected index and submits conditions', async () => {
apiMocks.queryFlatItems.mockResolvedValue({items: [], nextCursor: null, count: 0})
const user = userEvent.setup()
renderExplorer()
await user.click(screen.getByRole('button', {name: 'Query'}))
await user.selectOptions(screen.getByLabelText('Index'), 'status-created')
expect(screen.getByText('status (S)')).toBeInTheDocument()
await user.type(screen.getByLabelText('Partition key value'), 'OPEN')
await user.selectOptions(screen.getByLabelText('Sort key condition'), 'begins_with')
await user.type(screen.getByLabelText('Sort key value'), '2026-09')
await user.click(screen.getByRole('button', {name: 'query'}))
await waitFor(() => expect(apiMocks.queryFlatItems).toHaveBeenLastCalledWith(
'aws', 'nosql', 'orders', {
operation: 'query', indexName: 'status-created', partitionKeyValue: 'OPEN',
sortKeyCondition: {operator: 'begins_with', value: '2026-09'},
}, undefined, 100, expect.any(AbortSignal),
))
})