diff --git a/.github/workflows/pr-check.yaml b/.github/workflows/pr-check.yaml index 3329426..af850fd 100644 --- a/.github/workflows/pr-check.yaml +++ b/.github/workflows/pr-check.yaml @@ -151,7 +151,7 @@ jobs: SKIP_INSTALLATION: true steps: - uses: actions/checkout@v4 - with: + with: path: podman-desktop-sandbox-ext # Install nodejs @@ -195,7 +195,7 @@ jobs: - name: Execute yarn in Sandbox extension working-directory: ./podman-desktop-sandbox-ext run: yarn install - + - name: Build Sandbox extension from container file working-directory: ./podman-desktop-sandbox-ext run: | @@ -211,7 +211,7 @@ jobs: working-directory: ./podman-desktop-sandbox-ext run: | rm -rf tests/playwright/output/sandbox-tests-pd/plugins/extension/node_modules/electron/dist/resources - + - name: Run E2E tests working-directory: ./podman-desktop-sandbox-ext env: @@ -222,4 +222,4 @@ jobs: if: always() with: name: e2e-tests - path: ./**/tests/**/output/ \ No newline at end of file + path: ./**/tests/**/output/ diff --git a/tests/src/developer-sandbox.spec.ts b/tests/src/developer-sandbox.spec.ts index c13e77f..0e36c5f 100644 --- a/tests/src/developer-sandbox.spec.ts +++ b/tests/src/developer-sandbox.spec.ts @@ -23,8 +23,17 @@ import { ExtensionCardPage, RunnerOptions, test, + ResourceConnectionCardPage, + startChromium, + findPageWithTitleInBrowser, + ConfirmInputValue, + KubeContextPage, + performBrowserLogin, } from '@podman-desktop/tests-playwright'; import { DeveloperSandboxPage } from './model/pages/developer-sandbox-page'; +import { CreateResourcePage } from './model/pages/create-resource-page'; +import type { Browser, BrowserContext, Page } from '@playwright/test'; +import path, { join } from 'node:path'; let extensionInstalled = false; let extensionCard: ExtensionCardPage; @@ -37,6 +46,12 @@ const activeExtensionStatus = 'ACTIVE'; const disabledExtensionStatus = 'DISABLED'; const activeConnectionStatus = 'RUNNING'; const skipInstallation = process.env.SKIP_INSTALLATION === 'true'; +let browserOutputPath: string; +let loginCommand = ''; +const resourceCardLabel = 'redhat.sandbox'; +const resourceName = 'Developer Sandbox'; +const contextName = 'dev-sandbox-context-3'; +const chromePort = '9222'; test.use({ runnerOptions: new RunnerOptions({ customFolder: 'sandbox-tests-pd', autoUpdate: false, autoCheckUpdates: false }), @@ -45,6 +60,8 @@ test.beforeAll(async ({ runner, page, welcomePage }) => { runner.setVideoAndTraceName('sandbox-e2e'); await welcomePage.handleWelcomePage(true); extensionCard = new ExtensionCardPage(page, extensionLabelName, extensionLabel); + browserOutputPath = test.info().project.outputDir; + console.log(`Saving browser test artifacts to: '${browserOutputPath}'`); }); test.afterAll(async ({ runner }) => { @@ -140,6 +157,168 @@ test.describe.serial('Red Hat Developer Sandbox extension verification', () => { }); }); + test.describe.serial('Developer Sandbox cluster verification', async () => { + test.describe.serial('Fetch login command via browser', async () => { + let chromiumPage: Page | undefined; + let browser: Browser | undefined; + let context: BrowserContext | undefined; + + test.afterAll(async () => { + if (browser) { + console.log('Stopping tracing and closing browser...'); + await context?.tracing.stop({ + path: join(path.join(browserOutputPath), 'traces', 'browser-sandbox-trace.zip'), + }); + if (chromiumPage) { + await chromiumPage.close(); + } + await browser.close(); + } + }); + + test('Open Developer Sandbox page in browser', async ({ navigationBar, page }) => { + test.setTimeout(120_000); + //get sandbox url + const settingsBar = await navigationBar.openSettings(); + await settingsBar.resourcesTab.click(); + const resourcesPage = new ResourcesPage(page); + playExpect(await resourcesPage.resourceCardIsVisible(resourceCardLabel)).toBeTruthy(); + const createNewSandboxButton = page.getByRole('button', { name: `Create new ${resourceName}` }); //goToCreateNewResourcePage only takes 1 argument + await createNewSandboxButton.click(); + const createResourcePage = new CreateResourcePage(page); + await createResourcePage.logIntoSandboxButton.click(); + const websiteDialog = page.getByRole('dialog', { name: 'Open External Website' }); + await playExpect(websiteDialog).toBeVisible(); + const sandboxUrl = await websiteDialog.getByLabel('Dialog Details').textContent(); + const cancelDialogButton = websiteDialog.getByRole('button', { name: 'Cancel' }); + await cancelDialogButton.click(); + + //open the website + if (sandboxUrl) { + browser = await startChromium(chromePort, path.join(browserOutputPath)); + context = await browser.newContext(); + await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); + const newPage = await context.newPage(); + await newPage.goto(sandboxUrl); + await newPage.waitForURL(/developers.redhat.com/); + chromiumPage = newPage; + if (browser) { + await findPageWithTitleInBrowser(browser, 'Developer Sandbox | Red Hat Developer'); + } + console.log(`Found page with title: ${await chromiumPage?.title()}`); + } else { + throw new Error('Did not find Developer Sandbox page'); + } + }); + test('Log into Red Hat Sandbox', async () => { + //go to login page + playExpect(chromiumPage).toBeDefined(); + if (!chromiumPage) { + throw new Error('Chromium browser page was not initialized'); + } + await chromiumPage.bringToFront(); + console.log(`Switched to Chrome tab with title: ${await chromiumPage.title()}`); + const startSandboxButton = chromiumPage.getByRole('button', { name: 'Start your sandbox for free' }); + await playExpect(startSandboxButton).toBeVisible(); + await startSandboxButton.click(); + + //log in, same tab + const usernameAction: ConfirmInputValue = { + inputLocator: chromiumPage.getByRole('textbox', { name: 'username' }), + inputValue: process.env.DVLPR_USERNAME ?? 'unknown', + confirmLocator: chromiumPage.getByRole('button', { name: 'Next' }), + }; + const passwordAction: ConfirmInputValue = { + inputLocator: chromiumPage.getByRole('textbox', { name: 'password' }), + inputValue: process.env.DVLPR_PASSWORD ?? 'unknown', + confirmLocator: chromiumPage.getByRole('button', { name: 'Log in' }), + }; + const usernameBox = chromiumPage.getByRole('textbox', { name: 'Red Hat login' }); + await playExpect(usernameBox).toBeVisible({ timeout: 5_000 }); + await usernameBox.focus(); + + //after login redirect twice to sandbox.redhat.com, same tab + await performBrowserLogin(chromiumPage, /Log In/, usernameAction, passwordAction, async chromiumPage => { + playExpect(chromiumPage).toBeDefined(); + if (!chromiumPage) { + throw new Error('Chromium browser page was not initialized'); + } + playExpect(await chromiumPage.title()).toBe('Developer Sandbox | Developer Sandbox'); + await chromiumPage.screenshot({ + path: join(path.join(browserOutputPath), 'screenshots', 'after_login_in_browser.png'), + type: 'png', + fullPage: true, + }); + }); + }); + test('Fetch the login command', async () => { + //open "try it" openshift + playExpect(chromiumPage).toBeDefined(); + if (!chromiumPage) { + throw new Error('Chromium browser page was not initialized'); + } + await chromiumPage.bringToFront(); + const openshiftBoxLabel = chromiumPage.getByAltText('Openshift', { exact: true }); + await playExpect(openshiftBoxLabel).toBeVisible(); + const openshiftBox = openshiftBoxLabel.locator('..').locator('..').locator('..'); + const tryItButton = openshiftBox.getByRole('button', { name: 'Try it' }); + await playExpect(tryItButton).toBeVisible(); + await tryItButton.click(); + + //new tab, log in through the Openshift auth page (sometimes might need reload) + await loginThroughOpenshiftServicePage(browser!, chromiumPage); + + //same tab, get login command from the Console Openshift page + const userDropdownMenuButton = chromiumPage.getByRole('button', { name: 'User menu' }); + await playExpect(userDropdownMenuButton).toBeVisible({ timeout: 50_000 }); + await userDropdownMenuButton.click(); + const copyLoginCommandButton = chromiumPage.getByText('Copy login command'); + await playExpect(copyLoginCommandButton).toBeVisible(); + await copyLoginCommandButton.click(); + + //new tab, find command (sandbox login might need reload) + await loginThroughOpenshiftServicePage(browser!, chromiumPage); + + const displayTokenButton = chromiumPage.getByRole('button', { name: 'Display Token' }); + await playExpect(displayTokenButton).toBeVisible(); + await displayTokenButton.click(); + const commandElement = chromiumPage.getByText('oc login').locator('..'); + await playExpect(commandElement).toBeVisible(); + loginCommand = await commandElement.innerText(); + }); + }); + + test('Create Sandbox cluster', async ({ page }) => { + await page.bringToFront(); + const createResourcePage = new CreateResourcePage(page); + await createResourcePage.createResource(loginCommand, contextName); + }); + + test('Verify Sandbox cluster and context', async ({ page, navigationBar }) => { + const sandboxClusterCard = new ResourceConnectionCardPage(page, resourceCardLabel, contextName); + playExpect(await sandboxClusterCard.doesResourceElementExist()).toBeTruthy(); + await playExpect(sandboxClusterCard.resourceElementConnectionStatus).toHaveText('RUNNING'); + + const settingsBar = await navigationBar.openSettings(); + await settingsBar.kubernetesTab.click(); + const kubeContextPage = new KubeContextPage(page); + playExpect(await kubeContextPage.pageIsEmpty()).not.toBeTruthy(); + playExpect(await kubeContextPage.isContextReachable(contextName)).toBeTruthy(); + playExpect(await kubeContextPage.isContextDefault(contextName)).not.toBeTruthy(); + }); + + test('Delete remote cluster context', async ({ page, navigationBar }) => { + const kubeContextPage = new KubeContextPage(page); + await kubeContextPage.deleteContext(contextName); + playExpect(await kubeContextPage.pageIsEmpty()).toBeTruthy(); + + const settingsBar = await navigationBar.openSettings(); + await settingsBar.resourcesTab.click(); + const sandboxClusterCard = new ResourceConnectionCardPage(page, resourceCardLabel, contextName); + playExpect(await sandboxClusterCard.doesResourceElementExist()).not.toBeTruthy(); + }); + }); + test('Extension can be removed', async ({ navigationBar }) => { await removeExtension(navigationBar); }); @@ -184,3 +363,14 @@ async function checkSandboxInDashboard(navigationBar: NavigationBar, isInstalled await playExpect(sandboxProviderCard).toBeHidden(); } } + +async function loginThroughOpenshiftServicePage(browser: Browser, chromiumPage: Page) { + let loginSandboxPage = await findPageWithTitleInBrowser(browser!, 'Login - Red Hat OpenShift Service on AWS'); + if (!loginSandboxPage) { + throw new Error('Sandbox service login browser page was not initialized'); + } + await loginSandboxPage.bringToFront(); + const loginWithSandboxButton = chromiumPage.getByRole('button', { name: 'Log in with DevSandbox' }); + await playExpect(loginWithSandboxButton).toBeVisible(); + await loginWithSandboxButton.click(); +} diff --git a/tests/src/model/pages/create-resource-page.ts b/tests/src/model/pages/create-resource-page.ts new file mode 100644 index 0000000..f07a55d --- /dev/null +++ b/tests/src/model/pages/create-resource-page.ts @@ -0,0 +1,68 @@ +/********************************************************************** + * Copyright (C) 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 type { Page, Locator } from '@playwright/test'; +import { BasePage, ResourcesPage, expect as playExpect } from '@podman-desktop/tests-playwright'; + +export class CreateResourcePage extends BasePage { + readonly heading: Locator; + readonly content: Locator; + readonly logIntoSandboxButton: Locator; + readonly contextName: Locator; + readonly setAsCurrentContext: Locator; + readonly loginCommand: Locator; + readonly closeButton: Locator; + readonly createButton: Locator; + + constructor(page: Page) { + super(page); + this.heading = this.page.getByRole('heading', { name: 'Create Developer Sandbox' }); + this.content = this.page.getByRole('region', { name: 'Tab Content' }); + this.logIntoSandboxButton = this.page.getByRole('button', { name: 'Log into Developer Sandbox' }); + this.contextName = this.page.getByRole('textbox', { name: 'Context name' }); + this.setAsCurrentContext = this.page.getByRole('checkbox', { name: 'Set as current context' }); + this.loginCommand = this.page.getByRole('textbox', { name: 'Login command from Developer Console' }); + this.closeButton = this.page.getByRole('button', { name: 'Close page' }); + this.createButton = this.page.getByRole('button', { name: 'Create' }); + } + + async createResource( + loginCommandValue: string, + contextNameValue?: string, + setAsCurrentContextValue = false, + ): Promise { + await this.loginCommand.fill(loginCommandValue); + + if (contextNameValue) { + await this.contextName.fill(contextNameValue); + } + + if (setAsCurrentContextValue !== (await this.setAsCurrentContext.isChecked())) { + await this.setAsCurrentContext.locator('..').click(); + playExpect(await this.setAsCurrentContext.isChecked()).toBe(setAsCurrentContextValue); + } + + const successMessage = this.page.getByText('Successful operation'); + const goToResourcesButton = this.page.getByRole('button', { name: 'Go back to resources' }); + await playExpect(successMessage).toBeVisible(); + await playExpect(goToResourcesButton).toBeVisible(); + + await goToResourcesButton.click(); + return new ResourcesPage(this.page); + } +} diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 9eef966..97981dc 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -1,16 +1,16 @@ { - "compilerOptions": { - "strictNullChecks": true, - "lib": [ "ES2017", "webworker" ], - "module": "esnext", - "target": "esnext", - "sourceMap": true, - "rootDir": "src", - "outDir": "dist", - "skipLibCheck": true, - "types": [ "node" ], - "allowSyntheticDefaultImports": true, - "moduleResolution": "Node", - "esModuleInterop": true - } - } \ No newline at end of file + "compilerOptions": { + "strictNullChecks": true, + "lib": ["ES2017", "webworker"], + "module": "esnext", + "target": "esnext", + "sourceMap": true, + "rootDir": "src", + "outDir": "dist", + "skipLibCheck": true, + "types": ["node"], + "allowSyntheticDefaultImports": true, + "moduleResolution": "Node", + "esModuleInterop": true + } +}