playground: add test page for the install dialog

Test various install dialog scenarios in a controlled environment in
preparation for supporting and verifying our dnf5daemon backend
implementation.
This commit is contained in:
Jelle van der Waa 2025-09-10 19:43:59 +02:00 committed by Jelle van der Waa
parent 5d52d60799
commit 4b5c290f98
8 changed files with 177 additions and 23 deletions

View file

@ -28,6 +28,7 @@ const info = {
"playground/journal.jsx",
"playground/remote.tsx",
"playground/terminal.tsx",
"playground/packagemanager.tsx",
"selinux/selinux.js",
"shell/shell.jsx",
@ -122,6 +123,7 @@ const info = {
"playground/journal.html",
"playground/remote.html",
"playground/terminal.html",
"playground/packagemanager.html",
"selinux/index.html",

View file

@ -82,6 +82,9 @@ QUnit.test("simple request", assert => {
},
test: {
label: "Playground"
},
packagemanager: {
label: "PackageManager (install dialog)"
}
},
preload: ["preloaded"],

View file

@ -47,6 +47,9 @@
},
"terminal": {
"label": "Terminal"
},
"packagemanager": {
"label": "PackageManager (install dialog)"
}
},
"preload": [ "preloaded" ],

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Cockpit Package Manager Integration</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="packagemanager.css" type="text/css" rel="stylesheet" />
<script src="../base1/cockpit.js"></script>
<script src="packagemanager.js"></script>
</head>
<body class="pf-v6-m-tabular-nums">
<div id="packagemanager"></div>
</body>
</html>

View file

@ -0,0 +1,50 @@
import React from 'react';
import { createRoot } from "react-dom/client";
import '../lib/patternfly/patternfly-6-cockpit.scss';
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Card, CardBody, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
import { Content } from "@patternfly/react-core/dist/esm/components/Content/index.js";
import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { install_dialog } from "../lib/cockpit-components-install-dialog.jsx";
import 'cockpit-dark-theme'; // once per page
import 'page.scss';
const PackageManagerPage = () => {
const [isInstalling, setIsInstalling] = React.useState(false);
const [installPackages, setInstallPackages] = React.useState("");
const openInstallDialog = async () => {
setIsInstalling(true);
try {
await install_dialog(installPackages.split(","));
} finally {
setIsInstalling(false);
}
};
return (
<Page isContentFilled className="no-masthead-sidebar">
<PageSection hasBodyWrapper={false}>
<Content>
<Card>
<CardTitle id="install-card-title">Install dialog test</CardTitle>
<CardBody>
<TextInput id="install-packages" value={installPackages} onChange={(_event, value) => setInstallPackages(value)} />
<Button id="install-button" variant="primary" onClick={() => openInstallDialog()} isLoading={isInstalling}>Install Package</Button>
</CardBody>
</Card>
</Content>
</PageSection>
</Page>
);
};
document.addEventListener("DOMContentLoaded", async () => {
const root = createRoot(document.getElementById("packagemanager")!);
root.render(<PackageManagerPage />);
});

View file

