Skip to content
Open
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
Expand Up @@ -7,22 +7,105 @@ exports[`ServiceTerminal should match snapshot 1`] = `
class="flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden rounded-none border-0 bg-background"
>
<div
class="flex h-11 justify-between border-b border-neutral px-4 py-2"
class="flex h-16 justify-between border-b border-neutral p-4"
>
<div
class="flex gap-2"
/>
class="flex gap-2 [&_input]:w-64"
>
<div
class="relative"
>
<div
class="relative z-10"
>
<button
class="items-center shrink-0 font-medium transition-[background-color,transform] active:scale-[0.97] disabled:scale-100 disabled:pointer-events-none disabled:border disabled:border-neutral disabled:text-neutral-disabled disabled:bg-surface-neutral-component focus-visible:[&:not(:active)]:outline-2 outline-0 select-none outline-neutral-strong text-sm h-8 px-2.5 rounded-md bg-surface-neutral-component hover:bg-surface-neutral-componentHover text-neutral hidden"
>
<span>
<i
aria-hidden="true"
class="fa-regular fa-xmark ml-2 text-sm"
/>
</span>
</button>
<div
class="relative"
>
<i
aria-hidden="true"
class="fa-regular fa-magnifying-glass absolute left-2.5 top-1/2 -translate-y-1/2 text-xs text-neutral-subtle"
/>
<input
aria-activedescendant=""
aria-autocomplete="list"
aria-controls="downshift-:r6:-menu"
aria-expanded="false"
aria-labelledby="downshift-:r6:-label"
autocomplete="off"
class="w-56 rounded border border-neutral bg-surface-neutral pl-7 pr-2 text-xs text-neutral placeholder:text-neutral-subtle focus:border-brand-8 focus:outline-none focus:transition-[border-color] h-8"
id="downshift-:r6:-input"
placeholder="Select a pod to connect to"
role="combobox"
value=""
/>
</div>
<div
class="mt-1 max-h-40 w-full min-w-56 overflow-y-auto rounded-md border border-neutral bg-surface-neutral p-2 shadow-[0_0_32px_rgba(0,0,0,0.08)] hidden"
>
<ul
aria-labelledby="downshift-:r6:-label"
id="downshift-:r6:-menu"
role="listbox"
>
<p
class="flex w-full flex-col py-2 text-center text-xs text-neutral-subtle"
>
<i
aria-hidden="true"
class="fa-regular fa-wave-pulse mb-1 block"
/>
No results found
</p>
</ul>
</div>
</div>
<div
class="pointer-events-none absolute right-2.5 top-1/2 z-20 -translate-y-1/2 text-xs text-neutral-subtle"
>
<i
aria-hidden="true"
class="fa-regular fa-angle-down "
/>
</div>
</div>
</div>
<a
class="inline-flex items-center shrink-0 font-medium transition-[background-color,transform] active:scale-[0.97] disabled:scale-100 disabled:pointer-events-none disabled:border disabled:border-neutral disabled:text-neutral-disabled disabled:bg-surface-neutral-component focus-visible:[&:not(:active)]:outline-2 outline-0 select-none outline-neutral-strong text-sm h-8 px-2.5 rounded-md bg-surface-neutral-component hover:bg-surface-neutral-componentHover text-neutral gap-1.5"
href="https://www.qovery.com/docs/cli/overview"
rel="noopener noreferrer"
target="_blank"
>
<i
aria-hidden="true"
class="fa-regular fa-book "
/>
CLI docs
</a>
</div>
<div
class="relative h-full min-h-[248px] flex-1 border-neutral bg-background px-4 py-2"
class="flex h-full flex-1 flex-col bg-background p-3"
>
<div
class="flex h-40 items-start justify-center p-5"
class="relative min-h-0 flex-1"
>
<div
class="aspect-square border-2 animate-spin rounded-full border-solid border-neutral border-r-neutral-strong w-4 "
data-testid="spinner"
/>
class="flex h-40 items-start justify-center p-5"
>
<div
class="aspect-square border-2 animate-spin rounded-full border-solid border-neutral border-r-neutral-strong w-4 "
data-testid="spinner"
/>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ exports[`InputSearch should match snapshot 1`] = `
</span>
</button>
<div
class=""
class="relative"
>
<i
aria-hidden="true"
class="fa-regular fa-magnifying-glass absolute left-2.5 top-1.5 text-xs text-neutral-subtle"
class="fa-regular fa-magnifying-glass absolute left-2.5 top-1/2 -translate-y-1/2 text-xs text-neutral-subtle"
/>
<input
aria-activedescendant=""
Expand All @@ -31,7 +31,7 @@ exports[`InputSearch should match snapshot 1`] = `
aria-expanded="false"
aria-labelledby="downshift-:r6:-label"
autocomplete="off"
class="h-7 w-56 rounded border border-neutral bg-surface-neutral pl-7 pr-2 text-xs text-neutral placeholder:text-neutral-subtle focus:border-brand-8 focus:outline-none focus:transition-[border-color]"
class="w-56 rounded border border-neutral bg-surface-neutral pl-7 pr-2 text-xs text-neutral placeholder:text-neutral-subtle focus:border-brand-8 focus:outline-none focus:transition-[border-color] h-7"
id="downshift-:r6:-input"
placeholder="placeholder"
role="combobox"
Expand Down Expand Up @@ -80,11 +80,11 @@ exports[`InputSearch should match with snapshot 1`] = `
</span>
</button>
<div
class=""
class="relative"
>
<i
aria-hidden="true"
class="fa-regular fa-magnifying-glass absolute left-2.5 top-1.5 text-xs text-neutral-subtle"
class="fa-regular fa-magnifying-glass absolute left-2.5 top-1/2 -translate-y-1/2 text-xs text-neutral-subtle"
/>
<input
aria-activedescendant=""
Expand All @@ -93,7 +93,7 @@ exports[`InputSearch should match with snapshot 1`] = `
aria-expanded="false"
aria-labelledby="downshift-:rd:-label"
autocomplete="off"
class="h-7 w-56 rounded border border-neutral bg-surface-neutral pl-7 pr-2 text-xs text-neutral placeholder:text-neutral-subtle focus:border-brand-8 focus:outline-none focus:transition-[border-color]"
class="w-56 rounded border border-neutral bg-surface-neutral pl-7 pr-2 text-xs text-neutral placeholder:text-neutral-subtle focus:border-brand-8 focus:outline-none focus:transition-[border-color] h-7"
id="downshift-:rd:-input"
placeholder="placeholder"
role="combobox"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ export interface InputSearchProps {
data: string[]
onChange: (value?: string) => void
placeholder: string
size?: 'sm' | 'md'
value?: string
trimLabel?: boolean
}

export function InputSearch({ data, value, onChange, placeholder, trimLabel }: InputSearchProps) {
export function InputSearch({ data, value, onChange, placeholder, trimLabel, size = 'sm' }: InputSearchProps) {
const [items, setItems] = useState(data)
const inputRef = useRef<HTMLInputElement>(null)
const buttonSize = size === 'md' ? 'md' : 'sm'

// https://github.com/radix-ui/primitives/issues/1342
// We are waiting for radix combobox primitives
Expand All @@ -34,6 +36,7 @@ export function InputSearch({ data, value, onChange, placeholder, trimLabel }: I
<Button
color="neutral"
variant="surface"
size={buttonSize}
className={!selectedItem ? 'hidden' : ''}
onClick={() => {
reset()
Expand All @@ -47,11 +50,17 @@ export function InputSearch({ data, value, onChange, placeholder, trimLabel }: I
<Icon iconName="xmark" className="ml-2 text-sm" />
</span>
</Button>
<div className={selectedItem ? 'hidden' : ''}>
<Icon iconName="magnifying-glass" className="absolute left-2.5 top-1.5 text-xs text-neutral-subtle" />
<div className={clsx({ hidden: selectedItem }, 'relative')}>
<Icon
iconName="magnifying-glass"
className="absolute left-2.5 top-1/2 -translate-y-1/2 text-xs text-neutral-subtle"
/>
<input
placeholder={placeholder}
className="h-7 w-56 rounded border border-neutral bg-surface-neutral pl-7 pr-2 text-xs text-neutral placeholder:text-neutral-subtle focus:border-brand-8 focus:outline-none focus:transition-[border-color]"
className={clsx(
'w-56 rounded border border-neutral bg-surface-neutral pl-7 pr-2 text-xs text-neutral placeholder:text-neutral-subtle focus:border-brand-8 focus:outline-none focus:transition-[border-color]',
size === 'md' ? 'h-8' : 'h-7'
)}
{...getInputProps({ ref: inputRef })}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { type QueryClient } from '@tanstack/react-query'
import { act, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { renderWithProviders } from '@qovery/shared/util-tests'
import { useReactQueryWsSubscription } from '@qovery/state/util-queries'
import { ServiceTerminal, type ServiceTerminalProps } from './service-terminal'

const mockUseRunningStatus = jest.fn()

jest.mock('color', () => ({
__esModule: true,
default: () => ({
Expand All @@ -13,7 +19,7 @@ jest.mock('@qovery/state/util-queries', () => ({
}))

jest.mock('../..', () => ({
useRunningStatus: () => ({ data: { pods: [] }, isLoading: false }),
useRunningStatus: (...args: unknown[]) => mockUseRunningStatus(...args),
}))

const props: ServiceTerminalProps = {
Expand All @@ -23,9 +29,83 @@ const props: ServiceTerminalProps = {
environmentId: '0',
serviceId: '0',
}

const useReactQueryWsSubscriptionMock = jest.mocked(useReactQueryWsSubscription)

const getLatestWsSubscriptionConfig = (): Parameters<typeof useReactQueryWsSubscription>[0] => {
const latestCall = useReactQueryWsSubscriptionMock.mock.calls.at(-1)

if (!latestCall) {
throw new Error('Expected useReactQueryWsSubscription to be called at least once.')
}

return latestCall[0]
}

describe('ServiceTerminal', () => {
beforeEach(() => {
jest.clearAllMocks()
mockUseRunningStatus.mockReturnValue({
data: {
pods: [
{ name: 'pod-1', containers: [{ name: 'container-1' }] },
{ name: 'pod-2', containers: [{ name: 'container-2' }] },
],
state: 'STOPPED',
},
isLoading: false,
})
})

it('should match snapshot', () => {
const { baseElement } = renderWithProviders(<ServiceTerminal {...props} />)
expect(baseElement).toMatchSnapshot()
})

it('should show a retry empty state and disable subscription on terminal launch error', () => {
renderWithProviders(<ServiceTerminal {...props} />)

act(() => {
getLatestWsSubscriptionConfig().onClose?.(
{} as QueryClient,
new CloseEvent('close', { code: 1000, reason: 'No pod exists for this application.' })
)
})

expect(screen.getByText('Unable to launch CLI')).toBeInTheDocument()
expect(screen.getByText("We could not launch the CLI for this service because it's stopped.")).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Relaunch' })).toBeInTheDocument()
expect(useReactQueryWsSubscriptionMock).toHaveBeenLastCalledWith(expect.objectContaining({ enabled: false }))
})

it('should restart terminal launch flow when retrying from empty state', async () => {
const user = userEvent.setup()
renderWithProviders(<ServiceTerminal {...props} />)

act(() => {
getLatestWsSubscriptionConfig().onClose?.(
{} as QueryClient,
new CloseEvent('close', { code: 1000, reason: 'No pod exists for this application.' })
)
})

await user.click(screen.getByRole('button', { name: 'Relaunch' }))

await waitFor(() => {
expect(screen.queryByText('Unable to launch CLI')).not.toBeInTheDocument()
expect(useReactQueryWsSubscriptionMock).toHaveBeenLastCalledWith(expect.objectContaining({ enabled: true }))
})
})

it('should show pod and container placeholders', async () => {
const { userEvent } = renderWithProviders(<ServiceTerminal {...props} />)

expect(screen.getByPlaceholderText('Select a pod to connect to')).toBeInTheDocument()

await userEvent.click(screen.getByPlaceholderText('Select a pod to connect to'))
const [firstPodOption] = screen.getAllByRole('option')
await userEvent.click(firstPodOption)

expect(screen.getByPlaceholderText('Select a container to connect to')).toBeInTheDocument()
})
})
Loading
Loading