2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-27 19:16:44 +00:00

[CI] Playwright improvements (#9395)

* Allow port 4173 (vite preview)

* Change 'base' attr based on vite command

* Allow api_host to be specified separately

* Harden API host functionality

* Adjust server selections

* Cleanup vite.config.ts

* Adjust playwright configuration

- Allow to run in "production" mode
- Builds the code first
- Runs only the backend web server
- Not suitable for coverage

* Tweak github actions

* Tweak QC file

* Reduce number of steps

* Tweak CI file

* Fix typo

* Ensure translation before build

* Fix hard-coded test

* Test tweaks

* uncomment

* Revert some changes

* Run with gunicorn, single worker

* Reduce log output in DEBUG mode

* Update deps

* Add global-setup func

* Fix for .gitignore file

* Cached auth state

* Tweak login func

* Updated tests

* Enable parallel workers again

* Simplify config

* Try with a single worker again

* Single retry mode

* Run auth setup first

- Prevent issues with parallel test doing login

* Improve test setup process

* Tweaks

* Bump to 3 workers

* Tweak playwright settings

* Revert change

* Revert change
This commit is contained in:
Oliver 2025-03-30 14:12:48 +11:00 committed by GitHub
parent 858eb8f807
commit 7f5a447769
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 794 additions and 575 deletions

View File

@ -554,7 +554,7 @@ jobs:
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate
platform_ui:
web_ui:
name: Tests - Web UI
runs-on: ubuntu-24.04
timeout-minutes: 60
@ -584,7 +584,6 @@ jobs:
INVENTREE_DB_PASSWORD: inventree_password
INVENTREE_DEBUG: true
INVENTREE_PLUGINS_ENABLED: false
VITE_COVERAGE: true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
@ -599,13 +598,19 @@ jobs:
apt-dependency: postgresql-client libpq-dev
pip-dependency: psycopg2
- name: Set up test data
run: invoke dev.setup-test -iv
- name: Rebuild thumbnails
run: invoke int.rebuild-thumbnails
run: |
invoke dev.setup-test -iv
invoke int.rebuild-thumbnails
- name: Install dependencies
run: invoke int.frontend-compile
- name: Install Playwright Browsers
run: cd src/frontend && npx playwright install --with-deps
run: |
invoke int.frontend-compile
cd src/frontend && npx playwright install --with-deps
- name: Set Production Mode
if: github.event_name == 'pull_request'
run: echo "VITE_PRODUCTION_BUILD=true" >> $GITHUB_ENV
- name: Set Coverage Mode
if: github.event_name != 'pull_request'
run: echo "VITE_COVERAGE_BUILD=true" >> $GITHUB_ENV
- name: Run Playwright tests
id: tests
run: cd src/frontend && npx nyc playwright test
@ -616,16 +621,17 @@ jobs:
path: src/frontend/playwright-report/
retention-days: 14
- name: Report coverage
if: always()
if: github.event_name != 'pull_request'
run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # pin@v5.4.0
if: always()
if: github.event_name != 'pull_request'
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: inventree/InvenTree
flags: web
- name: Upload bundler info
if: github.event_name != 'pull_request'
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: |
@ -633,7 +639,7 @@ jobs:
yarn install
yarn run build
platform_ui_build:
web_ui_build:
name: Build - Web UI
runs-on: ubuntu-24.04
timeout-minutes: 60

View File

@ -406,13 +406,22 @@ def get_frontend_settings(debug=True):
'INVENTREE_FRONTEND_SETTINGS', 'frontend_settings', {}, typecast=dict
)
# Set the base URL
# Set the base URL for the user interface
# This is the UI path e.g. '/web/'
if 'base_url' not in frontend_settings:
frontend_settings['base_url'] = (
get_setting('INVENTREE_FRONTEND_URL_BASE', 'frontend_url_base', 'web')
or 'web'
)
# If provided, specify the API host
api_host = frontend_settings.get('api_host', None) or get_setting(
'INVENTREE_FRONTEND_API_HOST', 'frontend_api_host', None
)
if api_host:
frontend_settings['api_host'] = api_host
# Set the server list
frontend_settings['server_list'] = frontend_settings.get('server_list', [])

View File

@ -1088,6 +1088,7 @@ if DEBUG:
'http://localhost',
'http://*.localhost',
'http://*localhost:8000',
'http://*localhost:4173',
'http://*localhost:5173',
]:
if origin not in CSRF_TRUSTED_ORIGINS:

View File

@ -2,6 +2,7 @@
from datetime import timedelta
from django.conf import settings
from django.utils import timezone
import structlog
@ -60,13 +61,16 @@ def check_system_health(**kwargs):
if not is_worker_running(**kwargs): # pragma: no cover
result = False
logger.warning('Background worker check failed')
if not settings.DEBUG:
logger.warning('Background worker check failed')
if not InvenTree.helpers_email.is_email_configured(): # pragma: no cover
result = False
logger.warning('Email backend not configured')
if not settings.DEBUG:
logger.warning('Email backend not configured')
if not result: # pragma: no cover
logger.warning('InvenTree system health checks failed')
if not settings.DEBUG:
logger.warning('InvenTree system health checks failed')
return result

View File

@ -41,6 +41,7 @@
"@mantine/notifications": "^7.16.0",
"@mantine/spotlight": "^7.16.0",
"@mantine/vanilla-extract": "^7.16.0",
"@messageformat/date-skeleton": "^1.1.0",
"@sentry/react": "^8.43.0",
"@tabler/icons-react": "^3.17.0",
"@tanstack/react-query": "^5.56.2",
@ -83,7 +84,7 @@
"@lingui/cli": "^5.3.0",
"@lingui/macro": "^5.3.0",
"@playwright/test": "^1.49.1",
"@types/node": "^22.6.0",
"@types/node": "^22.13.14",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.3.8",
"@types/react-dom": "^18.3.0",
@ -94,6 +95,7 @@
"@vitejs/plugin-react": "^4.3.4",
"babel-plugin-macros": "^3.1.0",
"nyc": "^17.1.0",
"path": "^0.12.7",
"rollup": "^4.0.0",
"rollup-plugin-license": "^3.5.3",
"typescript": "^5.8.2",

View File

@ -1,13 +1,96 @@
import { defineConfig, devices } from '@playwright/test';
import type TestConfigWebServer from '@playwright/test';
// Detect if running in CI
const IS_CI = !!process.env.CI;
const IS_COVERAGE = !!process.env.VITE_COVERAGE_BUILD;
// If specified, tests will be run against the production build
const IS_PRODUCTION = !!process.env.VITE_PRODUCTION_BUILD;
console.log('Running Playwright tests:');
console.log(` - CI Mode: ${IS_CI}`);
console.log(` - Coverage Mode: ${IS_COVERAGE}`);
console.log(` - Production Mode: ${IS_PRODUCTION}`);
const MAX_WORKERS: number = 3;
const MAX_RETRIES: number = 3;
/* We optionally spin-up services based on the testing mode:
*
* Local Development:
* - If running locally (developer mode), we run "vite dev" with HMR enabled
* - This allows playwright to monitor the code for changes
* - WORKERS = 1 (to avoid conflicts with HMR)
*
* CI Mode (Production):
* - In CI (GitHub actions), we run "vite build" to generate a production build
* - This build is then served by a local server for testing
* - This allows the tests to run much faster and with parallel workers
* - Run a Gunicorn multi-threaded web server to handle multiple requests
* - WORKERS = MAX_WORKERS (to speed up the tests)
*
* CI Mode (Coverage):
* - In coverage mode (GitHub actions), we cannot compile the code first
* - This is because we need to compile coverage report against ./src directory
* - So, we run "vite dev" with coverage enabled (similar to the local dev mode)
* - WORKERS = 1 (to avoid conflicts with HMR)
*/
const devServer: TestConfigWebServer = {
command: 'yarn run dev --host --port 5173',
url: 'http://localhost:5173',
reuseExistingServer: IS_CI,
stdout: 'pipe',
stderr: 'pipe',
timeout: 120 * 1000
};
const WEB_BUILD_CMD: string =
'yarn run extract && yarn run compile && yarn run build';
// Command to spin-up the backend server
// In production mode, we want a stronger webserver to handle multiple requests
const WEB_SERVER_CMD: string = IS_PRODUCTION
? `${WEB_BUILD_CMD} && gunicorn --chdir ../backend/InvenTree --workers 8 --thread 8 --bind 127.0.0.1:8000 InvenTree.wsgi`
: 'invoke dev.server -a 127.0.0.1:8000';
const webServer: TestConfigWebServer = {
// If running in production mode, we need to build the frontend first
command: WEB_SERVER_CMD,
env: {
INVENTREE_DEBUG: 'True',
INVENTREE_PLUGINS_ENABLED: 'True',
INVENTREE_ADMIN_URL: 'test-admin',
INVENTREE_SITE_URL: 'http://localhost:8000',
INVENTREE_FRONTEND_API_HOST: 'http://localhost:8000',
INVENTREE_CORS_ORIGIN_ALLOW_ALL: 'True',
INVENTREE_COOKIE_SAMESITE: 'Lax',
INVENTREE_LOGIN_ATTEMPTS: '100'
},
url: 'http://localhost:8000/api/',
reuseExistingServer: IS_CI,
stdout: 'pipe',
stderr: 'pipe',
timeout: 120 * 1000
};
const serverList: TestConfigWebServer[] = [];
if (!IS_PRODUCTION) {
serverList.push(devServer);
}
serverList.push(webServer);
export default defineConfig({
testDir: './tests',
fullyParallel: true,
fullyParallel: false,
timeout: 90000,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 3 : 0,
workers: 1, // process.env.CI ? 3 : undefined,
reporter: process.env.CI ? [['html', { open: 'never' }], ['github']] : 'list',
forbidOnly: !!IS_CI,
retries: IS_CI ? MAX_RETRIES : 0,
workers: IS_PRODUCTION ? MAX_WORKERS : 1,
reporter: IS_CI ? [['html', { open: 'never' }], ['github']] : 'list',
/* Configure projects for major browsers */
projects: [
@ -26,35 +109,11 @@ export default defineConfig({
],
/* Run your local dev server before starting the tests */
webServer: [
{
command: 'yarn run dev --host',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
timeout: 120 * 1000
},
{
command: 'invoke dev.server -a 127.0.0.1:8000',
env: {
INVENTREE_DEBUG: 'True',
INVENTREE_PLUGINS_ENABLED: 'True',
INVENTREE_ADMIN_URL: 'test-admin',
INVENTREE_SITE_URL: 'http://localhost:8000',
INVENTREE_CORS_ORIGIN_ALLOW_ALL: 'True',
INVENTREE_COOKIE_SAMESITE: 'Lax',
INVENTREE_LOGIN_ATTEMPTS: '100'
},
url: 'http://127.0.0.1:8000/api/',
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
timeout: 120 * 1000
}
],
webServer: serverList,
globalSetup: './playwright/global-setup.ts',
use: {
baseURL: 'http://localhost:5173',
baseURL: IS_PRODUCTION ? 'http://localhost:8000' : 'http://localhost:5173',
headless: IS_PRODUCTION ? true : undefined,
trace: 'on-first-retry'
}
});

3
src/frontend/playwright/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Ignore auth cache
auth/
*.json

View File

@ -0,0 +1,45 @@
import { type FullConfig, chromium } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
import { doCachedLogin } from '../tests/login';
async function globalSetup(config: FullConfig) {
const authDir = path.resolve('./playwright/auth');
if (fs.existsSync(authDir)) {
// Clear out the cached authentication states
fs.rm(path.resolve('./playwright/auth'), { recursive: true }, (err) => {
if (err) {
console.error('Failed to clear out cached authentication states:', err);
} else {
console.log('Removed cached authentication states');
}
});
}
// Perform login for each user
const browser = await chromium.launch();
await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
});
await doCachedLogin(browser, {
username: 'allaccess',
password: 'nolimits'
});
await doCachedLogin(browser, {
username: 'reader',
password: 'readonly'
});
await doCachedLogin(browser, {
username: 'steven',
password: 'wizardstaff'
});
}
export default globalSetup;

