diff --git a/.gitignore b/.gitignore index eae746c4d3..4b834d7f1d 100644 --- a/.gitignore +++ b/.gitignore @@ -80,5 +80,6 @@ dist/ /playwright-report/ /playwright/.cache/ /coverage +tests/screenshots haproxy-local.cfg diff --git a/README.md b/README.md index 7731463dc3..0e50ea5461 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,12 @@ LXD-UI is a single page application written in TypeScript and React. See [Archit | Create an instance | Instance list | Instance terminal | |-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------| -| ![0create](https://github.com/canonical/lxd-ui/assets/1155472/7f0c45a6-2ba2-4cc7-bd7c-c0ebca76d648) | ![1instance-overview](https://github.com/canonical/lxd-ui/assets/1155472/c71d2153-ea71-4ecb-ab25-fabcd6fb1e55) | ![2instance-term](https://github.com/canonical/lxd-ui/assets/1155472/c2b741e2-8806-4d4d-9a9a-f536f76a13b9) | +| ![0create](https://github.com/canonical/lxd-ui/assets/1155472/8c4f5eee-9d5a-40ca-93e1-57b1c393dbd9) | ![1instance-overview](https://github.com/canonical/lxd-ui/assets/1155472/af4a92ce-e562-43eb-945f-98b78b4bb03e) | ![2instance-term](https://github.com/canonical/lxd-ui/assets/1155472/14eaaffb-c770-4f34-936f-075ceb6be42e) | | Graphic console | Profile list | Cluster groups | |----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------| -| ![3-instance-console](https://github.com/canonical/lxd-ui/assets/1155472/0f8d742d-3f9c-4906-90da-e740e8ff353b) | ![profile-list](https://github.com/canonical/lxd-ui/assets/1155472/36a0f619-767f-4949-804d-061e5e28c87a) | ![6cluster](https://github.com/canonical/lxd-ui/assets/1155472/85f61ef9-a45f-4b4a-abee-8fa9dfa69bd2) | +| ![3-instance-console](https://github.com/canonical/lxd-ui/assets/1155472/e3301135-e737-4f7f-8bfb-1297135402a4) | ![profile-list](https://github.com/canonical/lxd-ui/assets/1155472/f19c3d70-5c25-47b0-9c8e-636bfa42fabe) | ![6cluster](https://github.com/canonical/lxd-ui/assets/1155472/85f61ef9-a45f-4b4a-abee-8fa9dfa69bd2) | | Storage | Operations | Warnings | |-------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------| -| ![5storage](https://github.com/canonical/lxd-ui/assets/1155472/38d7b8ab-d652-4c18-b71e-0098efe73702) | ![operations](https://github.com/canonical/lxd-ui/assets/1155472/d3168891-19fb-4724-95cb-9afc91191555) | ![warnings](https://github.com/canonical/lxd-ui/assets/1155472/56499dfc-15a2-4c59-8761-47709b4be957) | +| ![5storage](https://github.com/canonical/lxd-ui/assets/1155472/d78759a6-9e54-41d4-b9b9-f9905c550763) | ![operations](https://github.com/canonical/lxd-ui/assets/1155472/c8dfd5c8-5634-4d2e-9167-204c730df574) | ![warnings](https://github.com/canonical/lxd-ui/assets/1155472/a22334c8-61b8-4f8a-b49e-b4d4ad311285) | diff --git a/tests/helpers/instances.ts b/tests/helpers/instances.ts index 839140ff30..3801628f67 100644 --- a/tests/helpers/instances.ts +++ b/tests/helpers/instances.ts @@ -8,9 +8,11 @@ export const randomInstanceName = (): string => { export const createInstance = async ( page: Page, instance: string, - type = "container", + type: string = "container", + project: string = "default", + image: string = "alpine/3.19/cloud", ) => { - await page.goto("/ui/"); + await page.goto(`/ui/project/${project}`); await page .getByRole("link", { name: "Instances", exact: true }) .first() @@ -20,8 +22,8 @@ export const createInstance = async ( await page.getByLabel("Instance name").fill(instance); await page.getByRole("button", { name: "Browse images" }).click(); await page.getByPlaceholder("Search an image").click(); - await page.getByPlaceholder("Search an image").fill("alpine/3.19/cloud"); - await page.getByRole("button", { name: "Select" }).click(); + await page.getByPlaceholder("Search an image").fill(image); + await page.getByRole("button", { name: "Select" }).first().click(); await page .getByRole("combobox", { name: "Instance type" }) .selectOption(type); @@ -30,8 +32,12 @@ export const createInstance = async ( await page.waitForSelector(`text=Created instance ${instance}.`); }; -export const visitInstance = async (page: Page, instance: string) => { - await page.goto("/ui/"); +export const visitInstance = async ( + page: Page, + instance: string, + project: string = "default", +) => { + await page.goto(`/ui/project/${project}`); await page.getByPlaceholder("Search").click(); await page.getByPlaceholder("Search").fill(instance); await page.getByRole("link", { name: instance }).first().click(); @@ -49,8 +55,12 @@ export const saveInstance = async (page: Page, instance: string) => { await page.getByRole("button", { name: "Close notification" }).click(); }; -export const deleteInstance = async (page: Page, instance: string) => { - await visitInstance(page, instance); +export const deleteInstance = async ( + page: Page, + instance: string, + project: string = "default", +) => { + await visitInstance(page, instance, project); const stopButton = page.getByRole("button", { name: "Stop", exact: true }); if (await stopButton.isEnabled()) { await page.keyboard.down("Shift"); diff --git a/tests/helpers/profile.ts b/tests/helpers/profile.ts index 2b2bd4f0a8..deb456fc9f 100644 --- a/tests/helpers/profile.ts +++ b/tests/helpers/profile.ts @@ -5,13 +5,21 @@ export const randomProfileName = (): string => { return `playwright-profile-${randomNameSuffix()}`; }; -export const createProfile = async (page: Page, profile: string) => { - await startProfileCreation(page, profile); +export const createProfile = async ( + page: Page, + profile: string, + project: string = "default", +) => { + await startProfileCreation(page, profile, project); await finishProfileCreation(page, profile); }; -export const startProfileCreation = async (page: Page, profile: string) => { - await page.goto("/ui/"); +export const startProfileCreation = async ( + page: Page, + profile: string, + project: string = "default", +) => { + await page.goto(`/ui/project/${project}`); await page.getByRole("link", { name: "Profiles" }).click(); await page.getByRole("button", { name: "Create profile" }).click(); await page.getByLabel("Profile name").fill(profile); @@ -22,8 +30,12 @@ export const finishProfileCreation = async (page: Page, profile: string) => { await page.waitForSelector(`text=Profile ${profile} created.`); }; -export const deleteProfile = async (page: Page, profile: string) => { - await visitProfile(page, profile); +export const deleteProfile = async ( + page: Page, + profile: string, + project: string = "default", +) => { + await visitProfile(page, profile, project); await page.getByRole("button", { name: "Delete" }).click(); await page .getByRole("dialog", { name: "Confirm delete" }) @@ -32,8 +44,12 @@ export const deleteProfile = async (page: Page, profile: string) => { await page.waitForSelector(`text=Profile ${profile} deleted.`); }; -export const visitProfile = async (page: Page, profile: string) => { - await page.goto("/ui/"); +export const visitProfile = async ( + page: Page, + profile: string, + project: string = "default", +) => { + await page.goto(`/ui/project/${project}`); await page.getByRole("link", { name: "Profiles" }).click(); await page.getByPlaceholder("Search").click(); await page.getByPlaceholder("Search").fill(profile); diff --git a/tests/readme-screenshots.spec.ts b/tests/readme-screenshots.spec.ts new file mode 100644 index 0000000000..d0649723e2 --- /dev/null +++ b/tests/readme-screenshots.spec.ts @@ -0,0 +1,163 @@ +import { expect, test } from "./fixtures/lxd-test"; +import { + createInstance, + deleteInstance, + visitAndStartInstance, +} from "./helpers/instances"; +import { createProject, deleteProject } from "./helpers/projects"; +import { createProfile, deleteProfile, visitProfile } from "./helpers/profile"; + +test.beforeAll(() => { + test.skip( + Boolean(process.env.CI), + "This suite is only run manually to create screenshots for the readme file", + ); +}); + +test("instance creation screen", async ({ page }) => { + await page.goto("/ui/"); + await page.getByText("Instances", { exact: true }).click(); + await page.getByText("Create instance").click(); + await page.getByPlaceholder("Enter name").fill("comic-glider"); + await page.getByRole("button", { name: "Browse images" }).click(); + await page + .locator("tr") + .filter({ hasText: "Ubuntu24.04 LTS" }) + .first() + .getByRole("button") + .click(); + + await page.screenshot({ path: "tests/screenshots/create-instance.png" }); +}); + +test("instance list screen", async ({ page }) => { + await page.goto("/ui/"); + const project = "my-cluster"; + await createProject(page, project); + await visitProfile(page, "default", project); + await page.getByTestId("tab-link-Configuration").click(); + await page.getByRole("button", { name: "Edit profile" }).click(); + await page.getByText("Disk devices").click(); + await page.getByRole("button", { name: "Create override" }).click(); + await page.getByRole("button", { name: "Save changes" }).click(); + const instances = [ + "comic-glider", + "deciding-flounder", + "native-sailfish", + "precise-lacewing", + "ready-grizzly", + "singular-moose", + ]; + for (const instance of instances) { + await createInstance(page, instance, "container", project, "24.04"); + } + await page.goto(`/ui/project/${project}`); + await page + .getByRole("row", { + name: "Select comic-glider Name Type Description Status Actions", + }) + .getByLabel("Type") + .click(); + + await page.screenshot({ path: "tests/screenshots/instance-list.png" }); + + for (const instance of instances) { + await deleteInstance(page, instance, project); + } + await page.getByRole("link", { name: "Images", exact: true }).click(); + await page.getByRole("button", { name: "Delete", exact: true }).click(); + await page.getByText("Delete", { exact: true }).click(); + await deleteProject(page, project); +}); + +test("instance terminal screen", async ({ page }) => { + await page.goto("/ui/"); + const instance = "comic-glider"; + await createInstance(page, instance, "container", "default", "24.04"); + await visitAndStartInstance(page, instance); + await page.getByRole("button", { name: "Close notification" }).click(); + await page.getByTestId("tab-link-Terminal").click(); + await expect(page.getByText("~#")).toBeVisible(); + await page.waitForTimeout(1000); // ensure the terminal is ready + await page.keyboard.type("cd /"); + await page.keyboard.press("Enter"); + await page.keyboard.type("ll"); + await page.keyboard.press("Enter"); + await page.keyboard.type("cat /etc/issue"); + await page.keyboard.press("Enter"); + + await page.screenshot({ path: "tests/screenshots/instance-terminal.png" }); + + await deleteInstance(page, instance); +}); + +test("instance graphical console screen", async ({ page }) => { + await page.goto("/ui/"); + const instance = "upright-pangolin"; + await page.getByText("Instances", { exact: true }).click(); + await page.getByText("Create instance").click(); + await page.getByPlaceholder("Enter name").fill(instance); + await page.getByRole("button", { name: "Browse images" }).click(); + await page + .locator("tr") + .filter({ hasText: "ubuntu/24.04/desktop" }) + .first() + .getByRole("button") + .click(); + await page.getByRole("button", { name: "Create", exact: true }).click(); + await visitAndStartInstance(page, instance); + await page.getByRole("button", { name: "Close notification" }).click(); + await page.getByTestId("tab-link-Console").click(); + await page.waitForTimeout(40000); // ensure the vm is booted + + await page.screenshot({ + path: "tests/screenshots/instance-graphical-console.png", + }); + + await deleteInstance(page, instance); +}); + +test("profile list screen", async ({ page }) => { + await page.goto("/ui/"); + const project = "my-cluster"; + await createProject(page, project); + await createProfile(page, "small", project); + await createProfile(page, "medium", project); + await createProfile(page, "large", project); + await page.goto(`/ui/project/${project}/profiles`); + + await page.screenshot({ path: "tests/screenshots/profile-list.png" }); + + await deleteProfile(page, "small", project); + await deleteProfile(page, "medium", project); + await deleteProfile(page, "large", project); + await deleteProject(page, project); +}); + +test("storage pool screen", async ({ page }) => { + await page.goto("/ui/"); + await page.getByText("Storage").click(); + await page.getByText("Pools").click(); + await page.getByText("Created").first().click(); + + await page.screenshot({ path: "tests/screenshots/storage-pool-list.png" }); +}); + +test("operations screen", async ({ page }) => { + await page.goto("/ui/"); + await createInstance(page, "comic-glider"); + await page.getByRole("button", { name: "Close notification" }).click(); + await page.getByText("Operations").click(); + await page.getByText("Creating instance").first().click(); + + await page.screenshot({ path: "tests/screenshots/operations-list.png" }); + + await deleteInstance(page, "comic-glider"); +}); + +test("warnings screen", async ({ page }) => { + await page.goto("/ui/"); + await page.getByText("Warnings").click(); + + await page.screenshot({ path: "tests/screenshots/warnings-list.png" }); +});