mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +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:
		
							
								
								
									
										28
									
								
								.github/workflows/qc_checks.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/qc_checks.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||
|   | ||||
| @@ -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', []) | ||||
|  | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
|         if not settings.DEBUG: | ||||
|             logger.warning('Background worker check failed') | ||||
|  | ||||
|     if not InvenTree.helpers_email.is_email_configured():  # pragma: no cover | ||||
|         result = False | ||||
|         if not settings.DEBUG: | ||||
|             logger.warning('Email backend not configured') | ||||
|  | ||||
|     if not result:  # pragma: no cover | ||||
|         if not settings.DEBUG: | ||||
|             logger.warning('InvenTree system health checks failed') | ||||
|  | ||||
|     return result | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										3
									
								
								src/frontend/playwright/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Ignore auth cache | ||||
| auth/ | ||||
| *.json | ||||
							
								
								
									
										45
									
								
								src/frontend/playwright/global-setup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/frontend/playwright/global-setup.ts
									
									
									
									
									
										Normal 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; | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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> | ||||
|   ); | ||||
| } | ||||
| @@ -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} | ||||
|           disabled={hostListEdit} | ||||
|         /> | ||||
|         <EditButton | ||||
|           setEditing={setHostListEdit} | ||||
|           editing={HostListEdit} | ||||
|           disabled={HostListEdit} | ||||
|         <Group gap='xs' wrap='nowrap'> | ||||
|           <Tooltip label={t`Edit host options`} position='top'> | ||||
|             <ActionButton | ||||
|               variant='transparent' | ||||
|               disabled={hostListEdit} | ||||
|               onClick={setHostListEdit} | ||||
|               icon={<IconEdit />} | ||||
|             /> | ||||
|         <EditButton | ||||
|           setEditing={setHostEdit} | ||||
|           editing={true} | ||||
|           disabled={HostListEdit} | ||||
|           saveIcon={<IconCheck />} | ||||
|           </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> | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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 | ||||
|       } | ||||
|     ) | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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 | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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: '', | ||||
|   | ||||
| @@ -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'); | ||||
| }; | ||||
|   | ||||
| @@ -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'); | ||||
| }; | ||||
|   | ||||
| @@ -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(); | ||||
| }); | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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(); | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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'); | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
| }); | ||||
|   | ||||
| @@ -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 | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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({ | ||||
|   | ||||
| @@ -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' | ||||
|   }); | ||||
|  | ||||
| test('Settings - User theme', async ({ page }) => { | ||||
|   await doQuickLogin(page); | ||||
|   await page.waitForLoadState('networkidle'); | ||||
|  | ||||
|   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(); | ||||
|   | ||||
| @@ -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); | ||||
| }); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -6,15 +6,23 @@ 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({ | ||||
| export default defineConfig(({ command, mode }) => { | ||||
|   return { | ||||
|     plugins: [ | ||||
|       react({ | ||||
|         babel: { | ||||
| @@ -48,11 +56,13 @@ export default defineConfig({ | ||||
|         uploadToken: process.env.CODECOV_TOKEN | ||||
|       }) | ||||
|     ], | ||||
|   base: '', | ||||
|     // 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: '../../src/backend/InvenTree/web/static/web', | ||||
|     sourcemap: is_coverage | ||||
|       outDir: OUTPUT_DIR, | ||||
|       sourcemap: IS_COVERAGE | ||||
|     }, | ||||
|     server: { | ||||
|       proxy: { | ||||
| @@ -63,9 +73,10 @@ export default defineConfig({ | ||||
|         } | ||||
|       }, | ||||
|       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 | ||||
|         // 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 | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| }); | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user