From 19e27bdf6d06ef74f23806beeddc61443b2797e4 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:47:22 +0100 Subject: [PATCH 1/3] feat(frontend/select): expose clearable prop Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> --- .../ContainerProviderConnectionSelect.spec.ts | 27 ++++++- .../ContainerProviderConnectionSelect.svelte | 4 +- .../frontend/src/lib/select/Select.spec.ts | 79 ++++++++++++------- .../frontend/src/lib/select/Select.svelte | 2 + 4 files changed, 82 insertions(+), 30 deletions(-) diff --git a/packages/frontend/src/lib/select/ContainerProviderConnectionSelect.spec.ts b/packages/frontend/src/lib/select/ContainerProviderConnectionSelect.spec.ts index 712ff618..8885f0b8 100644 --- a/packages/frontend/src/lib/select/ContainerProviderConnectionSelect.spec.ts +++ b/packages/frontend/src/lib/select/ContainerProviderConnectionSelect.spec.ts @@ -17,7 +17,7 @@ ***********************************************************************/ import '@testing-library/jest-dom/vitest'; -import { beforeEach, expect, test, vi } from 'vitest'; +import { beforeEach, expect, test, vi, describe } from 'vitest'; import { render, within } from '@testing-library/svelte'; import ContainerProviderConnectionSelect from '/@/lib/select/ContainerProviderConnectionSelect.svelte'; import { VMType } from '/@shared/src/utils/vm-types'; @@ -71,3 +71,28 @@ test('default value should be visible', async () => { const select = within(container).getByText(qemuConnection.name); expect(select).toBeDefined(); }); + +describe('clear button', () => { + test('clear button should be visible by default', async () => { + const { container } = render(ContainerProviderConnectionSelect, { + value: qemuConnection, + containerProviderConnections: [wslConnection, qemuConnection], + }); + + // find clear HTMLElement + const clear = container.querySelector('button[class~="clear-select"]'); + expect(clear).toBeDefined(); + }); + + test('clearable prop should be propagated to Select component', async () => { + const { container } = render(ContainerProviderConnectionSelect, { + value: qemuConnection, + containerProviderConnections: [wslConnection, qemuConnection], + clearable: false, + }); + + // find clear HTMLElement + const clear = container.querySelector('button[class~="clear-select"]'); + expect(clear).toBeNull(); + }); +}); diff --git a/packages/frontend/src/lib/select/ContainerProviderConnectionSelect.svelte b/packages/frontend/src/lib/select/ContainerProviderConnectionSelect.svelte index 49ea5036..dcd2f182 100644 --- a/packages/frontend/src/lib/select/ContainerProviderConnectionSelect.svelte +++ b/packages/frontend/src/lib/select/ContainerProviderConnectionSelect.svelte @@ -8,9 +8,10 @@ interface Props { onChange?: (value: ProviderContainerConnectionDetailedInfo | undefined) => void; containerProviderConnections: ProviderContainerConnectionDetailedInfo[]; disabled?: boolean; + clearable?: boolean; } -let { value = $bindable(), containerProviderConnections, onChange, disabled }: Props = $props(); +let { value = $bindable(), clearable = true, containerProviderConnections, onChange, disabled }: Props = $props(); /** * Handy mechanism to provide the mandatory property `label` and `value` to the Select component @@ -47,6 +48,7 @@ function getProviderStatusColor(item: ProviderContainerConnectionDetailedInfo): disabled={disabled} value={selected} onchange={handleOnChange} + clearable={clearable} placeholder="Select container provider to use" items={containerProviderConnections.map(containerProviderConnection => ({ ...containerProviderConnection, diff --git a/packages/frontend/src/lib/select/Select.spec.ts b/packages/frontend/src/lib/select/Select.spec.ts index 24925fcf..553f7621 100644 --- a/packages/frontend/src/lib/select/Select.spec.ts +++ b/packages/frontend/src/lib/select/Select.spec.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (C) 2024 Red Hat, Inc. + * Copyright (C) 2024-2025 Red Hat, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ ***********************************************************************/ import '@testing-library/jest-dom/vitest'; -import { beforeEach, vi, test, expect } from 'vitest'; +import { beforeEach, vi, test, expect, describe } from 'vitest'; import { render, fireEvent, within } from '@testing-library/svelte'; import Select from '/@/lib/select/Select.svelte'; @@ -122,37 +122,60 @@ test('selecting value should call onchange callback', async () => { }); }); -test('clearing value should call onchange callback with undefined', async () => { - const onChangeMock = vi.fn(); - const { container } = render(Select, { - label: 'Select Item', - items: [ - { - label: 'Dummy Item 1', - value: 'item-1', - }, - { +describe('clear button', () => { + test('clearing value should call onchange callback with undefined', async () => { + const onChangeMock = vi.fn(); + const { container } = render(Select, { + label: 'Select Item', + items: [ + { + label: 'Dummy Item 1', + value: 'item-1', + }, + { + label: 'Dummy Item 2', + value: 'item-2', + }, + ], + value: { label: 'Dummy Item 2', value: 'item-2', }, - ], - value: { - label: 'Dummy Item 2', - value: 'item-2', - }, - onchange: onChangeMock, - }); + onchange: onChangeMock, + }); - // get clear HTMLElement - const clear = container.querySelector('button[class~="clear-select"]'); - // ensure we have two options - expect(clear).not.toBeNull(); - if (!clear) throw new Error('clear is null'); + // get clear HTMLElement + const clear = container.querySelector('button[class~="clear-select"]'); + // ensure we have two options + expect(clear).not.toBeNull(); + if (!clear) throw new Error('clear is null'); - await fireEvent.click(clear); + await fireEvent.click(clear); - await vi.waitFor(() => { - expect(onChangeMock).toHaveBeenCalledWith(undefined); - expect(onChangeMock).toHaveBeenCalledOnce(); + await vi.waitFor(() => { + expect(onChangeMock).toHaveBeenCalledWith(undefined); + expect(onChangeMock).toHaveBeenCalledOnce(); + }); + }); + + test('clearable props should be respected', async () => { + const { container } = render(Select, { + label: 'Select Item', + items: [ + { + label: 'Dummy Item 1', + value: 'item-1', + }, + ], + value: { + label: 'Dummy Item 1', + value: 'item-1', + }, + clearable: false, + }); + + // find clear HTMLElement + const clear = container.querySelector('button[class~="clear-select"]'); + expect(clear).toBeNull(); }); }); diff --git a/packages/frontend/src/lib/select/Select.svelte b/packages/frontend/src/lib/select/Select.svelte index d87e5b44..e314f5fd 100644 --- a/packages/frontend/src/lib/select/Select.svelte +++ b/packages/frontend/src/lib/select/Select.svelte @@ -10,6 +10,7 @@ export let placeholder: string | undefined = undefined; export let label: string | undefined = undefined; export let name: string | undefined = undefined; export let onchange: ((value: T | undefined) => void) | undefined = undefined; +export let clearable: boolean = true; function handleOnChange(e: CustomEvent): void { value = e.detail; @@ -52,6 +53,7 @@ function handleOnClear(): void { --height="32px" --max-height="32px" placeholder={placeholder} + clearable={clearable} class="!bg-[var(--pd-content-bg)] !text-[var(--pd-content-card-text)]" items={items} showChevron={!disabled}> From 6d4f46bc69f0c1b73040377f501294824da96616 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:47:37 +0100 Subject: [PATCH 2/3] feat(frontend/forms): auto select container engines Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> --- .../lib/forms/compose/QuadletComposeForm.spec.ts | 15 +++++++++++++++ .../lib/forms/compose/QuadletComposeForm.svelte | 6 ++++++ .../forms/quadlet/QuadletGenerateForm.spec.ts | 16 ++++++++++++++++ .../lib/forms/quadlet/QuadletGenerateForm.svelte | 7 +++++++ 4 files changed, 44 insertions(+) diff --git a/packages/frontend/src/lib/forms/compose/QuadletComposeForm.spec.ts b/packages/frontend/src/lib/forms/compose/QuadletComposeForm.spec.ts index f058c310..a89ff7cb 100644 --- a/packages/frontend/src/lib/forms/compose/QuadletComposeForm.spec.ts +++ b/packages/frontend/src/lib/forms/compose/QuadletComposeForm.spec.ts @@ -29,6 +29,10 @@ import { readable } from 'svelte/store'; import type { ProviderApi } from '/@shared/src/apis/provide-api'; import type { PodletApi } from '/@shared/src/apis/podlet-api'; import type { QuadletApi } from '/@shared/src/apis/quadlet-api'; +import { router } from 'tinro'; + +// mock router lib +vi.mock(import('tinro')); // mock clients vi.mock(import('/@/api/client'), () => ({ @@ -67,6 +71,17 @@ beforeEach(() => { }); describe('step select', () => { + test('expect container engine to be automatically selected', async () => { + render(QuadletComposeForm, { + providerId: undefined, + connection: undefined, + loading: false, + }); + + expect(router.location.query.set).toHaveBeenCalledWith('providerId', WSL_PROVIDER_DETAILED_INFO.providerId); + expect(router.location.query.set).toHaveBeenCalledWith('connection', WSL_PROVIDER_DETAILED_INFO.name); + }); + test('file provided as parameter should be displayed', async () => { const { getByRole } = render(QuadletComposeForm, { filepath: FILEPATH_MOCK, diff --git a/packages/frontend/src/lib/forms/compose/QuadletComposeForm.svelte b/packages/frontend/src/lib/forms/compose/QuadletComposeForm.svelte index d80763f9..ca5d40c5 100644 --- a/packages/frontend/src/lib/forms/compose/QuadletComposeForm.svelte +++ b/packages/frontend/src/lib/forms/compose/QuadletComposeForm.svelte @@ -29,6 +29,12 @@ let selectedContainerProviderConnection: ProviderContainerConnectionDetailedInfo $providerConnectionsInfo.find(provider => provider.providerId === providerId && provider.name === connection), ); +$effect(() => { + if (!selectedContainerProviderConnection && $providerConnectionsInfo.length > 0) { + onContainerProviderConnectionChange($providerConnectionsInfo[0]); + } +}); + const DEFAULT_KUBE_QUADLET = ` [Unit] Description=A kubernetes yaml based service diff --git a/packages/frontend/src/lib/forms/quadlet/QuadletGenerateForm.spec.ts b/packages/frontend/src/lib/forms/quadlet/QuadletGenerateForm.spec.ts index 53532398..34f7c06b 100644 --- a/packages/frontend/src/lib/forms/quadlet/QuadletGenerateForm.spec.ts +++ b/packages/frontend/src/lib/forms/quadlet/QuadletGenerateForm.spec.ts @@ -30,6 +30,10 @@ import type { Component, ComponentProps } from 'svelte'; import type { ContainerApi } from '/@shared/src/apis/container-api'; import type { ProviderApi } from '/@shared/src/apis/provide-api'; import type { PodletApi } from '/@shared/src/apis/podlet-api'; +import { router } from 'tinro'; + +// mock router lib +vi.mock(import('tinro')); // mock clients vi.mock(import('/@/api/client'), () => ({ @@ -67,6 +71,18 @@ beforeEach(() => { }); describe('Step options', () => { + test('expect container engine to be automatically selected', async () => { + render(QuadletGenerateForm, { + providerId: undefined, + connection: undefined, + loading: false, + close: vi.fn(), + }); + + expect(router.location.query.set).toHaveBeenCalledWith('providerId', WSL_PROVIDER_DETAILED_INFO.providerId); + expect(router.location.query.set).toHaveBeenCalledWith('connection', WSL_PROVIDER_DETAILED_INFO.name); + }); + test('expect cancel to call close', async () => { const closeMock = vi.fn(); const { getByRole } = render(QuadletGenerateForm, { diff --git a/packages/frontend/src/lib/forms/quadlet/QuadletGenerateForm.svelte b/packages/frontend/src/lib/forms/quadlet/QuadletGenerateForm.svelte index 7f040a6f..7c5942bc 100644 --- a/packages/frontend/src/lib/forms/quadlet/QuadletGenerateForm.svelte +++ b/packages/frontend/src/lib/forms/quadlet/QuadletGenerateForm.svelte @@ -44,6 +44,12 @@ let selectedContainerProviderConnection: ProviderContainerConnectionDetailedInfo $providerConnectionsInfo.find(provider => provider.providerId === providerId && provider.name === connection), ); +$effect(() => { + if (!selectedContainerProviderConnection && $providerConnectionsInfo.length > 0) { + onContainerProviderConnectionChange($providerConnectionsInfo[0]); + } +}); + function onQuadletTypeChange(value: string): void { router.location.query.set('quadletType', value); router.location.query.delete(RESOURCE_ID_QUERY); // delete the key @@ -174,6 +180,7 @@ function resetGenerate(): void { disabled={loading} onChange={onContainerProviderConnectionChange} value={selectedContainerProviderConnection} + clearable={false} containerProviderConnections={$providerConnectionsInfo} /> {#if selectedContainerProviderConnection && selectedContainerProviderConnection.status !== 'started'}
From 3be6564b541891811212af5e287bb1fee72f8b35 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Fri, 26 Dec 2025 11:41:48 +0100 Subject: [PATCH 3/3] test(e2e): simply engine selection logic Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> --- tests/playwright/src/quadlet-extension.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/playwright/src/quadlet-extension.spec.ts b/tests/playwright/src/quadlet-extension.spec.ts index 045bfd3f..fff64749 100644 --- a/tests/playwright/src/quadlet-extension.spec.ts +++ b/tests/playwright/src/quadlet-extension.spec.ts @@ -137,18 +137,6 @@ test.describe.serial(`Podman Quadlet extension installation and verification`, { await playExpect(generateForm.cancelButton).toBeEnabled(); await playExpect(generateForm.generateButton).toBeDisabled(); // default should be disabled - // open the select dropdown - const podmanProviders = await generateForm.containerEngineSelect.getOptions(); - playExpect(podmanProviders.length).toBeGreaterThan(0); - - const sorted = podmanProviders.find(provider => provider.toLowerCase().includes('podman')); - if (!sorted) throw new Error('cannot found podman provider'); - - // Value can be `podman-machine-default (WSL)` - const machine = sorted.split(' ')[0]; - console.log(`Trying to use provider ${machine}`); - await generateForm.containerEngineSelect.set(machine); - // wait for loading to be finished await playExpect .poll(async () => await generateForm.isLoading(), {