diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4a12ec60..165bdfd2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -71,9 +71,10 @@ jobs: sed -i "s#version\":\ \"\(.*\)\",#version\":\ \"${{ steps.TAG_UTIL.outputs.extVersion }}\",#g" packages/extension/package.json sed -i "s#version\":\ \"\(.*\)\",#version\":\ \"${{ steps.TAG_UTIL.outputs.extVersion }}\",#g" packages/rpc/package.json sed -i "s#version\":\ \"\(.*\)\",#version\":\ \"${{ steps.TAG_UTIL.outputs.extVersion }}\",#g" packages/webview/package.json + sed -i "s#version\":\ \"\(.*\)\",#version\":\ \"${{ steps.TAG_UTIL.outputs.extVersion }}\",#g" tests/playwright/package.json sed -i "s#\(ghcr.io/podman-desktop/podman-desktop-extension-kubernetes-dashboard-builder:\)next#\1${{ steps.TAG_UTIL.outputs.extVersion }}#g" build/Containerfile - git add package.json packages/api/package.json packages/channels/package.json packages/extension/package.json packages/rpc/package.json packages/webview/package.json build/Containerfile + git add package.json packages/api/package.json packages/channels/package.json packages/extension/package.json packages/rpc/package.json packages/webview/package.json tests/playwright/package.json build/Containerfile # commit the changes git commit -m "chore: 🥁 tagging ${{ steps.TAG_UTIL.outputs.githubTag }} 🥳" @@ -107,9 +108,10 @@ jobs: sed -i "s#version\":\ \"\(.*\)\",#version\":\ \"${bumpedVersion}-next\",#g" packages/extension/package.json sed -i "s#version\":\ \"\(.*\)\",#version\":\ \"${bumpedVersion}-next\",#g" packages/rpc/package.json sed -i "s#version\":\ \"\(.*\)\",#version\":\ \"${bumpedVersion}-next\",#g" packages/webview/package.json + sed -i "s#version\":\ \"\(.*\)\",#version\":\ \"${bumpedVersion}-next\",#g" tests/playwright/package.json # put back next as the version in the Containerfile sed -i "s|\(ghcr.io/podman-desktop/podman-desktop-extension-kubernetes-dashboard-builder:\)[^ ]*|\1next|" build/Containerfile - git add package.json packages/api/package.json packages/channels/package.json packages/extension/package.json packages/rpc/package.json packages/webview/package.json build/Containerfile + git add package.json packages/api/package.json packages/channels/package.json packages/extension/package.json packages/rpc/package.json packages/webview/package.json tests/playwright/package.json build/Containerfile git commit -s --amend -m "chore: bump version to ${bumpedVersion}" git push origin "${bumpedBranchName}" diff --git a/build/Containerfile b/build/Containerfile index 790c4028..4bf930e0 100644 --- a/build/Containerfile +++ b/build/Containerfile @@ -54,7 +54,7 @@ RUN echo "node-linker=hoisted" >> /opt/app-root/extension/.npmrc RUN eval 'dep_version() { pnpm list -P $1 --json --depth 0 |jq -r ".[0].dependencies.\"$1\".version"; };' && \ ISOMORPHIC_WS_VERSION=$(dep_version "isomorphic-ws") && \ echo adding isomorphic-ws version ${ISOMORPHIC_WS_VERSION} && \ - pnpm --dir /opt/app-root/extension add isomorphic-ws@${ISOMORPHIC_WS_VERSION} --prod + pnpm --dir /opt/app-root/extension --workspace-root add isomorphic-ws@${ISOMORPHIC_WS_VERSION} --prod # Copy the extension to a new image FROM scratch diff --git a/package.json b/package.json index 3a0fd835..40b75baa 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "test:webview": "vitest run --project webview --passWithNoTests", "test:channels": "vitest run --project channels --passWithNoTests", "test": "pnpm run test:extension && pnpm run test:webview && pnpm run test:channels", + "test:e2e": "cd tests/playwright && npm run test:e2e", + "test:e2e:smoke": "cd tests/playwright && npm run test:e2e:smoke", "typecheck:rpc": "tsc --noEmit --project packages/rpc", "typecheck:channels": "tsc --noEmit --project packages/channels", "typecheck:webview": "tsc --noEmit --project packages/webview", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7bd0a3b..f2100cee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -353,6 +353,24 @@ importers: specifier: ^3 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(yaml@2.8.2) + tests/playwright: + devDependencies: + '@playwright/test': + specifier: ^1.57.0 + version: 1.57.0 + '@podman-desktop/tests-playwright': + specifier: ^1.23.1 + version: 1.23.1 + '@types/node': + specifier: ^24 + version: 24.10.1 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + xvfb-maybe: + specifier: ^0.2.1 + version: 0.2.1 + packages: '@acemir/cssom@0.9.23': @@ -900,9 +918,17 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.57.0': + resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + engines: {node: '>=18'} + hasBin: true + '@podman-desktop/api@1.23.1': resolution: {integrity: sha512-Z3cPF6l7YrydDxzXynsQc0pHo2ooopPqE3IM5EgVQq4/3B9ZBmPIo8P7eLMor44kFnLmQgAzUnX2AbUaXdPeqg==} + '@podman-desktop/tests-playwright@1.23.1': + resolution: {integrity: sha512-ZurtyNGE9zHGP/6ni0kdB/P7/7mBmowzGcGOTViW+IN2gTUsCZqPHTmwI+6WNEzQpiqXWZAT9cGSnIPUsasfaw==} + '@podman-desktop/ui-svelte@1.23.1': resolution: {integrity: sha512-WnwwJ3S0L6PMT569HH4wI6chLgLMrxa3XLLR7uSXDDaD6wyYUKQvmIk5Cna+5t4dBPI2RqB1drBm19NN9ihLZQ==} peerDependencies: @@ -1937,6 +1963,14 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2407,6 +2441,11 @@ packages: resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3141,6 +3180,9 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3314,6 +3356,16 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.57.0: + resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -4157,6 +4209,10 @@ packages: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -4206,6 +4262,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xvfb-maybe@0.2.1: + resolution: {integrity: sha512-9IyRz3l6Qyhl6LvnGRF5jMPB4oBEepQnuzvVAFTynP6ACLLSevqigICJ9d/+ofl29m2daeaVBChnPYUnaeJ7yA==} + hasBin: true + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4768,8 +4828,14 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.57.0': + dependencies: + playwright: 1.57.0 + '@podman-desktop/api@1.23.1': {} + '@podman-desktop/tests-playwright@1.23.1': {} + '@podman-desktop/ui-svelte@1.23.1(svelte-fa@4.0.4(svelte@5.45.5))(svelte@5.45.5)': dependencies: '@floating-ui/dom': 1.7.4 @@ -5135,7 +5201,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3) '@typescript-eslint/types': 8.48.1 - debug: 4.4.0 + debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5836,6 +5902,10 @@ snapshots: de-indent@1.0.2: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@3.2.7: dependencies: ms: 2.1.3 @@ -6478,6 +6548,9 @@ snapshots: dependencies: minipass: 7.1.2 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -7273,6 +7346,8 @@ snapshots: mri@1.2.0: {} + ms@2.0.0: {} + ms@2.1.3: {} muggle-string@0.4.1: {} @@ -7443,6 +7518,14 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 + playwright-core@1.57.0: {} + + playwright@1.57.0: + dependencies: + playwright-core: 1.57.0 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} possible-typed-array-names@1.1.0: {} @@ -8351,6 +8434,10 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -8386,6 +8473,13 @@ snapshots: xmlchars@2.2.0: {} + xvfb-maybe@0.2.1: + dependencies: + debug: 2.6.9 + which: 1.3.1 + transitivePeerDependencies: + - supports-color + y18n@5.0.8: {} yallist@4.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 61b3ebde..387a0ee6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - packages/* + - tests/* onlyBuiltDependencies: - esbuild diff --git a/tests/playwright/package.json b/tests/playwright/package.json new file mode 100644 index 00000000..5baf4c91 --- /dev/null +++ b/tests/playwright/package.json @@ -0,0 +1,20 @@ +{ + "name": "kubernetes-dashboard-tests-playwright", + "version": "0.2.0-next", + "description": "Kubernetes Dashboard extension Playwright E2E tests", + "scripts": { + "test:e2e:setup": "xvfb-maybe --auto-servernum --server-args='-screen 0 1280x960x24' --", + "test:e2e": "npm run test:e2e:setup playwright test src/", + "test:e2e:smoke": "npm run test:e2e:setup playwright test src/ --grep @smoke" + }, + "publisher": "redhat", + "license": "Apache-2.0", + "devDependencies": { + "@playwright/test": "^1.57.0", + "@podman-desktop/tests-playwright": "^1.23.1", + "@types/node": "^24", + "typescript": "^5.9.3", + "xvfb-maybe": "^0.2.1" + }, + "type": "module" +} diff --git a/tests/playwright/playwright.config.ts b/tests/playwright/playwright.config.ts new file mode 100644 index 00000000..0929ad67 --- /dev/null +++ b/tests/playwright/playwright.config.ts @@ -0,0 +1,40 @@ +/********************************************************************** + * Copyright (C) 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. + * 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 { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + outputDir: './output/', + workers: 1, + + reporter: [ + ['list'], + ['junit', { outputFile: './output/junit-results.xml' }], + ['json', { outputFile: './output/json-results.json' }], + ['html', { open: 'never', outputFolder: './output/html-results/' }], + ], + + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], +}); diff --git a/tests/playwright/src/extension.spec.ts b/tests/playwright/src/extension.spec.ts new file mode 100644 index 00000000..b501eef0 --- /dev/null +++ b/tests/playwright/src/extension.spec.ts @@ -0,0 +1,113 @@ +/********************************************************************** + * Copyright (C) 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. + * 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 type { ExtensionsPage } from '@podman-desktop/tests-playwright'; +import { + expect as playExpect, + test, + RunnerOptions, + waitForPodmanMachineStartup, +} from '@podman-desktop/tests-playwright'; +import { KubernetesDashboardDetailsPage } from './model/kd-details-page'; + +const EXTENSION_OCI_IMAGE = + process.env.EXTENSION_OCI_IMAGE ?? 'ghcr.io/podman-desktop/podman-desktop-extension-kubernetes-dashboard:latest'; +const EXTENSION_PREINSTALLED: boolean = process.env.EXTENSION_PREINSTALLED === 'true'; +const CATALOG_EXTENSION_LABEL: string = 'redhat.kubernetes-dashboard'; +const CATALOG_EXTENSION_NAME: string = 'Kubernetes Dashboard'; +const CATALOG_STATUS_ACTIVE: string = 'ACTIVE'; + +test.use({ + runnerOptions: new RunnerOptions({ + customFolder: 'kubernetes-dashboard-tests', + /** + * For performance reasons, disable extensions which are not necessary for the e2e + */ + customSettings: { + 'extensions.disabled': [ + 'podman-desktop.compose', + 'podman-desktop.docker', + 'podman-desktop.kind', + 'podman-desktop.kube-context', + 'podman-desktop.kubectl-cli', + 'podman-desktop.lima', + 'podman-desktop.minikube', + 'podman-desktop.registries', + ], + }, + }), +}); + +test.beforeAll(async ({ runner, welcomePage, page }) => { + test.setTimeout(80_000); + + runner.setVideoAndTraceName('kubernetes-dashboard-e2e'); + await welcomePage.handleWelcomePage(true); + await waitForPodmanMachineStartup(page, 80_000); // default is 30s let's increase that to 80s +}); + +test.afterAll(async ({ runner }) => { + test.setTimeout(200_000); + await runner.close(); +}); + +test.describe.serial(`Extension installation and verification`, { tag: '@smoke' }, () => { + test.describe.serial(`Extension installation`, () => { + let extensionsPage: ExtensionsPage; + + test(`Open Settings -> Extensions page`, async ({ navigationBar }) => { + const dashboardPage = await navigationBar.openDashboard(); + await playExpect(dashboardPage.mainPage).toBeVisible(); + extensionsPage = await navigationBar.openExtensions(); + await playExpect(extensionsPage.header).toBeVisible(); + }); + + test(`Install extension`, async () => { + test.skip(EXTENSION_PREINSTALLED, 'Extension is preinstalled'); + await extensionsPage.installExtensionFromOCIImage(EXTENSION_OCI_IMAGE); + }); + + test('Extension (card) is installed, present and active', async ({ navigationBar }) => { + const extensions = await navigationBar.openExtensions(); + await playExpect + .poll(async () => await extensions.extensionIsInstalled(CATALOG_EXTENSION_LABEL), { + timeout: 30000, + }) + .toBeTruthy(); + const extensionCard = await extensions.getInstalledExtension(CATALOG_EXTENSION_NAME, CATALOG_EXTENSION_LABEL); + await playExpect(extensionCard.status).toHaveText(CATALOG_STATUS_ACTIVE); + }); + + test(`Extension's details show correct status, no error`, async ({ page, navigationBar }) => { + const extensions = await navigationBar.openExtensions(); + const extensionCard = await extensions.getInstalledExtension('kubernetes-dashboard', CATALOG_EXTENSION_LABEL); + await extensionCard.openExtensionDetails(CATALOG_EXTENSION_NAME); + const details = new KubernetesDashboardDetailsPage(page); + await playExpect(details.heading).toBeVisible(); + await playExpect(details.status).toHaveText(CATALOG_STATUS_ACTIVE); + const errorTab = details.tabs.getByRole('button', { name: 'Error' }); + // we would like to propagate the error's stack trace into test failure message + let stackTrace = ''; + if ((await errorTab.count()) > 0) { + await details.activateTab('Error'); + stackTrace = await details.errorStackTrace.innerText(); + } + await playExpect(errorTab, `Error Tab was present with stackTrace: ${stackTrace}`).not.toBeVisible(); + }); + }); +}); diff --git a/tests/playwright/src/model/kd-details-page.ts b/tests/playwright/src/model/kd-details-page.ts new file mode 100644 index 00000000..cca4e990 --- /dev/null +++ b/tests/playwright/src/model/kd-details-page.ts @@ -0,0 +1,26 @@ +/********************************************************************** + * Copyright (C) 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. + * 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 type { Page } from '@playwright/test'; +import { ExtensionDetailsPage } from '@podman-desktop/tests-playwright'; + +export class KubernetesDashboardDetailsPage extends ExtensionDetailsPage { + constructor(page: Page) { + super(page, 'Kubernetes Dashboard'); + } +} diff --git a/tests/playwright/tsconfig.json b/tests/playwright/tsconfig.json new file mode 100644 index 00000000..fb981316 --- /dev/null +++ b/tests/playwright/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution":"node", + "strict": true, + "preserveValueImports": false, + "skipLibCheck": false, + "baseUrl": ".", + }, + "include": ["src/**/*.ts", "playwright.config.ts"], + "exclude": ["node_modules/**", "output/**", "tests/**"] +}