View File

@ -10,9 +10,9 @@ export const api = axios.create({});
* Setup default settings for the Axios API instance.
*/
export function setApiDefaults() {
const { host } = useLocalState.getState();
const { getHost } = useLocalState.getState();
api.defaults.baseURL = host;
api.defaults.baseURL = getHost();
api.defaults.timeout = 5000;
api.defaults.withCredentials = true;

View File

@ -1,25 +0,0 @@
import { ActionIcon } from '@mantine/core';
import { IconDeviceFloppy, IconEdit } from '@tabler/icons-react';
export function EditButton({
setEditing,
editing,
disabled,
saveIcon
}: Readonly<{
setEditing: (value?: React.SetStateAction<boolean> | undefined) => void;
editing: boolean;
disabled?: boolean;
saveIcon?: JSX.Element;
}>) {
saveIcon = saveIcon || <IconDeviceFloppy />;
return (
<ActionIcon
onClick={() => setEditing()}
disabled={disabled}
variant='default'
>
{editing ? saveIcon : <IconEdit />}
</ActionIcon>
);
}

View File

@ -1,10 +1,19 @@
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { ActionIcon, Divider, Group, Select, Table, Text } from '@mantine/core';
import {
ActionIcon,
Divider,
Group,
Select,
Table,
Text,
Tooltip
} from '@mantine/core';
import { useToggle } from '@mantine/hooks';
import {
IconApi,
IconCheck,
IconCircleCheck,
IconEdit,
IconInfoCircle,
IconPlugConnected,
IconServer,
@ -15,7 +24,7 @@ import { Wrapper } from '../../pages/Auth/Layout';
import { useServerApiState } from '../../states/ApiState';
import { useLocalState } from '../../states/LocalState';
import type { HostList } from '../../states/states';
import { EditButton } from '../buttons/EditButton';
import { ActionButton } from '../buttons/ActionButton';
import { HostOptionsForm } from './HostOptionsForm';
export function InstanceOptions({
@ -27,7 +36,7 @@ export function InstanceOptions({
ChangeHost: (newHost: string | null) => void;
setHostEdit: () => void;
}>) {
const [HostListEdit, setHostListEdit] = useToggle([false, true] as const);
const [hostListEdit, setHostListEdit] = useToggle([false, true] as const);
const [setHost, setHostList, hostList] = useLocalState((state) => [
state.setHost,
state.setHostList,
@ -48,27 +57,36 @@ export function InstanceOptions({
return (
<Wrapper titleText={t`Select Server`} smallPadding>
<Group gap='xs' wrap='nowrap'>
<Group gap='xs' justify='space-between' wrap='nowrap'>
<Select
style={{ width: '100%' }}
value={hostKey}
onChange={ChangeHost}
data={hostListData}
disabled={HostListEdit}
/>
<EditButton
setEditing={setHostListEdit}
editing={HostListEdit}
disabled={HostListEdit}
/>
<EditButton
setEditing={setHostEdit}
editing={true}
disabled={HostListEdit}
saveIcon={<IconCheck />}
disabled={hostListEdit}
/>
<Group gap='xs' wrap='nowrap'>
<Tooltip label={t`Edit host options`} position='top'>
<ActionButton
variant='transparent'
disabled={hostListEdit}
onClick={setHostListEdit}
icon={<IconEdit />}
/>
</Tooltip>
<Tooltip label={t`Save host selection`} position='top'>
<ActionButton
variant='transparent'
onClick={setHostEdit}
disabled={hostListEdit}
icon={<IconCircleCheck />}
color='green'
/>
</Tooltip>
</Group>
</Group>
{HostListEdit ? (
{hostListEdit ? (
<>
<Divider my={'sm'} />
<Text>

View File

@ -17,11 +17,11 @@ interface ApiImageProps extends ImageProps {
* Construct an image container which will load and display the image
*/
export function ApiImage(props: Readonly<ApiImageProps>) {
const { host } = useLocalState.getState();
const { getHost } = useLocalState.getState();
const imageUrl = useMemo(() => {
return generateUrl(props.src, host);
}, [host, props.src]);
return generateUrl(props.src, getHost());
}, [getHost, props.src]);
return (
<Stack>

View File

@ -65,7 +65,7 @@ export const doBasicLogin = async (
password: string,
navigate: NavigateFunction
) => {
const { host } = useLocalState.getState();
const { getHost } = useLocalState.getState();
const { clearUserState, setAuthenticated, fetchUserState } =
useUserState.getState();
const { setAuthContext } = useServerApiState.getState();
@ -80,6 +80,8 @@ export const doBasicLogin = async (
let loginDone = false;
let success = false;
const host: string = getHost();
// Attempt login with
await api
.post(
@ -156,7 +158,7 @@ export const doLogout = async (navigate: NavigateFunction) => {
};
export const doSimpleLogin = async (email: string) => {
const { host } = useLocalState.getState();
const { getHost } = useLocalState.getState();
const mail = await axios
.post(
apiUrl(ApiEndpoints.user_simple_login),
@ -164,7 +166,7 @@ export const doSimpleLogin = async (email: string) => {
email: email
},
{
baseURL: host,
baseURL: getHost(),
timeout: 2000
}
)

View File

@ -36,10 +36,12 @@ export function getDetailUrl(
* Returns the edit view URL for a given model type
*/
export function generateUrl(url: string | URL, base?: string): string {
const { host } = useLocalState.getState();
const { getHost } = useLocalState.getState();
let newUrl: string | URL = url;
const host: string = getHost();
try {
if (base) {
newUrl = new URL(url, base).toString();

View File

@ -22,7 +22,8 @@ declare global {
server_list: HostList;
default_server: string;
show_server_selector: boolean;
base_url: string;
base_url?: string;
api_host?: string;
sentry_dsn?: string;
environment?: string;
};
@ -30,12 +31,14 @@ declare global {
}
}
// Running in dev mode (i.e. vite)
export const IS_DEV = import.meta.env.DEV;
export const IS_DEMO = import.meta.env.VITE_DEMO === 'true';
export const IS_DEV_OR_DEMO = IS_DEV || IS_DEMO;
// Filter out any settings that are not defined
const loaded_vals = (window.INVENTREE_SETTINGS || {}) as any;
Object.keys(loaded_vals).forEach((key) => {
if (loaded_vals[key] === undefined) {
delete loaded_vals[key];
@ -48,13 +51,9 @@ Object.keys(loaded_vals).forEach((key) => {
window.INVENTREE_SETTINGS = {
server_list: {
'mantine-cqj63coxn': {
host: `${window.location.origin}/`,
name: 'Current Server'
},
...(IS_DEV
? {
'mantine-2j5j5j5j5': {
'server-localhost': {
host: 'http://localhost:8000',
name: 'Localhost'
}
@ -62,21 +61,25 @@ window.INVENTREE_SETTINGS = {
: {}),
...(IS_DEV_OR_DEMO
? {
'mantine-u56l5jt85': {
'server-demo': {
host: 'https://demo.inventree.org/',
name: 'InvenTree Demo'
}
}
: {})
: {}),
'server-current': {
host: `${window.location.origin}/`,
name: 'Current Server'
}
},
default_server: IS_DEV
? 'mantine-2j5j5j5j5'
? 'server-localhost'
: IS_DEMO
? 'mantine-u56l5jt85'
: 'mantine-cqj63coxn',
? 'server-demo'
: 'server-current',
show_server_selector: IS_DEV_OR_DEMO,
// merge in settings that are already set via django's spa_view or for development
// Merge in settings that are already set via django's spa_view or for development
...loaded_vals
};

View File

@ -21,6 +21,7 @@ interface LocalStateProps {
autoupdate: boolean;
toggleAutoupdate: () => void;
host: string;
getHost: () => string;
setHost: (newHost: string, newHostKey: string) => void;
hostKey: string;
hostList: HostList;
@ -65,6 +66,28 @@ export const useLocalState = create<LocalStateProps>()(
toggleAutoupdate: () =>
set((state) => ({ autoupdate: !state.autoupdate })),
host: '',
getHost: () => {
// Retrieve and validate the API host
const state = get();
let host = state.host;
// If the server provides an override for the host, use that
if (window.INVENTREE_SETTINGS?.api_host) {
host = window.INVENTREE_SETTINGS.api_host;
}
// If no host is provided, use the first host in the list
if (!host && Object.keys(state.hostList).length) {
host = Object.values(state.hostList)[0].host;
}
// If no host is provided, fallback to using the current URL (default)
if (!host) {
host = window.location.origin;
}
return host;
},
setHost: (newHost, newHostKey) =>
set({ host: newHost, hostKey: newHostKey }),
hostKey: '',

View File

@ -86,8 +86,7 @@ export const navigate = async (page, url: string) => {
url = `${baseUrl}/${url}`;
}
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle');
await page.goto(url);
};
/**
@ -98,7 +97,6 @@ export const loadTab = async (page, tabName) => {
.getByLabel(/panel-tabs-/)
.getByRole('tab', { name: tabName })
.click();
await page.waitForLoadState('networkidle');
};
// Activate "table" view in certain contexts
@ -121,5 +119,4 @@ export const globalSearch = async (page, query) => {
await page.getByLabel('global-search-input').clear();
await page.getByPlaceholder('Enter search text').fill(query);
await page.waitForTimeout(300);
await page.waitForLoadState('networkidle');
};

View File

@ -1,7 +1,11 @@
import type { Browser, Page } from '@playwright/test';
import { expect } from './baseFixtures.js';
import { baseUrl, logoutUrl, user } from './defaults';
import { user } from './defaults';
import { navigate } from './helpers.js';
import fs from 'node:fs';
import path from 'node:path';
/*
* Perform form based login operation from the "login" URL
*/
@ -9,7 +13,8 @@ export const doLogin = async (page, username?: string, password?: string) => {
username = username ?? user.username;
password = password ?? user.password;
await navigate(page, logoutUrl);
await page.goto('http://localhost:8000/web/logout', { waituntil: 'load' });
await expect(page).toHaveTitle(/^InvenTree.*$/);
await page.waitForURL('**/web/login');
await page.getByLabel('username').fill(username);
@ -19,31 +24,74 @@ export const doLogin = async (page, username?: string, password?: string) => {
await page.waitForTimeout(250);
};
export interface CachedLoginOptions {
username?: string;
password?: string;
url?: string;
}
// Set of users allowed to do cached login
// This is to prevent tests from running with the wrong user
const ALLOWED_USERS: string[] = ['admin', 'allaccess', 'reader', 'steven'];
/*
* Perform a quick login based on passing URL parameters
*/
export const doQuickLogin = async (
page,
username?: string,
password?: string,
url?: string
) => {
username = username ?? user.username;
password = password ?? user.password;
url = url ?? baseUrl;
export const doCachedLogin = async (
browser: Browser,
options?: CachedLoginOptions
): Promise<Page> => {
const username = options?.username ?? user.username;
const password = options?.password ?? user.password;
const url = options?.url ?? '';
await navigate(page, `${url}/login?login=${username}&password=${password}`);
await page.waitForURL('**/web/home');
// FAIL if an unsupported username is provided
if (!ALLOWED_USERS.includes(username)) {
throw new Error(`Invalid username provided to doCachedLogin: ${username}`);
}
// Cache the login state locally - and share between tests
const fn = path.resolve(`./playwright/auth/${username}.json`);
if (fs.existsSync(fn)) {
const page = await browser.newPage({
storageState: fn
});
console.log(`Using cached login state for ${username}`);
await navigate(page, url);
await page.waitForURL('**/web/**');
await page.waitForLoadState('load');
return page;
}
// Create a new blank page
const page = await browser.newPage();
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' });
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.waitForTimeout(250);
await page.waitForLoadState('load');
// Cache the login state
await page.context().storageState({ path: fn });
if (url) {
await navigate(page, url);
}
return page;
};
export const doLogout = async (page) => {
await navigate(page, 'logout');
await page.goto('http://localhost:8000/web/logout', { waitUntil: 'load' });
await page.waitForURL('**/web/login');
};

View File

@ -1,52 +0,0 @@
import { test } from './baseFixtures.js';
import { doQuickLogin } from './login.js';
test('Modals - Admin', async ({ page }) => {
await doQuickLogin(page, 'admin', 'inventree');
// use server info
await page.getByLabel('open-spotlight').click();
await page
.getByRole('button', {
name: 'Server Information About this InvenTree instance'
})
.click();
await page.getByRole('cell', { name: 'Instance Name' }).waitFor();
await page.getByRole('button', { name: 'Close' }).click();
await page.waitForURL('**/web/home');
// use license info
await page.getByLabel('open-spotlight').click();
await page
.getByRole('button', {
name: 'License Information Licenses for dependencies of the service'
})
.click();
await page.getByText('License Information').first().waitFor();
await page.getByRole('tab', { name: 'backend Packages' }).click();
await page.getByRole('button', { name: 'Babel BSD License' }).click();
await page
.getByText('by the Babel Team, see AUTHORS for more information')
.waitFor();
await page.getByRole('tab', { name: 'frontend Packages' }).click();
await page.getByRole('button', { name: '@sentry/core MIT' }).click();
await page
.getByLabel('@sentry/coreMIT')
.getByText('Copyright (c) 2019')
.waitFor();
await page
.getByLabel('License Information')
.getByRole('button')
.first()
.click();
// use about
await page.getByLabel('open-spotlight').click();
await page
.getByRole('button', { name: 'About InvenTree About the InvenTree org' })
.click();
await page.getByRole('cell', { name: 'InvenTree Version' }).click();
});

View File

@ -8,13 +8,15 @@ import {
navigate,
setTableChoiceFilter
} from '../helpers.ts';
import { doQuickLogin } from '../login.ts';
import { doCachedLogin } from '../login.ts';
test('Build Order - Basic Tests', async ({ page }) => {
await doQuickLogin(page);
test('Build Order - Basic Tests', async ({ browser }) => {
const page = await doCachedLogin(browser);
// Navigate to the correct build order
await page.getByRole('tab', { name: 'Manufacturing', exact: true }).click();
await page.getByRole('tab', { name: 'Manufacturing' }).click();
await page.waitForURL('**/manufacturing/index/**');
await loadTab(page, 'Build Orders');
await clearTableFilters(page);
@ -91,8 +93,8 @@ test('Build Order - Basic Tests', async ({ page }) => {
.waitFor();
});
test('Build Order - Calendar', async ({ page }) => {
await doQuickLogin(page);
test('Build Order - Calendar', async ({ browser }) => {
const page = await doCachedLogin(browser);
await navigate(page, 'manufacturing/index/buildorders');
await activateCalendarView(page);
@ -114,8 +116,8 @@ test('Build Order - Calendar', async ({ page }) => {
await page.context().close();
});
test('Build Order - Edit', async ({ page }) => {
await doQuickLogin(page);
test('Build Order - Edit', async ({ browser }) => {
const page = await doCachedLogin(browser);
await navigate(page, 'manufacturing/build-order/22/');
@ -141,8 +143,8 @@ test('Build Order - Edit', async ({ page }) => {
await page.getByRole('button', { name: 'Cancel' }).click();
});
test('Build Order - Build Outputs', async ({ page }) => {
await doQuickLogin(page);
test('Build Order - Build Outputs', async ({ browser }) => {
const page = await doCachedLogin(browser);
await navigate(page, 'manufacturing/index/');
await loadTab(page, 'Build Orders');
@ -217,8 +219,8 @@ test('Build Order - Build Outputs', async ({ page }) => {
await page.getByText('Build outputs have been completed').waitFor();
});
test('Build Order - Allocation', async ({ page }) => {
await doQuickLogin(page);
test('Build Order - Allocation', async ({ browser }) => {
const page = await doCachedLogin(browser);
await navigate(page, 'manufacturing/build-order/1/line-items');
@ -317,8 +319,8 @@ test('Build Order - Allocation', async ({ page }) => {
.waitFor();
});
test('Build Order - Filters', async ({ page }) => {
await doQuickLogin(page);
test('Build Order - Filters', async ({ browser }) => {
const page = await doCachedLogin(browser);
await navigate(page, 'manufacturing/index/buildorders');
@ -351,8 +353,8 @@ test('Build Order - Filters', async ({ page }) => {
await page.getByText('Pending Approval').first().waitFor();
});
test('Build Order - Duplicate', async ({ page }) => {
await doQuickLogin(page);
test('Build Order - Duplicate', async ({ browser }) => {
const page = await doCachedLogin(browser);
await navigate(page, 'manufacturing/build-order/24/details');
await page.getByLabel('action-menu-build-order-').click();

View File

@ -1,9 +1,9 @@
import { test } from '../baseFixtures.js';
import { loadTab, navigate } from '../helpers.js';
import { doQuickLogin } from '../login.js';
import { doCachedLogin } from '../login.js';
test('Company', async ({ page }) => {
await doQuickLogin(page);
test('Company', async ({ browser }) => {
const page = await doCachedLogin(browser);
await navigate(page, 'company/1/details');
await page.getByLabel('Details').getByText('DigiKey Electronics').waitFor();

View File

@ -1,13 +1,13 @@
import { test } from '../baseFixtures.js';
import { loadTab, navigate } from '../helpers.js';
import { doQuickLogin } from '../login.js';
import { doCachedLogin } from '../login.js';
test('Core User/Group/Contact', async ({ page }) => {
await doQuickLogin(page);
test('Core User/Group/Contact', async ({ browser }) => {
const page = await doCachedLogin(browser);
// groups
await navigate(page, '/core');
await page.getByText('System Overview', { exact: true }).click();
await page.getByText('System Overview', { exact: true }).first().click();
await loadTab(page, 'Groups');
await page.getByRole('cell', { name: 'all access' }).click();
await page.getByText('Group: all access', { exact: true }).click();

View File

@ -1,9 +1,9 @@
import { test } from '../baseFixtures.js';
import { doQuickLogin } from '../login.js';
import { doCachedLogin } from '../login.js';
import { setPluginState } from '../settings.js';
test('Dashboard - Basic', async ({ page }) => {
await doQuickLogin(page);
test('Dashboard - Basic', async ({ browser }) => {
const page = await doCachedLogin(browser);
await page.getByText('Use the menu to add widgets').waitFor();
@ -35,7 +35,7 @@ test('Dashboard - Basic', async ({ page }) => {
await page.getByLabel('dashboard-accept-layout').click();
});
test('Dashboard - Plugins', async ({ page, request }) => {
test('Dashboard - Plugins', async ({ browser, request }) => {
// Ensure that the "SampleUI" plugin is enabled
await setPluginState({
request,
@ -43,7 +43,7 @@ test('Dashboard - Plugins', async ({ page, request }) => {
state: true
});
await doQuickLogin(page);
const page = await doCachedLogin(browser);
// Add a dashboard widget from the SampleUI plugin
await page.getByLabel('dashboard-menu').click();

View File

@ -5,15 +5,17 @@ import {
loadTab,
navigate
} from '../helpers';
import { doQuickLogin } from '../login';
import { doCachedLogin } from '../login';
/**
* CHeck each panel tab for the "Parts" page
*/
test('Parts - Tabs', async ({ page }) => {
await doQuickLogin(page);
test('Parts - Tabs', async ({ browser }) => {
const page = await doCachedLogin(browser);
await page.getByRole('tab', { name: 'Parts' }).click();
await page.waitForURL('**/part/category/index/**');
await page
.getByLabel('panel-tabs-partcategory')
.getByRole('tab', { name: 'Parts' })
@ -56,10 +58,8 @@ test('Parts - Tabs', async ({ page }) => {
await loadTab(page, 'Build Orders');
});
test('Parts - Manufacturer Parts', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'part/84/suppliers');
test('Parts - Manufacturer Parts', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/84/suppliers' });
await loadTab(page, 'Suppliers');
await page.getByText('Hammond Manufacturing').click();
@ -69,10 +69,8 @@ test('Parts - Manufacturer Parts', async ({ page }) => {
await page.getByText('1551ACLR - 1551ACLR').waitFor();
});
test('Parts - Supplier Parts', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'part/15/suppliers');
test('Parts - Supplier Parts', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/15/suppliers' });
await loadTab(page, 'Suppliers');
await page.getByRole('cell', { name: 'DIG-84670-SJI' }).click();
@ -82,11 +80,8 @@ test('Parts - Supplier Parts', async ({ page }) => {
await page.getByText('DIG-84670-SJI - R_550R_0805_1%').waitFor();
});
test('Parts - Locking', async ({ page }) => {
await doQuickLogin(page);
// Navigate to a known assembly which is *not* locked
await navigate(page, 'part/104/bom');
test('Parts - Locking', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/104/bom' });
await loadTab(page, 'Bill of Materials');
await page.getByLabel('action-button-add-bom-item').waitFor();
await loadTab(page, 'Parameters');
@ -108,11 +103,9 @@ test('Parts - Locking', async ({ page }) => {
await page.getByText('Part parameters cannot be').waitFor();
});
test('Parts - Allocations', async ({ page }) => {
await doQuickLogin(page);
test('Parts - Allocations', async ({ browser }) => {
// Let's look at the allocations for a single stock item
await navigate(page, 'stock/item/324/');
const page = await doCachedLogin(browser, { url: 'stock/item/324/' });
await loadTab(page, 'Allocations');
await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor();
@ -173,11 +166,9 @@ test('Parts - Allocations', async ({ page }) => {
await page.keyboard.press('Escape');
});
test('Parts - Pricing (Nothing, BOM)', async ({ page }) => {
await doQuickLogin(page);
test('Parts - Pricing (Nothing, BOM)', async ({ browser }) => {
// Part with no history
await navigate(page, 'part/82/pricing');
const page = await doCachedLogin(browser, { url: 'part/82/pricing' });
await page.getByText('Small plastic enclosure, black').waitFor();
await loadTab(page, 'Part Pricing');
@ -223,11 +214,9 @@ test('Parts - Pricing (Nothing, BOM)', async ({ page }) => {
await page.waitForURL('**/part/98/**');
});
test('Parts - Pricing (Supplier)', async ({ page }) => {
await doQuickLogin(page);
test('Parts - Pricing (Supplier)', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/55/pricing' });
// Part
await navigate(page, 'part/55/pricing');
await page.getByText('Ceramic capacitor, 100nF in').waitFor();
await loadTab(page, 'Part Pricing');
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
@ -249,11 +238,8 @@ test('Parts - Pricing (Supplier)', async ({ page }) => {
// await page.waitForURL('**/purchasing/supplier-part/697/');
});
test('Parts - Pricing (Variant)', async ({ page }) => {
await doQuickLogin(page);
// Part
await navigate(page, 'part/106/pricing');
test('Parts - Pricing (Variant)', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/106/pricing' });
await page.getByText('A chair - available in multiple colors').waitFor();
await loadTab(page, 'Part Pricing');
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
@ -275,11 +261,8 @@ test('Parts - Pricing (Variant)', async ({ page }) => {
await page.waitForURL('**/part/109/**');
});
test('Parts - Pricing (Internal)', async ({ page }) => {
await doQuickLogin(page);
// Part
await navigate(page, 'part/65/pricing');
test('Parts - Pricing (Internal)', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/65/pricing' });
await page.getByText('Socket head cap screw, M2').waitFor();
await loadTab(page, 'Part Pricing');
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
@ -300,11 +283,9 @@ test('Parts - Pricing (Internal)', async ({ page }) => {
await page.getByText('Part *M2x4 SHCSSocket head').click();
});
test('Parts - Pricing (Purchase)', async ({ page }) => {
await doQuickLogin(page);
test('Parts - Pricing (Purchase)', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/69/pricing' });
// Part
await navigate(page, 'part/69/pricing');
await page.getByText('1.25mm Pitch, PicoBlade PCB').waitFor();
await loadTab(page, 'Part Pricing');
await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor();
@ -322,10 +303,8 @@ test('Parts - Pricing (Purchase)', async ({ page }) => {
await page.getByText('2022-04-29').waitFor();
});
test('Parts - Attachments', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'part/69/attachments');
test('Parts - Attachments', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/69/attachments' });
// Submit a new external link
await page.getByLabel('action-button-add-external-').click();
@ -344,10 +323,8 @@ test('Parts - Attachments', async ({ page }) => {
await page.getByRole('button', { name: 'Cancel' }).click();
});
test('Parts - Parameters', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'part/69/parameters');
test('Parts - Parameters', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/69/parameters' });
// Create a new template
await page.getByLabel('action-button-add-parameter').click();
@ -371,10 +348,8 @@ test('Parts - Parameters', async ({ page }) => {
await page.getByRole('button', { name: 'Cancel' }).click();
});
test('Parts - Notes', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'part/69/notes');
test('Parts - Notes', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/69/notes' });
// Enable editing
await page.getByLabel('Enable Editing').waitFor();
@ -393,20 +368,16 @@ test('Parts - Notes', async ({ page }) => {
await page.getByLabel('Close Editor').waitFor();
});
test('Parts - 404', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'part/99999/');
test('Parts - 404', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/99999/' });
await page.getByText('Page Not Found', { exact: true }).waitFor();
// Clear out any console error messages
await page.evaluate(() => console.clear());
});
test('Parts - Revision', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'part/906/details');
test('Parts - Revision', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/906/details' });
await page.getByText('Revision of').waitFor();
await page.getByText('Select Part Revision').waitFor();
@ -421,10 +392,10 @@ test('Parts - Revision', async ({ page }) => {
await page.getByText('Select Part Revision').waitFor();
});
test('Parts - Bulk Edit', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'part/category/index/parts');
test('Parts - Bulk Edit', async ({ browser }) => {
const page = await doCachedLogin(browser, {
url: 'part/category/index/parts'
});
// Edit the category for multiple parts
await page.getByLabel('Select record 1', { exact: true }).click();

View File

@ -11,12 +11,14 @@ import {
openFilterDrawer,
setTableChoiceFilter
} from '../helpers.ts';
import { doQuickLogin } from '../login.ts';
import { doCachedLogin } from '../login.ts';
test('Purchase Orders - Table', async ({ page }) => {
await doQuickLogin(page);
test('Purchase Orders - Table', async ({ browser }) => {
const page = await doCachedLogin(browser);
await page.getByRole('tab', { name: 'Purchasing' }).click();
await page.waitForURL('**/purchasing/index/**');
await loadTab(page, 'Purchase Orders');
await activateTableView(page);
@ -42,10 +44,11 @@ test('Purchase Orders - Table', async ({ page }) => {
await page.getByText('2025-07-17').waitFor(); // Target Date
});
test('Purchase Orders - Calendar', async ({ page }) => {
await doQuickLogin(page);
test('Purchase Orders - Calendar', async ({ browser }) => {
const page = await doCachedLogin(browser);
await page.getByRole('tab', { name: 'Purchasing' }).click();
await page.waitForURL('**/purchasing/index/**');
await loadTab(page, 'Purchase Orders');
// Ensure view is in "calendar" mode
@ -66,10 +69,11 @@ test('Purchase Orders - Calendar', async ({ page }) => {
await activateTableView(page);
});
test('Purchase Orders - Barcodes', async ({ page }) => {
await doQuickLogin(page);
test('Purchase Orders - Barcodes', async ({ browser }) => {
const page = await doCachedLogin(browser, {
url: 'purchasing/purchase-order/13/detail'
});
await navigate(page, 'purchasing/purchase-order/13/detail');
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
// Display QR code
@ -126,10 +130,11 @@ test('Purchase Orders - Barcodes', async ({ page }) => {
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
});
test('Purchase Orders - General', async ({ page }) => {
await doQuickLogin(page);
test('Purchase Orders - General', async ({ browser }) => {
const page = await doCachedLogin(browser);
await page.getByRole('tab', { name: 'Purchasing' }).click();
await page.waitForURL('**/purchasing/index/**');
await page.getByRole('cell', { name: 'PO0012' }).click();
await page.waitForTimeout(200);
@ -179,10 +184,15 @@ test('Purchase Orders - General', async ({ page }) => {
await page.getByRole('tab', { name: 'Details' }).waitFor();
});
test('Purchase Orders - Filters', async ({ page }) => {
await doQuickLogin(page, 'reader', 'readonly');
test('Purchase Orders - Filters', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'reader',
password: 'readonly'
});
await page.getByRole('tab', { name: 'Purchasing' }).click();
await page.waitForURL('**/purchasing/index/**');
await loadTab(page, 'Purchase Orders');
await activateTableView(page);
@ -204,11 +214,13 @@ test('Purchase Orders - Filters', async ({ page }) => {
await page.getByRole('option', { name: 'Target Date After' }).waitFor();
});
test('Purchase Orders - Order Parts', async ({ page }) => {
await doQuickLogin(page);
test('Purchase Orders - Order Parts', async ({ browser }) => {
const page = await doCachedLogin(browser);
// Open "Order Parts" wizard from the "parts" table
await page.getByRole('tab', { name: 'Parts' }).click();
await page.waitForURL('**/part/category/index/**');
await page
.getByLabel('panel-tabs-partcategory')
.getByRole('tab', { name: 'Parts' })
@ -284,10 +296,12 @@ test('Purchase Orders - Order Parts', async ({ page }) => {
/**
* Tests for receiving items against a purchase order
*/
test('Purchase Orders - Receive Items', async ({ page }) => {
await doQuickLogin(page);
test('Purchase Orders - Receive Items', async ({ browser }) => {
const page = await doCachedLogin(browser);
await page.getByRole('tab', { name: 'Purchasing' }).click();
await page.waitForURL('**/purchasing/index/**');
await page.getByRole('cell', { name: 'PO0014' }).click();
await loadTab(page, 'Order Details');
@ -351,10 +365,11 @@ test('Purchase Orders - Receive Items', async ({ page }) => {
await page.getByRole('cell', { name: 'bucket' }).first().waitFor();
});
test('Purchase Orders - Duplicate', async ({ page }) => {
await doQuickLogin(page);
test('Purchase Orders - Duplicate', async ({ browser }) => {
const page = await doCachedLogin(browser, {
url: 'purchasing/purchase-order/13/detail'
});
await navigate(page, 'purchasing/purchase-order/13/detail');
await page.getByLabel('action-menu-order-actions').click();
await page.getByLabel('action-menu-order-actions-duplicate').click();

View File

@ -4,15 +4,13 @@ import {
clearTableFilters,
globalSearch,
loadTab,
navigate,
setTableChoiceFilter
} from '../helpers.ts';
import { doQuickLogin } from '../login.ts';
import { doCachedLogin } from '../login.ts';
test('Sales Orders - Tabs', async ({ page }) => {
await doQuickLogin(page);
test('Sales Orders - Tabs', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'sales/index/' });
await navigate(page, 'sales/index/');
await page.waitForURL('**/web/sales/**');
await loadTab(page, 'Sales Orders');
@ -63,10 +61,12 @@ test('Sales Orders - Tabs', async ({ page }) => {
await loadTab(page, 'Notes');
});
test('Sales Orders - Basic Tests', async ({ page }) => {
await doQuickLogin(page);
test('Sales Orders - Basic Tests', async ({ browser }) => {
const page = await doCachedLogin(browser);
await page.getByRole('tab', { name: 'Sales' }).click();
await page.waitForURL('**/sales/index/**');
await loadTab(page, 'Sales Orders');
await clearTableFilters(page);
@ -102,10 +102,12 @@ test('Sales Orders - Basic Tests', async ({ page }) => {
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
});
test('Sales Orders - Shipments', async ({ page }) => {
await doQuickLogin(page);
test('Sales Orders - Shipments', async ({ browser }) => {
const page = await doCachedLogin(browser);
await page.getByRole('tab', { name: 'Sales' }).click();
await page.waitForURL('**/sales/index/**');
await loadTab(page, 'Sales Orders');
await clearTableFilters(page);
@ -131,7 +133,7 @@ test('Sales Orders - Shipments', async ({ page }) => {
await page.getByRole('menuitem', { name: 'Edit' }).click();
// Ensure the form has loaded
await page.waitForTimeout(500);
await page.waitForLoadState('networkidle');
let tracking_number = await page
.getByLabel('text-field-tracking_number')
@ -201,10 +203,11 @@ test('Sales Orders - Shipments', async ({ page }) => {
.click();
});
test('Sales Orders - Duplicate', async ({ page }) => {
await doQuickLogin(page);
test('Sales Orders - Duplicate', async ({ browser }) => {
const page = await doCachedLogin(browser, {
url: 'sales/sales-order/11/detail'
});
await navigate(page, 'sales/sales-order/11/detail');
await page.getByLabel('action-menu-order-actions').click();
await page.getByLabel('action-menu-order-actions-duplicate').click();

View File

@ -1,6 +1,5 @@
import { test } from '../baseFixtures';
import { navigate } from '../helpers';
import { doQuickLogin } from '../login';
import { doCachedLogin } from '../login';
const scan = async (page, barcode) => {
await page.getByLabel('barcode-input-scanner').click();
@ -8,8 +7,8 @@ const scan = async (page, barcode) => {
await page.getByRole('button', { name: 'Scan', exact: true }).click();
};
test('Scanning - Dialog', async ({ page }) => {
await doQuickLogin(page);
test('Scanning - Dialog', async ({ browser }) => {
const page = await doCachedLogin(browser);
await page.getByRole('button', { name: 'Open Barcode Scanner' }).click();
await scan(page, '{"part": 15}');
@ -19,8 +18,8 @@ test('Scanning - Dialog', async ({ page }) => {
await page.getByText('Required:').waitFor();
});
test('Scanning - Basic', async ({ page }) => {
await doQuickLogin(page);
test('Scanning - Basic', async ({ browser }) => {
const page = await doCachedLogin(browser);
// Navigate to the 'scan' page
await page.getByLabel('navigation-menu').click();
@ -40,9 +39,8 @@ test('Scanning - Basic', async ({ page }) => {
await page.getByText('No match found for barcode').waitFor();
});
test('Scanning - Part', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'scan/');
test('Scanning - Part', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'scan/' });
await scan(page, '{"part": 1}');
@ -51,9 +49,8 @@ test('Scanning - Part', async ({ page }) => {
await page.getByRole('cell', { name: 'part', exact: true }).waitFor();
});
test('Scanning - Stockitem', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'scan/');
test('Scanning - Stockitem', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'scan/' });
await scan(page, '{"stockitem": 408}');
await page.getByText('1551ABK').waitFor();
@ -61,9 +58,9 @@ test('Scanning - Stockitem', async ({ page }) => {
await page.getByRole('cell', { name: 'Quantity: 100' }).waitFor();
});
test('Scanning - StockLocation', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'scan/');
test('Scanning - StockLocation', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'scan/' });
await scan(page, '{"stocklocation": 3}');
// stocklocation: 3
@ -74,20 +71,17 @@ test('Scanning - StockLocation', async ({ page }) => {
.waitFor();
});
test('Scanning - SupplierPart', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'scan/');
test('Scanning - SupplierPart', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'scan/' });
await scan(page, '{"supplierpart": 204}');
// supplierpart: 204
await page.waitForTimeout(1000);
await page.waitForLoadState('networkidle');
await page.getByText('1551ABK').first().waitFor();
await page.getByRole('cell', { name: 'supplierpart', exact: true }).waitFor();
});
test('Scanning - PurchaseOrder', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'scan/');
test('Scanning - PurchaseOrder', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'scan/' });
await scan(page, '{"purchaseorder": 12}');
// purchaseorder: 12
@ -98,9 +92,9 @@ test('Scanning - PurchaseOrder', async ({ page }) => {
.waitFor();
});
test('Scanning - SalesOrder', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'scan/');
test('Scanning - SalesOrder', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'scan/' });
await scan(page, '{"salesorder": 6}');
// salesorder: 6
@ -109,9 +103,8 @@ test('Scanning - SalesOrder', async ({ page }) => {
await page.getByRole('cell', { name: 'salesorder', exact: true }).waitFor();
});
test('Scanning - Build', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'scan/');
test('Scanning - Build', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'scan/' });
await scan(page, '{"build": 8}');
// build: 8

View File

@ -7,12 +7,11 @@ import {
openFilterDrawer,
setTableChoiceFilter
} from '../helpers.js';
import { doQuickLogin } from '../login.js';
import { doCachedLogin } from '../login.js';
test('Stock - Basic Tests', async ({ page }) => {
await doQuickLogin(page);
test('Stock - Basic Tests', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'stock/location/index/' });
await navigate(page, 'stock/location/index/');
await page.waitForURL('**/web/stock/location/**');
await loadTab(page, 'Location Details');
@ -39,10 +38,9 @@ test('Stock - Basic Tests', async ({ page }) => {
await loadTab(page, 'Installed Items');
});
test('Stock - Location Tree', async ({ page }) => {
await doQuickLogin(page);
test('Stock - Location Tree', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'stock/location/index/' });
await navigate(page, 'stock/location/index/');
await page.waitForURL('**/web/stock/location/**');
await loadTab(page, 'Location Details');
@ -56,10 +54,13 @@ test('Stock - Location Tree', async ({ page }) => {
await page.getByRole('cell', { name: 'Factory' }).first().waitFor();
});
test('Stock - Filters', async ({ page }) => {
await doQuickLogin(page, 'steven', 'wizardstaff');
test('Stock - Filters', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'steven',
password: 'wizardstaff',
url: '/stock/location/index/'
});
await navigate(page, 'stock/location/index/');
await loadTab(page, 'Stock Items');
await openFilterDrawer(page);
@ -101,8 +102,8 @@ test('Stock - Filters', async ({ page }) => {
await clearTableFilters(page);
});
test('Stock - Serial Numbers', async ({ page }) => {
await doQuickLogin(page);
test('Stock - Serial Numbers', async ({ browser }) => {
const page = await doCachedLogin(browser);
// Use the "global search" functionality to find a part we are interested in
// This is to exercise the search functionality and ensure it is working as expected
@ -167,10 +168,8 @@ test('Stock - Serial Numbers', async ({ page }) => {
/**
* Test various 'actions' on the stock detail page
*/
test('Stock - Stock Actions', async ({ page }) => {
await doQuickLogin(page);
await navigate(page, 'stock/item/1225/details');
test('Stock - Stock Actions', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'stock/item/1225/details' });
// Helper function to launch a stock action
const launchStockAction = async (action: string) => {
@ -231,11 +230,9 @@ test('Stock - Stock Actions', async ({ page }) => {
await page.getByLabel('action-menu-stock-operations-return').click();
});
test('Stock - Tracking', async ({ page }) => {
await doQuickLogin(page);
test('Stock - Tracking', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'stock/item/176/details' });
// Navigate to the "stock item" page
await navigate(page, 'stock/item/176/details/');
await page.getByRole('link', { name: 'Widget Assembly # 2' }).waitFor();
// Navigate to the "stock tracking" tab

View File

@ -1,6 +1,6 @@
import test from '@playwright/test';
import { globalSearch, loadTab, navigate } from './helpers';
import { doQuickLogin } from './login';
import { doCachedLogin } from './login';
// Helper function to open the export data dialog
const openExportDialog = async (page) => {
@ -11,11 +11,12 @@ const openExportDialog = async (page) => {
};
// Test data export for various order types
test('Exporting - Orders', async ({ page }) => {
await doQuickLogin(page, 'steven', 'wizardstaff');
// Download list of purchase orders
await navigate(page, 'purchasing/index/purchase-orders');
test('Exporting - Orders', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'steven',
password: 'wizardstaff',
url: 'purchasing/index/purchase-orders'
});
await openExportDialog(page);
@ -69,8 +70,11 @@ test('Exporting - Orders', async ({ page }) => {
});
// Test for custom BOM exporter
test('Exporting - BOM', async ({ page }) => {
await doQuickLogin(page, 'steven', 'wizardstaff');
test('Exporting - BOM', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'steven',
password: 'wizardstaff'
});
await globalSearch(page, 'MAST');
await page.getByLabel('search-group-results-part').locator('a').click();

View File

@ -1,11 +1,15 @@
/** Unit tests for form validation, rendering, etc */
import test from 'playwright/test';
import { navigate } from './helpers';
import { doQuickLogin } from './login';
import { doCachedLogin } from './login';
test('Forms - Stock Item Validation', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'steven',
password: 'wizardstaff',
url: 'stock/location/index/stock-items'
});
test('Forms - Stock Item Validation', async ({ page }) => {
await doQuickLogin(page, 'steven', 'wizardstaff');
await navigate(page, 'stock/location/index/stock-items');
await page.waitForURL('**/web/stock/location/**');
// Create new stock item form
@ -74,9 +78,12 @@ test('Forms - Stock Item Validation', async ({ page }) => {
await page.getByRole('cell', { name: 'Electronics Lab' }).waitFor();
});
test('Forms - Supplier Validation', async ({ page, request }) => {
await doQuickLogin(page, 'steven', 'wizardstaff');
await navigate(page, 'purchasing/index/suppliers');
test('Forms - Supplier Validation', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'steven',
password: 'wizardstaff',
url: 'purchasing/index/suppliers'
});
await page.waitForURL('**/purchasing/index/**');
await page.getByLabel('action-button-add-company').click();

View File

@ -1,13 +1,16 @@
import { test } from './baseFixtures.js';
import { globalSearch, navigate } from './helpers.js';
import { doQuickLogin } from './login.js';
import { globalSearch } from './helpers.js';
import { doCachedLogin } from './login.js';
/**
* Test for integration of django admin button
*/
test('Admin Button', async ({ page }) => {
await doQuickLogin(page, 'admin', 'inventree');
await navigate(page, 'company/1/details');
test('Admin Button', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree',
url: 'company/1/details'
});
// Click on the admin button
await page.getByLabel(/action-button-open-in-admin/).click();
@ -18,8 +21,11 @@ test('Admin Button', async ({ page }) => {
});
// Tests for the global search functionality
test('Search', async ({ page }) => {
await doQuickLogin(page, 'steven', 'wizardstaff');
test('Search', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'steven',
password: 'wizardstaff'
});
await globalSearch(page, 'another customer');

View File

@ -1,41 +1,7 @@
import { expect, test } from './baseFixtures.js';
import { logoutUrl, user } from './defaults.js';
import { logoutUrl } from './defaults.js';
import { navigate } from './helpers.js';
import { doLogin, doQuickLogin } from './login.js';
test('Login - Basic Test', async ({ page }) => {
await doLogin(page);
// Check that the username is provided
await page.getByText(user.username);
// Logout (via menu)
await page.getByRole('button', { name: 'Ally Access' }).click();
await page.getByRole('menuitem', { name: 'Logout' }).click();
await page.waitForURL('**/web/login');
await page.getByLabel('username');
});
test('Login - Quick Test', async ({ page }) => {
await doQuickLogin(page);
// Check that the username is provided
await page.getByText(user.username);
await expect(page).toHaveTitle(/^InvenTree/);
// Go to the dashboard
await navigate(page, '');
await page.waitForURL('**/web');
await page.getByText('InvenTree Demo Server - ').waitFor();
// Logout (via URL)
await navigate(page, 'logout');
await page.waitForURL('**/web/login');
await page.getByLabel('username');
});
import { doLogin } from './login.js';
/**
* Test various types of login failure
@ -79,7 +45,7 @@ test('Login - Failures', async ({ page }) => {
});
test('Login - Change Password', async ({ page }) => {
await doQuickLogin(page, 'noaccess', 'youshallnotpass');
await doLogin(page, 'noaccess', 'youshallnotpass');
// Navigate to the 'change password' page
await navigate(page, 'settings/user/account');
@ -105,6 +71,4 @@ test('Login - Change Password', async ({ page }) => {
await page.getByText('Password Changed').waitFor();
await page.getByText('The password was set successfully').waitFor();
await page.waitForTimeout(1000);
});

View File

@ -1,8 +1,61 @@
import { systemKey, test } from './baseFixtures.js';
import { doQuickLogin } from './login.js';
import { doCachedLogin } from './login.js';
test('Quick Command', async ({ page }) => {
await doQuickLogin(page);
test('Modals - Admin', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
});
// use server info
await page.getByLabel('open-spotlight').click();
await page
.getByRole('button', {
name: 'Server Information About this InvenTree instance'
})
.click();
await page.getByRole('cell', { name: 'Instance Name' }).waitFor();
await page.getByRole('button', { name: 'Close' }).click();
// use license info
await page.getByLabel('open-spotlight').click();
await page
.getByRole('button', {
name: 'License Information Licenses for dependencies of the service'
})
.click();
await page.getByText('License Information').first().waitFor();
await page.getByRole('tab', { name: 'backend Packages' }).click();
await page.getByRole('button', { name: 'Babel BSD License' }).click();
await page
.getByText('by the Babel Team, see AUTHORS for more information')
.waitFor();
await page.getByRole('tab', { name: 'frontend Packages' }).click();
await page.getByRole('button', { name: '@sentry/core MIT' }).click();
await page
.getByLabel('@sentry/coreMIT')
.getByText('Copyright (c) 2019')
.waitFor();
await page
.getByLabel('License Information')
.getByRole('button')
.first()
.click();
// use about
await page.getByLabel('open-spotlight').click();
await page
.getByRole('button', { name: 'About InvenTree About the InvenTree org' })
.click();
await page.getByRole('cell', { name: 'InvenTree Version' }).click();
});
test('Quick Command', async ({ browser }) => {
const page = await doCachedLogin(browser);
await page.waitForLoadState('networkidle');
// Open Spotlight with Keyboard Shortcut and Search
await page.locator('body').press(`${systemKey}+k`);
@ -10,11 +63,12 @@ test('Quick Command', async ({ page }) => {
await page.getByPlaceholder('Search...').fill('Dashboard');
await page.getByPlaceholder('Search...').press('Tab');
await page.getByPlaceholder('Search...').press('Enter');
await page.waitForURL('**/web/home');
});
test('Quick Command - No Keys', async ({ page }) => {
await doQuickLogin(page);
test('Quick Command - No Keys', async ({ browser }) => {
const page = await doCachedLogin(browser);
await page.waitForLoadState('networkidle');
// Open Spotlight with Button
await page.getByLabel('open-spotlight').click();
@ -23,15 +77,15 @@ test('Quick Command - No Keys', async ({ page }) => {
.click();
await page.getByText('InvenTree Demo Server - ').waitFor();
await page.waitForURL('**/web/home');
// Use navigation menu
await page.getByLabel('open-spotlight').click();
await page
.getByRole('button', { name: 'Open Navigation Open the main' })
.click();
await page.waitForTimeout(1000);
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: 'Open Navigation' }).click();
await page.waitForTimeout(250);
// assert the nav headers are visible
await page.getByText('Navigation').waitFor();
@ -55,8 +109,6 @@ test('Quick Command - No Keys', async ({ page }) => {
await page.getByRole('cell', { name: 'Instance Name' }).waitFor();
await page.getByRole('button', { name: 'Close' }).click();
await page.waitForURL('**/web/home');
// use license info
await page.getByLabel('open-spotlight').click();
await page

View File

@ -6,12 +6,15 @@ import {
loadTab,
navigate
} from './helpers.js';
import { doQuickLogin } from './login.js';
import { doCachedLogin } from './login.js';
import { setPluginState, setSettingState } from './settings.js';
// Unit test for plugin settings
test('Plugins - Settings', async ({ page, request }) => {
await doQuickLogin(page, 'admin', 'inventree');
test('Plugins - Settings', async ({ browser, request }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
});
// Ensure that the SampleIntegration plugin is enabled
await setPluginState({
@ -57,11 +60,14 @@ test('Plugins - Settings', async ({ page, request }) => {
});
// Test base plugin functionality
test('Plugins - Functionality', async ({ page, request }) => {
await doQuickLogin(page, 'admin', 'inventree');
test('Plugins - Functionality', async ({ browser }) => {
// Navigate and select the plugin
await navigate(page, 'settings/admin/plugin/');
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree',
url: 'settings/admin/plugin/'
});
await clearTableFilters(page);
await page.getByPlaceholder('Search').fill('sample');
await page.waitForLoadState('networkidle');
@ -80,8 +86,11 @@ test('Plugins - Functionality', async ({ page, request }) => {
await page.getByText('The plugin was deactivated').waitFor();
});
test('Plugins - Panels', async ({ page, request }) => {
await doQuickLogin(page, 'admin', 'inventree');
test('Plugins - Panels', async ({ browser, request }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
});
// Ensure that UI plugins are enabled
await setSettingState({
@ -108,7 +117,7 @@ test('Plugins - Panels', async ({ page, request }) => {
await loadTab(page, 'Part Details');
// Allow time for the plugin panels to load (they are loaded asynchronously)
await page.waitForTimeout(1000);
await page.waitForLoadState('networkidle');
// Check out each of the plugin panels
await loadTab(page, 'Broken Panel');
@ -139,8 +148,11 @@ test('Plugins - Panels', async ({ page, request }) => {
/**
* Unit test for custom admin integration for plugins
*/
test('Plugins - Custom Admin', async ({ page, request }) => {
await doQuickLogin(page, 'admin', 'inventree');
test('Plugins - Custom Admin', async ({ browser, request }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
});
// Ensure that the SampleUI plugin is enabled
await setPluginState({
@ -170,8 +182,11 @@ test('Plugins - Custom Admin', async ({ page, request }) => {
await page.getByText('hello: world').waitFor();
});
test('Plugins - Locate Item', async ({ page, request }) => {
await doQuickLogin(page, 'admin', 'inventree');
test('Plugins - Locate Item', async ({ browser, request }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
});
// Ensure that the sample location plugin is enabled
await setPluginState({
@ -184,6 +199,7 @@ test('Plugins - Locate Item', async ({ page, request }) => {
// Navigate to the "stock item" page
await navigate(page, 'stock/item/287/');
await page.waitForLoadState('networkidle');
// "Locate" this item
await page.getByLabel('action-button-locate-item').click();

View File

@ -1,6 +1,6 @@
import { expect, test } from './baseFixtures.js';
import { activateTableView, loadTab, navigate } from './helpers.js';
import { doQuickLogin } from './login.js';
import { activateTableView, loadTab } from './helpers.js';
import { doCachedLogin } from './login.js';
import { setPluginState } from './settings.js';
/*
@ -8,10 +8,9 @@ import { setPluginState } from './settings.js';
* Select a number of stock items from the table,
* and print labels against them
*/
test('Label Printing', async ({ page }) => {
await doQuickLogin(page);
test('Label Printing', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'stock/location/index/' });
await navigate(page, 'stock/location/index/');
await page.waitForURL('**/web/stock/location/**');
await loadTab(page, 'Stock Items');
@ -50,10 +49,9 @@ test('Label Printing', async ({ page }) => {
* Navigate to a PurchaseOrder detail page,
* and print a report against it.
*/
test('Report Printing', async ({ page }) => {
await doQuickLogin(page);
test('Report Printing', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'stock/location/index/' });
await navigate(page, 'stock/location/index/');
await page.waitForURL('**/web/stock/location/**');
// Navigate to a specific PurchaseOrder
@ -81,9 +79,11 @@ test('Report Printing', async ({ page }) => {
await page.context().close();
});
test('Report Editing', async ({ page, request }) => {
const [username, password] = ['admin', 'inventree'];
await doQuickLogin(page, username, password);
test('Report Editing', async ({ browser, request }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
});
// activate the sample plugin for this test
await setPluginState({

View File

@ -1,49 +1,45 @@
import { expect, test } from './baseFixtures.js';
import { apiUrl } from './defaults.js';
import { getRowFromCell, loadTab, navigate } from './helpers.js';
import { doQuickLogin } from './login.js';
import { doCachedLogin } from './login.js';
import { setSettingState } from './settings.js';
/**
* Adjust language and color settings
*
* TODO: Reimplement this - without logging out a cached user
*/
test('Settings - Language / Color', async ({ page }) => {
await doQuickLogin(page);
// test('Settings - Language / Color', async ({ browser }) => {
// const page = await doCachedLogin(browser);
await page.getByRole('button', { name: 'Ally Access' }).click();
await page.getByRole('menuitem', { name: 'Logout' }).click();
await page.getByRole('button', { name: 'Send me an email' }).click();
await page.getByLabel('Language toggle').click();
await page.getByLabel('Select language').first().click();
await page.getByRole('option', { name: 'German' }).click();
await page.waitForTimeout(200);
// await page.getByRole('button', { name: 'Ally Access' }).click();
// await page.getByRole('menuitem', { name: 'Logout' }).click();
// await page.getByRole('button', { name: 'Send me an email' }).click();
// await page.getByLabel('Language toggle').click();
// await page.getByLabel('Select language').first().click();
// await page.getByRole('option', { name: 'German' }).click();
// await page.waitForTimeout(200);
await page.getByRole('button', { name: 'Benutzername und Passwort' }).click();
await page.getByPlaceholder('Ihr Benutzername').click();
await page.getByPlaceholder('Ihr Benutzername').fill('admin');
await page.getByPlaceholder('Ihr Benutzername').press('Tab');
await page.getByPlaceholder('Dein Passwort').fill('inventree');
await page.getByRole('button', { name: 'Anmelden' }).click();
await page.waitForTimeout(200);
// await page.getByRole('button', { name: 'Benutzername und Passwort' }).click();
// await page.getByPlaceholder('Ihr Benutzername').click();
// await page.getByPlaceholder('Ihr Benutzername').fill('admin');
// await page.getByPlaceholder('Ihr Benutzername').press('Tab');
// await page.getByPlaceholder('Dein Passwort').fill('inventree');
// await page.getByRole('button', { name: 'Anmelden' }).click();
// await page.waitForTimeout(200);
// Note: changes to the dashboard have invalidated these tests (for now)
// await page
// .locator('span')
// .filter({ hasText: 'AnzeigeneinstellungenFarbmodusSprache' })
// .getByRole('button')
// .click();
// await page
// .locator('span')
// .filter({ hasText: 'AnzeigeneinstellungenFarbmodusSprache' })
// .getByRole('button')
// .click();
// await page.getByRole('tab', { name: 'Dashboard' }).click();
// await page.waitForURL('**/web/home');
// });
await page.getByRole('tab', { name: 'Dashboard' }).click();
await page.waitForURL('**/web/home');
});
test('Settings - User theme', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'allaccess',
password: 'nolimits'
});
await page.waitForLoadState('networkidle');
test('Settings - User theme', async ({ page }) => {
await doQuickLogin(page);
await page.getByRole('button', { name: 'Ally Access' }).click();
await page.getByRole('menuitem', { name: 'Account settings' }).click();
@ -82,14 +78,14 @@ test('Settings - User theme', async ({ page }) => {
// primary
await page.getByLabel('#fab005').click();
await page.getByLabel('#228be6').click();
// language
await page.getByRole('button', { name: 'Use pseudo language' }).click();
});
test('Settings - Admin', async ({ page }) => {
test('Settings - Admin', async ({ browser }) => {
// Note here we login with admin access
await doQuickLogin(page, 'admin', 'inventree');
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
});
// User settings
await page.getByRole('button', { name: 'admin' }).click();
@ -184,9 +180,12 @@ test('Settings - Admin', async ({ page }) => {
await page.getByRole('button', { name: 'Submit' }).click();
});
test('Settings - Admin - Barcode History', async ({ page, request }) => {
test('Settings - Admin - Barcode History', async ({ browser, request }) => {
// Login with admin credentials
await doQuickLogin(page, 'admin', 'inventree');
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
});
// Ensure that the "save scans" setting is enabled
await setSettingState({
@ -221,11 +220,14 @@ test('Settings - Admin - Barcode History', async ({ page, request }) => {
});
});
test('Settings - Admin - Unauthorized', async ({ page }) => {
test('Settings - Admin - Unauthorized', async ({ browser }) => {
// Try to access "admin" page with a non-staff user
await doQuickLogin(page, 'allaccess', 'nolimits');
const page = await doCachedLogin(browser, {
username: 'allaccess',
password: 'nolimits',
url: 'settings/admin/'
});
await navigate(page, 'settings/admin/');
await page.waitForURL('**/settings/admin/**');
// Should get a permission denied message
@ -252,9 +254,12 @@ test('Settings - Admin - Unauthorized', async ({ page }) => {
});
// Test for user auth configuration
test('Settings - Auth - Email', async ({ page }) => {
await doQuickLogin(page, 'allaccess', 'nolimits');
await navigate(page, 'settings/user/');
test('Settings - Auth - Email', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'allaccess',
password: 'nolimits',
url: 'settings/user/'
});
await loadTab(page, 'Security');
@ -269,9 +274,8 @@ test('Settings - Auth - Email', async ({ page }) => {
await page.getByRole('button', { name: 'Remove' }).click();
await page.getByText('Currently no email addresses are registered').waitFor();
await page.waitForTimeout(2500);
});
async function testColorPicker(page, ref: string) {
const element = page.getByLabel(ref);
await element.click();

View File

@ -4,13 +4,11 @@ import {
navigate,
setTableChoiceFilter
} from './helpers.js';
import { doQuickLogin } from './login.js';
test('Tables - Filters', async ({ page }) => {
await doQuickLogin(page);
import { doCachedLogin } from './login.js';
test('Tables - Filters', async ({ browser }) => {
// Head to the "build order list" page
await navigate(page, 'manufacturing/index/');
const page = await doCachedLogin(browser, { url: 'manufacturing/index/' });
await clearTableFilters(page);
@ -41,11 +39,11 @@ test('Tables - Filters', async ({ page }) => {
await clearTableFilters(page);
});
test('Tables - Columns', async ({ page }) => {
await doQuickLogin(page);
test('Tables - Columns', async ({ browser }) => {
// Go to the "stock list" page
await navigate(page, 'stock/location/index/stock-items');
const page = await doCachedLogin(browser, {
url: 'stock/location/index/stock-items'
});
// Open column selector
await page.getByLabel('table-select-columns').click();
@ -64,6 +62,4 @@ test('Tables - Columns', async ({ page }) => {
await page.getByRole('menuitem', { name: 'Target Date' }).click();
await page.getByRole('menuitem', { name: 'Reference', exact: true }).click();
await page.getByRole('menuitem', { name: 'Project Code' }).click();
await page.waitForTimeout(1000);
});

View File

@ -1,9 +1,12 @@
import { test } from '../baseFixtures';
import { navigate } from '../helpers';
import { doQuickLogin } from '../login';
import { doCachedLogin } from '../login';
test('PUI - Admin - Parameter', async ({ page }) => {
await doQuickLogin(page, 'admin', 'inventree');
test('PUI - Admin - Parameter', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'admin',
password: 'inventree'
});
await page.getByRole('button', { name: 'admin' }).click();
await page.getByRole('menuitem', { name: 'Admin Center' }).click();
await page.getByRole('tab', { name: 'Part Parameters' }).click();

View File

@ -6,66 +6,77 @@ import license from 'rollup-plugin-license';
import { defineConfig, splitVendorChunkPlugin } from 'vite';
import istanbul from 'vite-plugin-istanbul';
// Detect if the current environment is WSL
// Required for enabling file system polling
const IS_IN_WSL = platform().includes('WSL') || release().includes('WSL');
const is_coverage = process.env.VITE_COVERAGE === 'true';
// Detect if code coverage is enabled (runs in GitHub CI)
const IS_COVERAGE = !!process.env.VITE_COVERAGE_BUILD;
if (IS_IN_WSL) {
console.log('WSL detected: using polling for file system events');
}
// Output directory for the built files
const OUTPUT_DIR = '../../src/backend/InvenTree/web/static/web';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['macros']
}
}),
vanillaExtractPlugin(),
splitVendorChunkPlugin(),
license({
sourcemap: true,
thirdParty: {
includePrivate: true,
multipleVersions: true,
output: {
file: '../backend/InvenTree/web/static/web/.vite/dependencies.json',
template(dependencies) {
return JSON.stringify(dependencies);
export default defineConfig(({ command, mode }) => {
return {
plugins: [
react({
babel: {
plugins: ['macros']
}
}),
vanillaExtractPlugin(),
splitVendorChunkPlugin(),
license({
sourcemap: true,
thirdParty: {
includePrivate: true,
multipleVersions: true,
output: {
file: '../backend/InvenTree/web/static/web/.vite/dependencies.json',
template(dependencies) {
return JSON.stringify(dependencies);
}
}
}
}
}),
istanbul({
include: 'src/*',
exclude: ['node_modules', 'test/'],
extension: ['.js', '.ts', '.tsx'],
requireEnv: true
}),
codecovVitePlugin({
enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined,
bundleName: 'pui_v1',
uploadToken: process.env.CODECOV_TOKEN
})
],
base: '',
build: {
manifest: true,
outDir: '../../src/backend/InvenTree/web/static/web',
sourcemap: is_coverage
},
server: {
proxy: {
'/media': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: true
}
}),
istanbul({
include: 'src/*',
exclude: ['node_modules', 'test/'],
extension: ['.js', '.ts', '.tsx'],
requireEnv: true
}),
codecovVitePlugin({
enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined,
bundleName: 'pui_v1',
uploadToken: process.env.CODECOV_TOKEN
})
],
// When building, set the base path to an empty string
// This is required to ensure that the static path prefix is observed
base: command == 'build' ? '' : undefined,
build: {
manifest: true,
outDir: OUTPUT_DIR,
sourcemap: IS_COVERAGE
},
watch: {
// use polling only for WSL as the file system doesn't trigger notifications for Linux apps
// ref: https://github.com/vitejs/vite/issues/1153#issuecomment-785467271
usePolling: IS_IN_WSL
server: {
proxy: {
'/media': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: true
}
},
watch: {
// Use polling only for WSL as the file system doesn't trigger notifications for Linux apps
// Ref: https://github.com/vitejs/vite/issues/1153#issuecomment-785467271
usePolling: IS_IN_WSL
}
}
}
};
});

View File

@ -1519,6 +1519,11 @@
resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
"@messageformat/date-skeleton@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz#3bad068cbf5873d14592cfc7a73dd4d8615e2739"
integrity sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==
"@messageformat/parser@^5.0.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@messageformat/parser/-/parser-5.1.0.tgz#05e4851c782d633ad735791dd0a68ee65d2a7201"
@ -1951,10 +1956,10 @@
dependencies:
undici-types "~6.19.2"
"@types/node@^22.6.0":
version "22.10.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.7.tgz#14a1ca33fd0ebdd9d63593ed8d3fbc882a6d28d7"
integrity sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==
"@types/node@^22.13.14":
version "22.13.14"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.14.tgz#70d84ec91013dcd2ba2de35532a5a14c2b4cc912"
integrity sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==
dependencies:
undici-types "~6.20.0"
@ -3312,6 +3317,11 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
inquirer@^7.3.3:
version "7.3.3"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
@ -3996,6 +4006,14 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
path@^0.12.7:
version "0.12.7"
resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==
dependencies:
process "^0.11.1"
util "^0.10.3"
pathe@^1.1.0, pathe@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec"
@ -4112,6 +4130,11 @@ process-on-spawn@^1.0.0:
dependencies:
fromentries "^1.2.0"
process@^0.11.1:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
prop-types@15.x, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
@ -4954,6 +4977,13 @@ util-deprecate@^1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
util@^0.10.3:
version "0.10.4"
resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
dependencies:
inherits "2.0.3"
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"