diff --git a/app/e2e-tests/README.md b/app/e2e-tests/README.md new file mode 100644 index 0000000000..1fc1da0132 --- /dev/null +++ b/app/e2e-tests/README.md @@ -0,0 +1,69 @@ +# e2e test for local playwright app mode + +Currently we have the original e2e tests for the web mode in the `e2e-tests` directory. We are adding new tests for the app mode in the `app-e2e-tests` directory for local testing. Unlike the other tests, these tests do not require a token to run so the setup is as followed: + +## Running app mode tests + +## Setup + +- Before running the tests, be sure to have an instance of Minikube running with the name `minikube` + +### Running the tests + +To run the tests for the app mode, follow the steps below: + +- cd into the e2e-tests directory within the headlamp repository + `cd headlamp/app/e2e-tests` + +- npm install the needed packages + `npm install` + +- run the following command + `npm run test-app` + (optional: include `-- --headed` to run the tests in headed mode) + (optional: include `-- --ui` to run the tests in ui mode) + +## Running web mode tests + +Running the tests for the web mode requires the backend and frontend to be running. Follow the steps below to run the tests for the web mode: + +Note: You may encouter issues switching from the app mode tests to the web mode tests. If you do, search for any running headlamp server processes and end them before running the web mode tests or app mode tests. + +## Setup + +- Before running the tests, be sure to have an instance of Minikube running with the name `minikube` + +### Backend + +To run the tests for the web mode, you will need to have the backend running. Follow the steps below to run the backend: + +- cd into the headlamp directory in a singular terminal + `cd headlamp` + +- run the following command + `make backend` followed by `make run-backend` + +### Frontend + +To run the tests for the web mode, you will need to have the frontend running. Follow the steps below to run the frontend: + +- cd into the headlamp directory in a separate terminal + `cd headlamp/frontend` + +- run the following command + `make frontend` followed by `make run-frontend` + +### Running the tests + +To run the tests for the web mode, follow the steps below: + +- cd into the e2e-tests directory within the headlamp repository in a separate terminal + `cd headlamp/app/e2e-tests` + +- npm install the needed packages + `npm install` + +- run the following command + `npm run test-web` + (optional: include `-- --headed` to run the tests in headed mode) + (optional: include `-- --ui` to run the tests in ui mode) diff --git a/app/e2e-tests/package.json b/app/e2e-tests/package.json index 711d3e95c7..2da0bbd744 100644 --- a/app/e2e-tests/package.json +++ b/app/e2e-tests/package.json @@ -2,7 +2,10 @@ "name": "e2e-tests", "version": "1.0.0", "main": "index.js", - "scripts": {}, + "scripts": { + "test-app": "PLAYWRIGHT_TEST_MODE=app playwright test", + "test-web": "PLAYWRIGHT_TEST_MODE=web playwright test" + }, "keywords": [], "author": "", "license": "ISC", diff --git a/app/e2e-tests/playwright.config.ts b/app/e2e-tests/playwright.config.ts index a05d8b5a19..601fc4b67b 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. */ @@ -38,36 +46,6 @@ export default defineConfig({ name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, ], /* Run your local dev server before starting the tests */ 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 216ea1b6b4..0000000000 --- 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 8ae5fdffc3..0000000000 --- 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 0000000000..2a6237e041 --- /dev/null +++ b/app/e2e-tests/tests/headlampPage.ts @@ -0,0 +1,154 @@ +/// +import { expect, Page } from '@playwright/test'; + +export class HeadlampPage { + constructor(private page: Page) {} + + async authenticate() { + // If we are running in cluster, we need to authenticate + if (process.env.PLAYWRIGHT_TEST_MODE === 'app' || process.env.PLAYWRIGHT_TEST_MODE === 'web') { + await this.startFromMainPage(); + return; + } + + // Go to the authentication page + const url = process.env.HEADLAMP_TEST_URL; + await this.page.goto(url || '/'); + await this.page.waitForSelector('h1:has-text("Authentication")'); + + // Check to see if already authenticated + if (await this.page.isVisible('button:has-text("Authenticate")')) { + 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', exact: true }).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 0000000000..ba2958b2b1 --- /dev/null +++ b/app/e2e-tests/tests/namespaces.spec.ts @@ -0,0 +1,57 @@ +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); + + await headlampPage.authenticate(); + + 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 0000000000..40e75ee186 --- /dev/null +++ b/app/e2e-tests/tests/namespacesPage.ts @@ -0,0 +1,86 @@ +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('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(); + } +} diff --git a/e2e-tests/tests/headlampPage.ts b/e2e-tests/tests/headlampPage.ts index 51ae4a2762..1ceab9eb70 100644 --- a/e2e-tests/tests/headlampPage.ts +++ b/e2e-tests/tests/headlampPage.ts @@ -5,23 +5,25 @@ export class HeadlampPage { constructor(private page: Page) {} async authenticate() { - await this.page.goto('/'); + // Go to the authentication page + const url = process.env.HEADLAMP_TEST_URL; + await this.page.goto(url || '/'); 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); + // Check to see if already authenticated + if (await this.page.isVisible('button:has-text("Authenticate")')) { + const token = process.env.HEADLAMP_TOKEN || ''; + this.hasToken(token); - // Fill in the token - await this.page.locator('#token').fill(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")'), - ]); + // 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) {