mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	[bug] Playwright fixes (#9933)
* Fixes for playwright testing - Ensure cookies are completely cleaned between sessions - Fix base URL based on vite command - Fix samesite cookie mode - Prevent /static/ files being served by web server on :8000 * Remove gunicorn option * Readjust base URL * Simplify doCachedLogin * Fix logic func * Revert webserver cmd * Set base URL in playwrightconfig file * Fix URL checks * Fix URL definitions * adjust playwright base URL * Tweak for URL helper * Further login tweaks * Tweak test * wait for API before starting tests * Handle error * Adjust login functions * Don't use gunicorn - But still use the webserver to serve static files in CI * Enhanced login functions * Tweak login tests * Fix broken test * Flipped the flippies
This commit is contained in:
		| @@ -52,7 +52,7 @@ export const test = baseTest.extend({ | ||||
|     } | ||||
|   }, | ||||
|   // Ensure no errors are thrown in the console | ||||
|   page: async ({ baseURL, page }, use) => { | ||||
|   page: async ({ page }, use) => { | ||||
|     const messages = []; | ||||
|     page.on('console', (msg) => { | ||||
|       const url = msg.location().url; | ||||
| @@ -67,20 +67,20 @@ export const test = baseTest.extend({ | ||||
|           ) < 0 && | ||||
|         msg.text() != | ||||
|           'Failed to load resource: the server responded with a status of 400 (Bad Request)' && | ||||
|         !msg.text().includes('http://localhost:8000/this/does/not/exist.js') && | ||||
|         url != 'http://localhost:8000/this/does/not/exist.js' && | ||||
|         url != 'http://localhost:8000/api/user/me/' && | ||||
|         url != 'http://localhost:8000/api/user/token/' && | ||||
|         url != 'http://localhost:8000/api/auth/v1/auth/login' && | ||||
|         url != 'http://localhost:8000/api/auth/v1/auth/session' && | ||||
|         url != 'http://localhost:8000/api/auth/v1/account/password/change' && | ||||
|         url != 'http://localhost:8000/api/barcode/' && | ||||
|         url != 'https://docs.inventree.org/en/versions.json' && | ||||
|         url != 'http://localhost:5173/favicon.ico' && | ||||
|         !msg.text().includes('/this/does/not/exist.js') && | ||||
|         !url.includes('/this/does/not/exist.js') && | ||||
|         !url.includes('/api/user/me/') && | ||||
|         !url.includes('/api/user/token/') && | ||||
|         !url.includes('/api/auth/v1/auth/login') && | ||||
|         !url.includes('/api/auth/v1/auth/session') && | ||||
|         !url.includes('/api/auth/v1/account/password/change') && | ||||
|         !url.includes('/api/barcode/') && | ||||
|         !url.includes('/favicon.ico') && | ||||
|         !url.startsWith('https://api.github.com/repos/inventree') && | ||||
|         !url.startsWith('http://localhost:8000/api/news/') && | ||||
|         !url.startsWith('http://localhost:8000/api/notifications/') && | ||||
|         !url.includes('/api/news/') && | ||||
|         !url.includes('/api/notifications/') && | ||||
|         !url.startsWith('chrome://') && | ||||
|         url != 'https://docs.inventree.org/en/versions.json' && | ||||
|         url.indexOf('99999') < 0 | ||||
|       ) | ||||
|         messages.push(msg); | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| export const classicUrl = 'http://127.0.0.1:8000'; | ||||
| export const webUrl = '/web'; | ||||
|  | ||||
| export const apiUrl = `${classicUrl}/api`; | ||||
| export const baseUrl = './web'; | ||||
| export const loginUrl = `${baseUrl}/login`; | ||||
| export const logoutUrl = `${baseUrl}/logout`; | ||||
| export const homeUrl = `${baseUrl}/home`; | ||||
| // Note: API requests are handled by the backend server | ||||
| export const apiUrl = 'http://localhost:8000/api/'; | ||||
|  | ||||
| export const homeUrl = `${webUrl}/home`; | ||||
| export const loginUrl = `${webUrl}/login`; | ||||
| export const logoutUrl = `${webUrl}/logout`; | ||||
|  | ||||
| export const user = { | ||||
|   name: 'Ally Access', | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| import { baseUrl } from './defaults'; | ||||
|  | ||||
| /** | ||||
|  * Open the filter drawer for the currently visible table | ||||
|  * @param page - The page object | ||||
| @@ -73,21 +71,30 @@ export const clickOnRowMenu = async (cell) => { | ||||
|   await row.getByLabel(/row-action-menu-/i).click(); | ||||
| }; | ||||
|  | ||||
| interface NavigateOptions { | ||||
|   waitUntil?: 'load' | 'domcontentloaded' | 'networkidle'; | ||||
|   baseUrl?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Navigate to the provided page, and wait for loading to complete | ||||
|  * @param page | ||||
|  * @param url | ||||
|  */ | ||||
| export const navigate = async (page, url: string) => { | ||||
|   if (!url.startsWith(baseUrl)) { | ||||
|     if (url.startsWith('/')) { | ||||
|       url = url.slice(1); | ||||
|     } | ||||
|  | ||||
|     url = `${baseUrl}/${url}`; | ||||
| export const navigate = async ( | ||||
|   page, | ||||
|   url: string, | ||||
|   options?: NavigateOptions | ||||
| ) => { | ||||
|   if (!url.startsWith('http') && !url.includes('web')) { | ||||
|     url = `/web/${url}`.replaceAll('//', '/'); | ||||
|   } | ||||
|  | ||||
|   await page.goto(url); | ||||
|   const path: string = options?.baseUrl | ||||
|     ? new URL(url, options.baseUrl).toString() | ||||
|     : url; | ||||
|  | ||||
|   await page.goto(path, { waitUntil: options?.waitUntil ?? 'load' }); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -1,33 +1,53 @@ | ||||
| import type { Browser, Page } from '@playwright/test'; | ||||
| import { expect } from './baseFixtures.js'; | ||||
| import { user } from './defaults'; | ||||
| import { navigate } from './helpers.js'; | ||||
| import { loginUrl, logoutUrl, user, webUrl } from './defaults'; | ||||
|  | ||||
| import fs from 'node:fs'; | ||||
| import path from 'node:path'; | ||||
| import { navigate } from './helpers.js'; | ||||
|  | ||||
| interface LoginOptions { | ||||
|   username?: string; | ||||
|   password?: string; | ||||
|   baseUrl?: string; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * Perform form based login operation from the "login" URL | ||||
|  */ | ||||
| export const doLogin = async (page, username?: string, password?: string) => { | ||||
|   username = username ?? user.username; | ||||
|   password = password ?? user.password; | ||||
| export const doLogin = async (page, options?: LoginOptions) => { | ||||
|   const username: string = options?.username ?? user.username; | ||||
|   const password: string = options?.password ?? user.password; | ||||
|  | ||||
|   await page.goto('http://localhost:8000/web/logout', { waituntil: 'load' }); | ||||
|   console.log('- Logging in with username:', username); | ||||
|  | ||||
|   await navigate(page, loginUrl, { | ||||
|     baseUrl: options?.baseUrl, | ||||
|     waitUntil: 'networkidle' | ||||
|   }); | ||||
|  | ||||
|   await expect(page).toHaveTitle(/^InvenTree.*$/); | ||||
|   await page.waitForURL('**/web/login'); | ||||
|  | ||||
|   await page.getByLabel('username').fill(username); | ||||
|   await page.getByLabel('password').fill(password); | ||||
|  | ||||
|   await page.waitForTimeout(100); | ||||
|  | ||||
|   await page.getByRole('button', { name: 'Log in' }).click(); | ||||
|   await page.waitForURL('**/web/home'); | ||||
|   await page.waitForTimeout(250); | ||||
|  | ||||
|   await page.waitForTimeout(100); | ||||
|   await page.waitForLoadState('networkidle'); | ||||
|  | ||||
|   await page.getByRole('link', { name: 'Dashboard' }).waitFor(); | ||||
|   await page.getByRole('button', { name: 'navigation-menu' }).waitFor(); | ||||
|   await page.waitForURL(/\/web(\/home)?/); | ||||
|   await page.waitForLoadState('networkidle'); | ||||
| }; | ||||
|  | ||||
| export interface CachedLoginOptions { | ||||
|   username?: string; | ||||
|   password?: string; | ||||
|   url?: string; | ||||
|   baseUrl?: string; | ||||
| } | ||||
|  | ||||
| // Set of users allowed to do cached login | ||||
| @@ -58,9 +78,17 @@ export const doCachedLogin = async ( | ||||
|       storageState: fn | ||||
|     }); | ||||
|     console.log(`Using cached login state for ${username}`); | ||||
|     await navigate(page, url); | ||||
|     await page.waitForURL('**/web/**'); | ||||
|     await page.waitForLoadState('load'); | ||||
|  | ||||
|     await navigate(page, url ?? webUrl, { | ||||
|       baseUrl: options?.baseUrl, | ||||
|       waitUntil: 'networkidle' | ||||
|     }); | ||||
|  | ||||
|     await page.getByRole('link', { name: 'Dashboard' }).waitFor(); | ||||
|     await page.getByRole('button', { name: 'navigation-menu' }).waitFor(); | ||||
|     await page.waitForURL(/\/web(\/home)?/); | ||||
|     await page.waitForLoadState('networkidle'); | ||||
|  | ||||
|     return page; | ||||
|   } | ||||
|  | ||||
| @@ -69,29 +97,37 @@ export const doCachedLogin = async ( | ||||
|  | ||||
|   console.log(`No cache found - logging in for ${username}`); | ||||
|  | ||||
|   // Ensure we start from the login page | ||||
|   await page.goto('http://localhost:8000/web/', { waitUntil: 'load' }); | ||||
|   // Completely clear the browser cache and cookies, etc | ||||
|   await page.context().clearCookies(); | ||||
|   await page.context().clearPermissions(); | ||||
|  | ||||
|   await doLogin(page, { | ||||
|     username: username, | ||||
|     password: password, | ||||
|     baseUrl: options?.baseUrl | ||||
|   }); | ||||
|  | ||||
|   await doLogin(page, username, password); | ||||
|   await page.getByLabel('navigation-menu').waitFor({ timeout: 5000 }); | ||||
|   await page.getByText(/InvenTree Demo Server -/).waitFor(); | ||||
|   await page.waitForURL('**/web/**'); | ||||
|  | ||||
|   // Wait for the dashboard to load | ||||
|   //await page.getByText('No widgets selected').waitFor() | ||||
|   await page.waitForLoadState('load'); | ||||
|  | ||||
|   // Cache the login state | ||||
|   await page.context().storageState({ path: fn }); | ||||
|  | ||||
|   if (url) { | ||||
|     await navigate(page, url); | ||||
|     await navigate(page, url, { baseUrl: options?.baseUrl }); | ||||
|   } | ||||
|  | ||||
|   return page; | ||||
| }; | ||||
|  | ||||
| export const doLogout = async (page) => { | ||||
|   await page.goto('http://localhost:8000/web/logout', { waitUntil: 'load' }); | ||||
| interface LogoutOptions { | ||||
|   baseUrl?: string; | ||||
| } | ||||
|  | ||||
| export const doLogout = async (page, options?: LogoutOptions) => { | ||||
|   await navigate(page, logoutUrl, { | ||||
|     baseUrl: options?.baseUrl, | ||||
|     waitUntil: 'load' | ||||
|   }); | ||||
|   await page.waitForURL('**/web/login'); | ||||
| }; | ||||
|   | ||||
| @@ -265,7 +265,7 @@ test('Parts - Pricing (Nothing, BOM)', async ({ browser }) => { | ||||
|   await page.getByRole('button', { name: 'Supplier Pricing' }).isDisabled(); | ||||
|  | ||||
|   // Part with history | ||||
|   await navigate(page, 'part/108/pricing'); | ||||
|   await navigate(page, 'part/108/pricing', { waitUntil: 'networkidle' }); | ||||
|   await page.getByText('A chair - with blue paint').waitFor(); | ||||
|   await loadTab(page, 'Part Pricing'); | ||||
|   await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); | ||||
|   | ||||
| @@ -45,13 +45,13 @@ test('Login - Failures', async ({ page }) => { | ||||
| }); | ||||
|  | ||||
| test('Login - Change Password', async ({ page }) => { | ||||
|   await doLogin(page, 'noaccess', 'youshallnotpass'); | ||||
|   await page.waitForLoadState('networkidle'); | ||||
|   await doLogin(page, { | ||||
|     username: 'noaccess', | ||||
|     password: 'youshallnotpass' | ||||
|   }); | ||||
|  | ||||
|   // Navigate to the 'change password' page | ||||
|   await navigate(page, 'settings/user/account'); | ||||
|   await page.waitForLoadState('networkidle'); | ||||
|  | ||||
|   await navigate(page, 'settings/user/account', { waitUntil: 'networkidle' }); | ||||
|   await page.getByLabel('action-menu-account-actions').click(); | ||||
|   await page.getByLabel('action-menu-account-actions-change-password').click(); | ||||
|  | ||||
| @@ -69,9 +69,16 @@ test('Login - Change Password', async ({ page }) => { | ||||
|   await page.getByText('This password is too short').waitFor(); | ||||
|   await page.getByText('This password is entirely numeric').waitFor(); | ||||
|  | ||||
|   await page.getByLabel('input-password-1').fill('youshallnotpass'); | ||||
|   await page.waitForTimeout(250); | ||||
|  | ||||
|   await page.getByLabel('password', { exact: true }).clear(); | ||||
|   await page.getByLabel('input-password-1').clear(); | ||||
|   await page.getByLabel('input-password-2').clear(); | ||||
|  | ||||
|   await page.getByLabel('password', { exact: true }).fill('youshallnotpass'); | ||||
|   await page.getByLabel('input-password-1').fill('youshallnotpass'); | ||||
|   await page.getByLabel('input-password-2').fill('youshallnotpass'); | ||||
|  | ||||
|   await page.getByRole('button', { name: 'Confirm' }).click(); | ||||
|  | ||||
|   await page.getByText('Password Changed').waitFor(); | ||||
|   | ||||
| @@ -198,7 +198,8 @@ test('Settings - Admin - Barcode History', async ({ browser, request }) => { | ||||
|   const barcodes = ['ABC1234', 'XYZ5678', 'QRS9012']; | ||||
|  | ||||
|   barcodes.forEach(async (barcode) => { | ||||
|     await request.post(`${apiUrl}/barcode/`, { | ||||
|     const url = new URL('barcode/', apiUrl).toString(); | ||||
|     await request.post(url, { | ||||
|       data: { | ||||
|         barcode: barcode | ||||
|       }, | ||||
|   | ||||
| @@ -14,7 +14,7 @@ export const setSettingState = async ({ | ||||
|   setting: string; | ||||
|   value: any; | ||||
| }) => { | ||||
|   const url = `${apiUrl}/settings/global/${setting}/`; | ||||
|   const url = new URL(`settings/global/${setting}/`, apiUrl).toString(); | ||||
|  | ||||
|   const response = await request.patch(url, { | ||||
|     data: { | ||||
| @@ -38,7 +38,7 @@ export const setPluginState = async ({ | ||||
|   plugin: string; | ||||
|   state: boolean; | ||||
| }) => { | ||||
|   const url = `${apiUrl}/plugins/${plugin}/activate/`; | ||||
|   const url = new URL(`plugins/${plugin}/activate/`, apiUrl).toString(); | ||||
|  | ||||
|   const response = await request.patch(url, { | ||||
|     data: { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user