From d684e6a39eca1dd2b368269cf19ce4c3b7c35c11 Mon Sep 17 00:00:00 2001 From: Vincent T Date: Wed, 16 Oct 2024 16:05:29 -0400 Subject: [PATCH] frontend: Add playwright app test Add working namespace test Signed-off-by: Vincent T --- app/e2e-tests/playwright.config.ts | 28 +- .../tests-examples/demo-todo-app.spec.ts | 424 ------------------ app/e2e-tests/tests/example.spec.ts | 18 - app/e2e-tests/tests/headlampPage.ts | 146 ++++++ app/e2e-tests/tests/namespaces.spec.ts | 64 +++ app/e2e-tests/tests/namespacesPage.ts | 87 ++++ 6 files changed, 315 insertions(+), 452 deletions(-) delete mode 100644 app/e2e-tests/tests-examples/demo-todo-app.spec.ts delete mode 100644 app/e2e-tests/tests/example.spec.ts create mode 100644 app/e2e-tests/tests/headlampPage.ts create mode 100644 app/e2e-tests/tests/namespaces.spec.ts create mode 100644 app/e2e-tests/tests/namespacesPage.ts diff --git a/app/e2e-tests/playwright.config.ts b/app/e2e-tests/playwright.config.ts index a05d8b5a19a..e1cb26456ac 100644 --- a/app/e2e-tests/playwright.config.ts +++ b/app/e2e-tests/playwright.config.ts @@ -14,13 +14,21 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', /* Run tests in files in parallel */ - fullyParallel: true, + timeout: 60 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 120000, + }, + fullyParallel: false, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -39,15 +47,15 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'] }, }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, /* Test against mobile viewports. */ // { diff --git a/app/e2e-tests/tests-examples/demo-todo-app.spec.ts b/app/e2e-tests/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 216ea1b6b4c..00000000000 --- a/app/e2e-tests/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { expect, type Page,test } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = ['buy some cheese', 'feed the cat', 'book a doctors appointment'] as const; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0]]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count'); - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass([ - 'completed', - 'completed', - 'completed', - ]); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ - page, - }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect( - todoItem.locator('label', { - hasText: TODO_ITEMS[1], - }) - ).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count'); - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return ( - JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e - ); - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']) - .map((todo: any) => todo.title) - .includes(t); - }, title); -} diff --git a/app/e2e-tests/tests/example.spec.ts b/app/e2e-tests/tests/example.spec.ts deleted file mode 100644 index 8ae5fdffc38..00000000000 --- a/app/e2e-tests/tests/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { expect,test } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -}); diff --git a/app/e2e-tests/tests/headlampPage.ts b/app/e2e-tests/tests/headlampPage.ts new file mode 100644 index 00000000000..5ddfd2b02c4 --- /dev/null +++ b/app/e2e-tests/tests/headlampPage.ts @@ -0,0 +1,146 @@ +/// +import { expect, Page } from '@playwright/test'; + +export class HeadlampPage { + constructor(private page: Page) {} + + async authenticate() { + await this.page.goto('/'); + await this.page.waitForSelector('h1:has-text("Authentication")'); + + // Expects the URL to contain c/main/token + this.hasURLContaining(/.*token/); + + const token = process.env.HEADLAMP_TOKEN || ''; + this.hasToken(token); + + // Fill in the token + await this.page.locator('#token').fill(token); + + // Click on the "Authenticate" button and wait for navigation + await Promise.all([ + this.page.waitForNavigation(), + this.page.click('button:has-text("Authenticate")'), + ]); + } + + async hasURLContaining(pattern: RegExp) { + await expect(this.page).toHaveURL(pattern); + } + + async hasTitleContaining(pattern: RegExp) { + await expect(this.page).toHaveTitle(pattern); + } + + async hasToken(token: string) { + expect(token).not.toBe(''); + } + + async hasNetworkTab() { + const networkTab = this.page.locator('span:has-text("Network")').first(); + expect(await networkTab.textContent()).toBe('Network'); + } + + async hasSecurityTab() { + const networkTab = this.page.locator('span:has-text("Security")').first(); + expect(await networkTab.textContent()).toBe('Security'); + } + + async checkPageContent(text: string) { + await this.page.waitForSelector(`:has-text("${text}")`); + const pageContent = await this.page.content(); + expect(pageContent).toContain(text); + } + + async pageLocatorContent(locator: string, text: string) { + const pageContent = this.page.locator(locator).textContent(); + expect(await pageContent).toContain(text); + } + + // note: must have minikube started before running these + async startFromMainPage() { + await this.page.waitForLoadState('load'); + + // note: backend must be running with connected frontend for web mode + if (process.env.PLAYWRIGHT_TEST_MODE === 'web') { + await this.page.goto('localhost:3000'); + } + + await this.page.waitForTimeout(5000); + const currentURL = this.page.url(); + + // note: this starts at the cluster select page if the URL does not contain minikube or main then there is more than one cluster + if (!currentURL.includes('c/minikube') && !currentURL.includes('c/main')) { + console.log('MORE THAN ONE CLUSTER'); + await this.page.waitForSelector('a:has-text("minikube")'); + await this.page.getByRole('link', { name: 'minikube' }).click(); + await this.page.waitForLoadState('load'); + } + } + + async navigateTopage(page: string, title: RegExp) { + await this.page.goto(page); + await this.page.waitForLoadState('load'); + await this.hasTitleContaining(title); + } + + async logout() { + // Click on the account button to open the user menu + await this.page.click('button[aria-label="Account of current user"]'); + + // Wait for the logout option to be visible and click on it + await this.page.waitForSelector('a.MuiMenuItem-root:has-text("Log out")'); + await this.page.click('a.MuiMenuItem-root:has-text("Log out")'); + await this.page.waitForLoadState('load'); + + // Expects the URL to contain c/main/token + await this.hasURLContaining(/.*token/); + } + + async tableHasHeaders(tableSelector: string, expectedHeaders: string[]) { + // Get all table headers + const headers = await this.page.$$eval(`${tableSelector} th`, ths => + ths.map(th => { + if (th && th.textContent) { + // Table header also contains a number, displayed during multi-sorting, so we remove it + return th.textContent.trim().replace('0', ''); + } + }) + ); + + // Check if all expected headers are present in the table + for (const header of expectedHeaders) { + if (!headers.includes(header)) { + throw new Error(`Table does not contain header: ${header}`); + } + } + } + + async clickOnPlugin(pluginName: string) { + await this.page.click(`a:has-text("${pluginName}")`); + await this.page.waitForLoadState('load'); + } + + async checkRows() { + // Get value of rows per page + const rowsDisplayed1 = await this.getRowsDisplayed(); + + // Click on the next page button + const nextPageButton = this.page.getByRole('button', { + name: 'Go to next page', + }); + await nextPageButton.click(); + + // Get value of rows per page after clicking next page button + const rowsDisplayed2 = await this.getRowsDisplayed(); + + // Check if the rows displayed are different + expect(rowsDisplayed1).not.toBe(rowsDisplayed2); + } + + async getRowsDisplayed() { + const paginationCaption = this.page.locator("span:has-text(' of ')"); + const captionText = await paginationCaption.textContent(); + return captionText; + } +} diff --git a/app/e2e-tests/tests/namespaces.spec.ts b/app/e2e-tests/tests/namespaces.spec.ts new file mode 100644 index 00000000000..423c0298826 --- /dev/null +++ b/app/e2e-tests/tests/namespaces.spec.ts @@ -0,0 +1,64 @@ +import { test } from '@playwright/test'; +import path from 'path'; +import { _electron, Page } from 'playwright'; +import { HeadlampPage } from './headlampPage'; +import { NamespacesPage } from './namespacesPage'; + +const electronExecutable = process.platform === 'win32' ? 'electron.cmd' : 'electron'; +const electronPath = path.resolve(__dirname, `../../node_modules/.bin/${electronExecutable}`); + +const electron = _electron; +const appPath = path.resolve(__dirname, '../../'); +let electronApp; +let electronPage: Page; + +if (process.env.PLAYWRIGHT_TEST_MODE === 'app') { + test.beforeAll(async () => { + electronApp = await electron.launch({ + cwd: appPath, + executablePath: electronPath, + args: ['.'], + env: { + ...process.env, + NODE_ENV: 'development', + ELECTRON_DEV: 'true', + }, + }); + + electronPage = await electronApp.firstWindow(); + }); + + test.beforeEach(async ({ page }) => { + if (process.env.PLAYWRIGHT_TEST_MODE === 'app') { + page.close(); + } + }); +} + +// note: this test is for local app development testing and will require: +// - a running minikube cluster named 'minikube' +// - an ENV variable of PLAYWRIGHT_TEST_MODE=app +test.describe('create a namespace with the minimal editor', async () => { + test.setTimeout(0); + test('create a namespace with the minimal editor then delete it', async ({ + page: browserPage, + }) => { + const page = process.env.PLAYWRIGHT_TEST_MODE === 'app' ? electronPage : browserPage; + const name = 'testing-e2e'; + const headlampPage = new HeadlampPage(page); + const namespacesPage = new NamespacesPage(page); + + // If we are running in cluster, we need to authenticate + if (process.env.PLAYWRIGHT_TEST_MODE !== 'app' && process.env.PLAYWRIGHT_TEST_MODE !== 'web') { + await headlampPage.authenticate(); + await namespacesPage.navigateToNamespaces(); + await namespacesPage.createNamespace(name); + await namespacesPage.deleteNamespace(name); + } else { + await headlampPage.startFromMainPage(); + await namespacesPage.navigateToNamespaces(); + await namespacesPage.createNamespace(name); + await namespacesPage.deleteNamespace(name); + } + }); +}); diff --git a/app/e2e-tests/tests/namespacesPage.ts b/app/e2e-tests/tests/namespacesPage.ts new file mode 100644 index 00000000000..1b829e03cbc --- /dev/null +++ b/app/e2e-tests/tests/namespacesPage.ts @@ -0,0 +1,87 @@ +import { expect, Page } from '@playwright/test'; + +export class NamespacesPage { + constructor(private page: Page) {} + + async navigateToNamespaces() { + await this.page.waitForLoadState('load'); + await this.page.waitForSelector('span:has-text("Cluster")'); + await this.page.getByText('Cluster', { exact: true }).click(); + await this.page.waitForSelector('span:has-text("Namespaces")'); + await this.page.click('span:has-text("Namespaces")'); + await this.page.waitForLoadState('load'); + } + + async createNamespace(name) { + const yaml = ` + apiVersion: v1 + kind: Namespace + metadata: + name: ${name} + `; + const page = this.page; + + await page.waitForSelector('span:has-text("Namespaces")'); + await page.click('span:has-text("Namespaces")'); + await page.waitForLoadState('load'); + + // If the namespace already exists, return. + // This makes it a bit more resilient to flakiness. + const pageContent = await this.page.content(); + if (pageContent.includes(name)) { + throw new Error(`Test failed: Namespace "${name}" already exists.`); + } + + await page.getByText('Create', { exact: true }).click(); + + await page.waitForLoadState('load'); + + // this is a workaround for the checked input not having any unique identifier + const checkedSpan = await page.$('span.Mui-checked'); + + if (!checkedSpan) { + await expect(page.getByText('Use minimal editor')).toBeVisible(); + + await page.getByText('Use minimal editor').click(); + } + + await page.waitForLoadState('load'); + + await page.waitForSelector('textarea[aria-label="yaml Code"]', { state: 'visible' }); + + await expect(page.getByRole('textbox', { name: 'yaml Code' })).toBeVisible(); + await page.fill('textarea[aria-label="yaml Code"]', yaml); + + await expect(page.getByRole('button', { name: 'Apply' })).toBeVisible(); + await page.getByRole('button', { name: 'Apply' }).click(); + + await page.waitForSelector(`a:has-text("${name}")`); + await expect(page.locator(`a:has-text("${name}")`)).toBeVisible(); + } + + async deleteNamespace(name) { + const page = this.page; + await page.click('span:has-text("Namespaces")'); + await page.waitForLoadState('load'); + + await page.waitForSelector(`text=${name}`); + await page.click(`a:has-text("${name}")`); + + await page.waitForSelector('button[aria-label="Delete"]'); + await page.click('button[aria-label="Delete"]'); + + await page.waitForLoadState('load'); + + await page.waitForSelector('text=Are you sure you want to delete this item?'); + await page.waitForSelector('button:has-text("Yes")'); + + await page.waitForLoadState('load'); + + await page.click('button:has-text("Yes")'); + + await page.waitForSelector('h1:has-text("Namespaces")'); + await page.waitForSelector('td:has-text("Terminating")'); + + await expect(page.locator(`a:has-text("${name}")`)).toBeHidden(); + } +}