diff --git a/packages/renderer/src/lib/engine/Prune.spec.ts b/packages/renderer/src/lib/engine/Prune.spec.ts new file mode 100644 index 000000000..5780ba58a --- /dev/null +++ b/packages/renderer/src/lib/engine/Prune.spec.ts @@ -0,0 +1,165 @@ +/********************************************************************** + * Copyright (C) 2023-2024 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import '@testing-library/jest-dom/vitest'; + +import { fireEvent, render, screen } from '@testing-library/svelte'; +import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; + +import Prune from './Prune.svelte'; + +beforeAll(() => { + Object.defineProperty(global, 'window', { + value: { + showMessageBox: vi.fn(), + pruneContainers: vi.fn(), + pruneImages: vi.fn(), + }, + writable: true, + }); +}); + +beforeEach(() => { + vi.resetAllMocks(); +}); + +describe('containers', () => { + test('pruned containers', async () => { + render(Prune, { + type: 'containers', + engines: [ + { + id: 'podman', + name: 'Podman', + }, + ], + }); + + // mock the window.showMessageBox method to return "all" + const response = 1; + vi.mocked(window.showMessageBox).mockResolvedValue({ + response, + }); + + // search for the button + const button = await screen.findByRole('button', { name: 'Prune' }); + expect(button).toBeInTheDocument(); + await fireEvent.click(button); + + // check if the showMessageBox method was called with all the right parameters + expect(window.showMessageBox).toHaveBeenCalledWith({ + buttons: ['Cancel', 'Yes'], + title: 'Prune', + message: 'This action will prune all unused containers from the Podman engine.', + }); + + expect(window.pruneContainers).toHaveBeenCalledWith('podman'); + }); +}); + +describe('images', () => { + const CANCEL_BUTTON = 'Cancel'; + const ALL_UNUSED_IMAGES = 'All unused images'; + const ALL_UNTAGGED_IMAGES = 'All untagged images'; + + const IMAGE_BUTTONS = [CANCEL_BUTTON, ALL_UNUSED_IMAGES, ALL_UNTAGGED_IMAGES]; + + const imageRender = () => { + render(Prune, { + type: 'images', + engines: [ + { + id: 'podman', + name: 'Podman', + }, + ], + }); + }; + + test('prune all untagged images', async () => { + imageRender(); + + // mock the window.showMessageBox method to return "all untaged images" + const response = IMAGE_BUTTONS.indexOf(ALL_UNTAGGED_IMAGES); + vi.mocked(window.showMessageBox).mockResolvedValue({ + response, + }); + + // search for the button + const button = await screen.findByRole('button', { name: 'Prune' }); + expect(button).toBeInTheDocument(); + await fireEvent.click(button); + + // check if the showMessageBox method was called with all the right parameters + expect(window.showMessageBox).toHaveBeenCalledWith({ + buttons: IMAGE_BUTTONS, + title: 'Prune', + message: 'This action will prune images from the Podman engine.', + }); + + expect(window.pruneImages).toHaveBeenCalledWith('podman', false); + }); + + test('prune all unused images', async () => { + imageRender(); + + // mock the window.showMessageBox method to return "all unused images" + const response = IMAGE_BUTTONS.indexOf(ALL_UNUSED_IMAGES); + vi.mocked(window.showMessageBox).mockResolvedValue({ + response, + }); + + // search for the button + const button = await screen.findByRole('button', { name: 'Prune' }); + expect(button).toBeInTheDocument(); + await fireEvent.click(button); + + // check if the showMessageBox method was called with all the right parameters + expect(window.showMessageBox).toHaveBeenCalledWith({ + buttons: IMAGE_BUTTONS, + title: 'Prune', + message: 'This action will prune images from the Podman engine.', + }); + + expect(window.pruneImages).toHaveBeenCalledWith('podman', true); + }); + + test('prune nothing (click cancel)', async () => { + imageRender(); + + // mock the window.showMessageBox method to return "Cancel" + const response = IMAGE_BUTTONS.indexOf(CANCEL_BUTTON); + vi.mocked(window.showMessageBox).mockResolvedValue({ + response, + }); + + // search for the button + const button = await screen.findByRole('button', { name: 'Prune' }); + expect(button).toBeInTheDocument(); + await fireEvent.click(button); + + // check if the showMessageBox method was called with all the right parameters + expect(window.showMessageBox).toHaveBeenCalledWith({ + buttons: IMAGE_BUTTONS, + title: 'Prune', + message: 'This action will prune images from the Podman engine.', + }); + + expect(window.pruneImages).not.toBeCalled(); + }); +}); diff --git a/packages/renderer/src/lib/engine/Prune.svelte b/packages/renderer/src/lib/engine/Prune.svelte index ed74ea181..556ad212d 100644 --- a/packages/renderer/src/lib/engine/Prune.svelte +++ b/packages/renderer/src/lib/engine/Prune.svelte @@ -5,32 +5,52 @@ import { Button } from '@podman-desktop/ui-svelte'; import type { EngineInfoUI } from './EngineInfoUI'; // Imported type for prune (containers, images, pods, volumes) -export let type: string; +export let type: 'containers' | 'images' | 'pods' | 'volumes'; // List of engines that the prune will work on export let engines: EngineInfoUI[]; +const LABEL_IMAGE_UNUSED = 'All unused images'; +const LABEL_IMAGE_UNTAGGED = 'All untagged images'; + async function openPruneDialog(): Promise { - let message = 'This action will prune all unused ' + type; + let message = 'This action will prune'; + + if (type === 'images') { + message += ' images'; + } else { + message += ` all unused ${type}`; + } if (engines.length > 1) { message += ' from all container engines.'; } else { message += ' from the ' + engines[0].name + ' engine.'; } + const buttons: string[] = []; + const cancel = 'Cancel'; + buttons.push(cancel); + if (type === 'images') { + buttons.push(LABEL_IMAGE_UNUSED); + buttons.push(LABEL_IMAGE_UNTAGGED); + } else { + buttons.push('Yes'); + } + const result = await window.showMessageBox({ title: 'Prune', message: message, - buttons: ['Yes', 'Cancel'], + buttons, }); - if (result && result.response === 0) { - await prune(type); + const selectedItemLabel = buttons[result.response ?? 0]; + if (selectedItemLabel !== cancel) { + await prune(type, selectedItemLabel); } } // Function to prune the selected type: containers, pods, images and volumes -async function prune(type: string) { +async function prune(type: string, selectedItemLabel: string): Promise { switch (type) { case 'containers': for (let engine of engines) { @@ -62,7 +82,7 @@ async function prune(type: string) { case 'images': for (let engine of engines) { try { - await window.pruneImages(engine.id); + await window.pruneImages(engine.id, selectedItemLabel === LABEL_IMAGE_UNUSED); } catch (error) { console.error(error); } diff --git a/tests/playwright/src/model/pages/images-page.ts b/tests/playwright/src/model/pages/images-page.ts index 6a4c05c34..7b07fbdc7 100644 --- a/tests/playwright/src/model/pages/images-page.ts +++ b/tests/playwright/src/model/pages/images-page.ts @@ -54,7 +54,7 @@ export class ImagesPage extends MainPage { exact: true, }); this.pruneConfirmationButton = this.page.getByRole('button', { - name: 'Yes', + name: 'All unused images', exact: true, }); this.loadImagesFromTarButton = this.additionalActions.getByLabel('Load Images', { exact: true }); @@ -128,7 +128,7 @@ export class ImagesPage extends MainPage { async pruneImages(): Promise { return test.step('Prune images', async () => { await this.pruneImagesButton.click(); - await handleConfirmationDialog(this.page, 'Prune'); + await handleConfirmationDialog(this.page, 'Prune', true, 'All unused images'); return this; }); }