143 lines
7.5 KiB
TypeScript
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),
|
|
))
|
|
})
|