@ -18,7 +18,6 @@
import logging
import os
import re
import textwrap
from collections.abc import Mapping
from testlib import MachineCase
@ -153,32 +152,31 @@ Server = file://{empty_repo_dir}
def createPackage(self, name: str, version: str, release: str, *, install: bool = False,
postinst: str | None = None, depends: str = "",
content: Mapping[str, Mapping[str, str] | str] | None = None,
arch: str | None = None, provides: str | None = None, **updateinfo: str | list[str]) -> None:
arch: str | None = None, provides: str | None = None,
conflicts: str | None = None,
**updateinfo: str | list[str]) -> None:
"""Create a dummy package in repo_dir on self.machine
If install is True, install the package. Otherwise, update the package
index in repo_dir.
"""
if provides:
provides = f"Provides: {provides}"
else:
provides = ""
if self.backend == "apt":
self.createDeb(name, version + '-' + release, depends, postinst, install=install, content=content,
arch=arch, provides=provides)
arch=arch, provides=provides, conflicts=conflicts)
elif self.backend == "alpm":
self.createPacmanPkg(name, version, release, depends, postinst, install=install, content=content,
arch=arch, provides=provides)
arch=arch, provides=provides, conflicts=conflicts)
else:
self.createRpm(name, version, release, depends, postinst, install=install, content=content,
arch=arch, provides=provides)
arch=arch, provides=provides, conflicts=conflicts)
if updateinfo:
self.updateInfo[name, version, release] = updateinfo
def createDeb(self, name: str, version: str, depends: str, postinst: str | None = None, *, install: bool,
content: Mapping[str, Mapping[str, str] | str] | None = None,
arch: str | None = None, provides: str | None = None) -> None:
arch: str | None = None, provides: str | None = None,
conflicts: str | None = None) -> None:
"""Create a dummy deb in repo_dir on self.machine
If install is True, install the package. Otherwise, update the package
@ -186,6 +184,12 @@ Server = file://{empty_repo_dir}
"""
m = self.machine
extra_fields = ""
if provides:
extra_fields += f"Provides: {provides}\n"
if conflicts:
extra_fields += f"Conflicts: {conflicts}\n"
if arch is None:
arch = self.primary_arch
deb = f"{self.repo_dir}/{name}_{version}_{arch}.deb"
@ -202,17 +206,17 @@ Server = file://{empty_repo_dir}
else:
m.write(dest, data)
m.execute(f"mkdir -p {self.repo_dir}")
m.write("/tmp/b/DEBIAN/control", textwrap.dedent(f"""
Package: {name}
Version: {version}
Priority: optional
Section: test
Maintainer: foo
Depends: {depends}
Architecture: {arch}
Description: dummy {name}
{provides}
"""))
m.write("/tmp/b/DEBIAN/control", f"""
Package: {name}
Version: {version}
Priority: optional
Section: test
Maintainer: foo
Depends: {depends}
Architecture: {arch}
Description: dummy {name}
{extra_fields}
""")
cmd = f"""set -e
{postinstcode}
@ -229,7 +233,8 @@ Server = file://{empty_repo_dir}
install: bool,
content: Mapping[str, Mapping[str, str] | str] | None = None,
arch: str | None = None,
provides: str | None = None) -> None:
provides: str | None = None,
conflicts: str | None = None) -> None:
"""Create a dummy rpm in repo_dir on self.machine
If install is True, install the package. Otherwise, update the package
@ -241,6 +246,10 @@ Server = file://{empty_repo_dir}
postcode = ''
if requires:
requires = f"Requires: {requires}\n"
conflicts = f"Conflicts: {conflicts}\n" if conflicts is not None else ""
provides = f"Provides: {provides}\n" if provides is not None else ""
if arch is None:
arch = self.primary_arch
installcmds = f"touch $RPM_BUILD_ROOT/stamp-{name}-{version}-{release}\n"
@ -266,6 +275,7 @@ License: BSD
{provides}
{architecture}
{requires}
{conflicts}
%define _build_id_links none
@ -296,7 +306,8 @@ rm -rf ~/rpmbuild
install: bool,
content: Mapping[str, Mapping[str, str] | str] | None = None,
arch: str | None = None,
provides: str | None = None) -> None:
provides: str | None = None,
conflicts: str | None = None) -> None:
"""Create a dummy pacman package in repo_dir on self.machine
If install is True, install the package. Otherwise, update the package
@ -306,6 +317,9 @@ rm -rf ~/rpmbuild
if arch is None:
arch = 'any'
conflicts = f'conflicts=("{conflicts}")' if conflicts else ''
provides = f'provides=("{provides}")' if provides else ''
sources = ""
installcmds = 'package() {\n'
if content is not None:
@ -339,6 +353,8 @@ pkgdesc="dummy {name}"
pkgrel={release}
arch=({arch})
depends=({requires})
{conflicts}
{provides}
options=(!debug)
{sources}

View file

@ -1841,5 +1841,70 @@ WantedBy=basic.target
b.wait_visible("#automatic-updates-dialog")
@testlib.skipOstree("Image uses OSTree")
@testlib.nondestructive
class TestInstallDialog(NoSubManCase):
def show_install_dialog(self, *packages: str):
b = self.browser
b.set_input_text("#install-packages", ",".join(packages))
b.click("#install-button")
b.wait_in_text("#dialog", "Install software")
def install_packages(self):
b = self.browser
b.click("#dialog button:contains('Install')")
# assert some progress
b.wait_not_present("#dialog")
def testBasic(self):
b = self.browser
m = self.machine
# dummy package
self.createPackage("dummy", "1", "1")
# package which depends on another package
self.createPackage("test-webserver", "998", "1", depends="test-webengine")
self.createPackage("test-webengine", "998", "1")
# package which conflicts with another package
self.createPackage("ok-software", "998", "1", install=True)
self.createPackage("breaking-software", "999", "1", conflicts="ok-software")
# Test error condition, package not being available yet
self.login_and_go('/playground/packagemanager')
b.wait_in_text("#install-card-title", "Install dialog test")
self.show_install_dialog("dummy")
b.wait_in_text("#dialog .pf-v6-c-alert__title", "dummy is not available from any repository.")
b.wait_visible("#dialog button.pf-m-primary:disabled")
b.click("#dialog button.cancel")
b.wait_not_present("#dialog")
# Install dummy which is now available
# Also works around https://github.com/PackageKit/PackageKit/issues/893
self.enableRepo()
if self.backend != "dnf5":
m.execute("pkcon refresh force")
self.show_install_dialog("dummy")
b.wait_in_text("#dialog", "dummy will be installed.")
self.install_packages()
# Test that additional installed packages are shown in the dialog
b.wait_in_text("#install-card-title", "Install dialog test")
self.show_install_dialog("test-webserver")
b.wait_in_text("#dialog", "test-webserver will be installed.")
b.wait_text("#dialog .scale-up-ct", "Additional packages:test-webengine")
self.install_packages()
# Test conflicting packages are shown as removed
self.show_install_dialog("breaking-software")
b.wait_in_text("#dialog", "breaking-software will be installed.")
# The svg icon creates a tiny bit of whitespace
b.wait_text("#dialog .scale-up-ct", " Removals:ok-software")
self.install_packages()
if __name__ == '__main__':
testlib.test_main()

View file

@ -332,6 +332,7 @@ class TestStoragePackagesNFS(packagelib.PackageCase, storagelib.StorageCase):
# anything on the Debian test images, for unknown reasons. So
# we install a dummy package to warm up all parts of the
# machinery and distribute the fluids evenly.
# https://github.com/PackageKit/PackageKit/issues/893
#
if "debian" in m.image or "ubuntu" in m.image:
m.execute("pkcon refresh; pkcon install -y dummy")