mirror of
https://github.com/inventree/InvenTree.git
synced 2025-09-13 22:21:37 +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