From 2e4bf3739ff50dca6851a4ffb3752566b146c150 Mon Sep 17 00:00:00 2001 From: Xhivo <35744300+xhivo97@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:21:12 +0200 Subject: [PATCH 1/4] Add HMR and React Fast Refresh support (#12060) * Add HMR and React Fast Refresh support * Run pre-commit hooks * Fix 'hmrSetModule' module loading The incoming module needs to include the URL from which it was loaded, so that it's possible to enforce only loading modules imported from the same pathname as the current module. * Add error handling and improvements - Add error handling to `useRemotePlugin` and simplify `RemoteComponent` - Improve HMR to use a registry instead of a single global callback. This should now handle two legacy plugin entry points being used at the same time via RemoteComponent. * Update docs * Update CHANGELOG * Remove use of LanguageContext from RemoteComponent LanguageContext should not be necessary here, as it's provided in ThemeContext, which is used in InvenTree's frontend entry. * Fix incorrect import.meta.hot access * Update Playwright test to match UI text changes --------- Co-authored-by: Oliver --- docs/docs/plugins/creator.md | 4 +- docs/docs/plugins/walkthrough.md | 8 +- src/frontend/CHANGELOG.md | 4 + .../components/plugins/RemoteComponent.tsx | 157 ++++-------------- src/frontend/src/hooks/UseRemotePlugin.tsx | 157 ++++++++++++++++++ src/frontend/tests/pui_plugins.spec.ts | 2 +- 6 files changed, 201 insertions(+), 131 deletions(-) create mode 100644 src/frontend/src/hooks/UseRemotePlugin.tsx diff --git a/docs/docs/plugins/creator.md b/docs/docs/plugins/creator.md index 39bee77aa8..74d4acd2b0 100644 --- a/docs/docs/plugins/creator.md +++ b/docs/docs/plugins/creator.md @@ -214,10 +214,10 @@ The frontend code for your plugin is located in the `frontend/src` directory. Yo Refer to the `./frontend/src/Panel.tsx` file as a starting point. This is where the custom panel for the part detail page is implemented. You can modify this file to change the content and behavior of the panel. -While the `npm dev` server is running, any changes you make to the frontend code will be automatically reloaded allowing for rapid development and testing of your plugin's frontend features. This avoids the need to rebuild the frontend code every time you make a change. +While the `npm dev` server is running, any changes to the frontend are reflected in the browser using React Fast Refresh, allowing for rapid development without rebuilding the frontend with every change. !!! info "Page Reload" - Due to the way the InvenTree frontend is structured, you will need to manually refresh the page in your browser to see changes to the frontend code. The development server will automatically reload the frontend code, but the InvenTree server needs to be aware of the changes. + All exports in plugin modules that export React components must start with a capital letter. Otherwise, React Fast Refresh will fall back to a full page reload instead of performing a component-level update. Additionally, any render functions referenced from Python must also be capitalized. ## Build Plugin diff --git a/docs/docs/plugins/walkthrough.md b/docs/docs/plugins/walkthrough.md index af2d75885e..5fe885e233 100644 --- a/docs/docs/plugins/walkthrough.md +++ b/docs/docs/plugins/walkthrough.md @@ -119,7 +119,7 @@ function AttachmentCarouselPanel({context}: {context: InvenTreePluginContext;}) } // This is the function which is called by InvenTree to render the actual panel component -export function renderAttachmentCarouselPanel(context: InvenTreePluginContext) { +export function RenderAttachmentCarouselPanel(context: InvenTreePluginContext) { checkPluginVersion(context); return ; } @@ -300,7 +300,7 @@ Back to the walkthrough, open `core.py` in the `attachment_carousel` folder and 'title': 'Attachment Carousel', 'description': 'Custom panel description', 'icon': 'ti:carousel-horizontal:outline', - 'source': self.plugin_static_file('Panel.js:renderAttachmentCarouselPanel'), + 'source': self.plugin_static_file('Panel.js:RenderAttachmentCarouselPanel'), 'context': { # Provide additional context data to the panel 'settings': self.get_settings_dict(), @@ -385,7 +385,7 @@ panels.append({ 'description': 'Custom panel description', - 'icon': 'ti:mood-smile:outline', + 'icon': 'ti:carousel-horizontal:outline', - 'source': self.plugin_static_file('Panel.js:renderAttachmentCarouselPanel'), + 'source': self.plugin_static_file('Panel.js:RenderAttachmentCarouselPanel'), 'context': { # Provide additional context data to the panel 'settings': self.get_settings_dict(), @@ -519,7 +519,7 @@ panels.append({ 'title': 'Attachment Carousel', 'description': 'Custom panel description', 'icon': 'ti:carousel-horizontal:outline', - 'source': self.plugin_static_file('Panel.js:renderAttachmentCarouselPanel'), + 'source': self.plugin_static_file('Panel.js:RenderAttachmentCarouselPanel'), 'context': { # Provide additional context data to the panel 'settings': self.get_settings_dict(), diff --git a/src/frontend/CHANGELOG.md b/src/frontend/CHANGELOG.md index 158082281c..c9c55f7a16 100644 --- a/src/frontend/CHANGELOG.md +++ b/src/frontend/CHANGELOG.md @@ -27,6 +27,10 @@ Exposes sub-components related to DetailDrawer rendering: - `DetailDrawerComponent` - `useLocalLibState` +#### Plugin System + +Enable React Fast Refresh support for plugin frontend development. Plugin modules exporting React components must start with a capital letter; otherwise, a full page reload occurs instead of a component-level update. + ### 0.11.3 - April 2026 Exposes additional type definitions related to rendering drawers from tables: diff --git a/src/frontend/src/components/plugins/RemoteComponent.tsx b/src/frontend/src/components/plugins/RemoteComponent.tsx index 671dc7c5a0..ad01ac30d7 100644 --- a/src/frontend/src/components/plugins/RemoteComponent.tsx +++ b/src/frontend/src/components/plugins/RemoteComponent.tsx @@ -1,17 +1,12 @@ import { t } from '@lingui/core/macro'; -import { Alert, MantineProvider, Stack, Text } from '@mantine/core'; -import { IconExclamationCircle } from '@tabler/icons-react'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Alert, Stack, Text } from '@mantine/core'; +import { useRef } from 'react'; import { Boundary } from '@lib/components/Boundary'; import { identifierString } from '@lib/functions/Conversion'; import type { InvenTreePluginContext } from '@lib/types/Plugins'; -import { type Root, createRoot } from 'react-dom/client'; -import { api, queryClient } from '../../App'; -import { ApiProvider } from '../../contexts/ApiContext'; -import { LanguageContext } from '../../contexts/LanguageContext'; -import { useLocalState } from '../../states/LocalState'; -import { findExternalPluginFunction } from './PluginSource'; +import { IconExclamationCircle } from '@tabler/icons-react'; +import { useRemotePlugin } from '../../hooks/UseRemotePlugin'; /** * A remote component which can be used to display plugin content. @@ -31,125 +26,39 @@ export default function RemoteComponent({ defaultFunctionName: string; context: InvenTreePluginContext; }>) { - const componentRef = useRef(null); - const rootElement = useRef(null); + const containerRef = useRef(null); - useEffect(() => { - if (componentRef.current && rootElement.current === null) { - rootElement.current = createRoot(componentRef.current); - } - }, [rootElement]); + const { componentFn, errorMsg, exportName, pluginContext, remountKey } = + useRemotePlugin({ + context, + source, + defaultFunctionName, + containerRef + }); - const [renderingError, setRenderingError] = useState( - undefined + const content = componentFn ? ( + componentFn(pluginContext) + ) : ( +
); - const func: string = useMemo(() => { - // Attempt to extract the function name from the source - const { getHost } = useLocalState.getState(); - const url = new URL(source, getHost()); - - if (url.pathname.includes(':')) { - const parts = url.pathname.split(':'); - return parts[1] || defaultFunctionName; // Use the second part as the function name, or fallback to default - } else { - return defaultFunctionName; - } - }, [source, defaultFunctionName]); - - const reloadPluginContent = useCallback(() => { - if (!rootElement.current) { - return; - } - - const ctx: InvenTreePluginContext = { - ...context, - reloadContent: reloadPluginContent - }; - - if (source && defaultFunctionName) { - findExternalPluginFunction(source, func) - .then((func) => { - if (!!func) { - try { - if (func.length > 1) { - // Support "legacy" plugin functions which call createRoot() internally - // Ref: https://github.com/inventree/InvenTree/pull/9439/ - func(componentRef.current, ctx); - } else { - // Render the plugin component into the target element - // Note that we have to provide the right context(s) to the component - // This approach ensures that the component is rendered in the correct context tree - rootElement.current?.render( - - - {func(ctx)} - - - ); - } - - setRenderingError(''); - } catch (error) { - setRenderingError(`${error}`); - console.error(error); - } - } else { - setRenderingError(`${source} / ${func}`); - } - }) - .catch((_error) => { - console.error( - `ERR: Failed to load remote plugin function: ${source} /${func}` - ); - }); - } else { - setRenderingError( - `${t`Invalid source or function name`} - ${source} /${func}` - ); - } - }, [ - componentRef.current, - rootElement.current, - source, - defaultFunctionName, - context - ]); - - // Reload the plugin content dynamically - useEffect(() => { - reloadPluginContent(); - }, [ - func, - rootElement.current, - context.id, - context.model, - context.instance, - context.user, - context.colorScheme, - context.locale, - context.context - ]); - return ( - - - {renderingError && ( - } - > - - {t`Error occurred while loading plugin content`}: {renderingError} - - - )} - {componentRef &&
} - - + + {errorMsg && ( + } + > + {errorMsg} + + )} + + {content} + + ); } diff --git a/src/frontend/src/hooks/UseRemotePlugin.tsx b/src/frontend/src/hooks/UseRemotePlugin.tsx new file mode 100644 index 0000000000..8d0460cfe4 --- /dev/null +++ b/src/frontend/src/hooks/UseRemotePlugin.tsx @@ -0,0 +1,157 @@ +import type { InvenTreePluginContext } from '@lib/types/Plugins'; +import { t } from '@lingui/core/macro'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import type { ReactElement } from 'react'; +import { useLocalState } from '../states/LocalState'; + +type LegacyPluginEntryFn = ( + container: HTMLDivElement, + ctx: InvenTreePluginContext +) => void; + +type PluginEntryFn = (ctx: InvenTreePluginContext) => ReactElement; + +type UseRemotePluginOptions = { + context: InvenTreePluginContext; + source: string; + defaultFunctionName: string; + containerRef: React.RefObject; +}; + +type UsePluginSourceOptions = { + source: string; + defaultFunctionName?: string; +}; + +type UseRemotePluginReturn = { + componentFn: PluginEntryFn | null; + errorMsg: string | null; + exportName: string; + pluginContext: InvenTreePluginContext; + remountKey: number; +}; + +function usePluginSource({ + source, + defaultFunctionName +}: UsePluginSourceOptions) { + const { getHost } = useLocalState.getState(); + + const { moduleUrl, exportName } = useMemo(() => { + const url = new URL(source, getHost()); + const parts = url.pathname.split(':'); + + return { + exportName: parts[1] || defaultFunctionName || 'default', + moduleUrl: url.origin + parts[0] + }; + }, [source, defaultFunctionName, getHost]); + + return { moduleUrl, exportName }; +} + +function getHmrCallbacks(url: string) { + const w = window as any; + w.__plugin_hmr_callbacks ??= {}; + w.__plugin_hmr_callbacks[url] ??= new Set(); + return w.__plugin_hmr_callbacks[url]; +} + +const hasHmr = import.meta.hot !== undefined; + +export function useRemotePlugin({ + context, + source, + defaultFunctionName, + containerRef +}: UseRemotePluginOptions): UseRemotePluginReturn { + const { moduleUrl, exportName } = usePluginSource({ + source, + defaultFunctionName + }); + + const [remoteModule, setRemoteModule] = useState | null>(null); + const [reloadVersion, setReloadVersion] = useState(0); + const [errorMsg, setErrorMsg] = useState(null); + + const reloadContent = useCallback(() => setReloadVersion((v) => v + 1), []); + + const hmrSetModule = useCallback( + (newRemoteModule: Record | null) => { + if (!hasHmr) return; + setRemoteModule(newRemoteModule); + }, + [] + ); + + useEffect(() => { + let cancelled = false; + + setErrorMsg(null); + + const loadModule = async () => { + try { + const mod = await import(/* @vite-ignore */ moduleUrl); + if (!cancelled) setRemoteModule(mod); + } catch (err) { + if (!cancelled) { + console.error(`ERR: Failed to load module: ${moduleUrl}:\n${err}`); + setErrorMsg(t`Failed to load module: ${moduleUrl}`); + } + } + }; + + loadModule(); + + return () => { + cancelled = true; + }; + }, [moduleUrl]); + + const [legacyRenderFn, componentFn, error] = useMemo(() => { + if (!remoteModule) return [null, null, null]; + + let err: string | null = null; + const func = remoteModule[exportName]; + + if (typeof func === 'function') { + if (func.length === 2) { + return [func as LegacyPluginEntryFn, null, null]; + } else if (func.length === 1) { + return [null, func as PluginEntryFn, null]; + } else { + err = `Entrypoint ${exportName} in ${moduleUrl} must accept 1-2 arguments`; + } + } else if (func !== undefined) { + err = t`Export ${exportName} in ${moduleUrl} is not a function (found type ${typeof func}).`; + } else { + err = t`Plugin entrypoint ${exportName} does not exist in ${moduleUrl}.`; + } + + return [null, null, err]; + }, [remoteModule, exportName]); + + useEffect(() => { + if (legacyRenderFn && containerRef.current) { + containerRef.current.innerHTML = ''; + legacyRenderFn(containerRef.current, context); + + if (hasHmr) getHmrCallbacks(moduleUrl)?.add(hmrSetModule); + } + + return () => { + if (hasHmr) getHmrCallbacks(moduleUrl)?.delete(hmrSetModule); + }; + }, [moduleUrl, legacyRenderFn, context, hmrSetModule]); + + return { + componentFn: componentFn, + errorMsg: error ?? errorMsg, + exportName: exportName, + pluginContext: { ...context, reloadContent: reloadContent }, + remountKey: reloadVersion + }; +} diff --git a/src/frontend/tests/pui_plugins.spec.ts b/src/frontend/tests/pui_plugins.spec.ts index a4bc753303..c6835418d1 100644 --- a/src/frontend/tests/pui_plugins.spec.ts +++ b/src/frontend/tests/pui_plugins.spec.ts @@ -174,7 +174,7 @@ test('Plugins - Panels', async ({ browser }) => { // Check out each of the plugin panels await loadTab(page, 'Broken Panel'); - await page.getByText('Error occurred while loading plugin content').waitFor(); + await page.getByText('Error Loading Plugin Content').waitFor(); await loadTab(page, 'Dynamic Panel'); await page.getByText('Instance ID: 69'); await page From 88524ac6d56b39a65e58e7da9648d12fbc29d16e Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 6 Jun 2026 07:55:49 +1000 Subject: [PATCH 2/4] fix(forntend): generate UI coverage again (#12066) * Attempt to fix UI coverage * Update CI workflows: - use test sharding - Only upload coverage on master * Restore line * Simplify test * Simplify test matrix * Fix env vars * Adjust matrix * Adjust output names * Fix paths * Simplify qc_checks * Revert missing line * Simplify coverage calls * Run firefox test against port 8000 * Fix VITE_COVERAGE env var * Capture browser name in report output * Increase timeout again * Enhanced feedback from playwright startup * Split UI checks into separate file * Fix workflow deps * Shard chromium build * Adjust reporter type * Reduce uncessesary build steps * Tweak paths filter * Reduce retries * Also generate HTML reports * Tweak reporter output * Fix custom splash URLs * Fix envs for customization tests * Shard the firefox runner too * Ignore customization tests for firefox too * Don't upload if tests fail * Fix triggers * Remove merged test coverage * Pin download action * Error if no artifact files found * Update ignore dirs * Adjust baseFixtures * Fix for teardown in baseFixtures.ts * Fix path for coverage files * include hidden files --- .github/actions/setup/action.yaml | 2 +- .github/workflows/frontend.yaml | 306 ++++++++++++++++++ .github/workflows/qc_checks.yaml | 127 -------- src/frontend/playwright.config.ts | 33 +- src/frontend/tests/baseFixtures.ts | 93 ++++-- .../tests/customization/customization.spec.ts | 9 +- src/frontend/tests/pages/pui_return.spec.ts | 2 +- src/frontend/tests/pui_exporting.spec.ts | 2 +- src/frontend/tests/pui_forms.spec.ts | 4 +- src/frontend/tests/pui_importing.spec.ts | 2 +- src/frontend/tests/pui_login.spec.ts | 3 +- src/frontend/tests/pui_machines.spec.ts | 2 +- src/frontend/tests/pui_permissions.spec.ts | 2 +- src/frontend/tests/pui_plugins.spec.ts | 2 +- src/frontend/vite.config.ts | 2 +- 15 files changed, 415 insertions(+), 176 deletions(-) create mode 100644 .github/workflows/frontend.yaml diff --git a/.github/actions/setup/action.yaml b/.github/actions/setup/action.yaml index 3c7526de71..f312390970 100644 --- a/.github/actions/setup/action.yaml +++ b/.github/actions/setup/action.yaml @@ -106,7 +106,7 @@ runs: - name: Run invoke update if: ${{ inputs.update == 'true' }} shell: bash - run: invoke update --skip-backup --skip-static + run: invoke update --skip-backup --skip-static --no-frontend - name: Collect static files if: ${{ inputs.static == 'true' }} shell: bash diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml new file mode 100644 index 0000000000..25fcfd6107 --- /dev/null +++ b/.github/workflows/frontend.yaml @@ -0,0 +1,306 @@ +# Playwright testing for frontend code +# Runs the following tests: +# - Playwright tests in Firefox (against compiled frontend code) +# - Playwright tests in Chromium (coverage enabled, against vite frontend code) +# - Build frontend code and upload as artifact + +name: Frontend + +on: + push: + branches-ignore: ["l10*"] + pull_request: + branches-ignore: ["l10*"] + +env: + python_version: 3.11 + node_version: 24 + # The OS version must be set per job + server_start_sleep: 60 + + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INVENTREE_DB_ENGINE: postgresql + INVENTREE_DB_NAME: inventree + INVENTREE_DB_HOST: "127.0.0.1" + INVENTREE_DB_PORT: 5432 + INVENTREE_DB_USER: inventree_user + INVENTREE_DB_PASSWORD: inventree_password + INVENTREE_DEBUG: true + INVENTREE_PLUGINS_ENABLED: false + INVENTREE_MEDIA_ROOT: /home/runner/work/InvenTree/test_inventree_media + INVENTREE_STATIC_ROOT: /home/runner/work/InvenTree/test_inventree_static + INVENTREE_BACKUP_DIR: /home/runner/work/InvenTree/test_inventree_backup + INVENTREE_SITE_URL: http://localhost:8000 + +permissions: + contents: read + +jobs: + paths-filter: + name: Filter + runs-on: ubuntu-latest + + outputs: + frontend: ${{ steps.filter.outputs.frontend }} + force: ${{ steps.force.outputs.force }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + with: + persist-credentials: false + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1 + id: filter + with: + filters: | + frontend: + - 'src/frontend/**' + - name: Is CI being forced? + run: echo "force=true" >> $GITHUB_OUTPUT + id: force + if: | + contains(github.event.pull_request.labels.*.name, 'dependency') || + contains(github.event.pull_request.labels.*.name, 'full-run') + + + build: + name: Build + runs-on: ubuntu-24.04 + timeout-minutes: 60 + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + with: + persist-credentials: false + - name: Environment Setup + uses: ./.github/actions/setup + with: + npm: true + - name: Install dependencies + run: cd src/frontend && yarn install + - name: Build frontend + run: cd src/frontend && yarn run compile && yarn run lib && yarn run build + - name: Write version file - SHA + run: cd src/backend/InvenTree/web/static/web/.vite && echo "$GITHUB_SHA" > sha.txt + - name: Zip frontend + run: | + cd src/backend/InvenTree/web/static + zip -r frontend-build.zip web/ web/.vite + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1 + with: + name: frontend-build + path: src/backend/InvenTree/web/static/web + include-hidden-files: true + + firefox: + name: Tests [Firefox ${{ matrix.shard }} / 2] + runs-on: ubuntu-24.04 + timeout-minutes: 60 + needs: ["build", "paths-filter"] + if: needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true' + strategy: + fail-fast: false + matrix: + shard: [1, 2] + services: + postgres: + image: postgres:17 + env: + POSTGRES_DB: inventree + POSTGRES_USER: inventree_user + POSTGRES_PASSWORD: inventree_password + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U testuser" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + VITE_COVERAGE: false + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + with: + persist-credentials: false + - name: Environment Setup + uses: ./.github/actions/setup + with: + npm: true + install: true + update: true + apt-dependency: gettext postgresql-client libpq-dev + pip-dependency: psycopg2 + - name: Set up test data + run: | + invoke dev.setup-test -iv + invoke int.rebuild-thumbnails + - name: Install dependencies + run: invoke int.frontend-compile --extract + - name: Cache Playwright browsers + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # pin@v4.3.0 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('src/frontend/yarn.lock') }} + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: cd src/frontend && npx playwright install --with-deps + - name: Install Playwright OS dependencies + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: cd src/frontend && npx playwright install-deps + - name: Install Sample Plugin + run: | + pip install -U inventree-plugin-creator + create-inventree-plugin --default + cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build + - name: Run Playwright tests + id: tests + run: | + cd src/frontend + cp ./tests/fixtures/playwright_custom_logo.png ../backend/InvenTree/InvenTree/static/img/playwright_custom_logo.png + cp ./tests/fixtures/playwright_custom_splash.png ../backend/InvenTree/InvenTree/static/img/playwright_custom_splash.png + invoke static + env INVENTREE_CUSTOM_SPLASH="img/playwright_custom_splash.png" INVENTREE_CUSTOM_LOGO="img/playwright_custom_logo.png" PLAYWRIGHT_BASE_URL=http://localhost:8000 npx playwright test --project=firefox --shard=${{ matrix.shard }}/2 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1 + if: ${{ !cancelled() && steps.tests.outcome == 'failure' }} + with: + name: playwright-report-firefox-${{ matrix.shard }} + path: src/frontend/playwright-report/ + retention-days: 14 + + chromium: + name: Tests [Chromium ${{ matrix.shard }} / 4] + runs-on: ubuntu-24.04 + timeout-minutes: 60 + needs: ["build", "paths-filter"] + if: needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true' + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4] + services: + postgres: + image: postgres:17 + env: + POSTGRES_DB: inventree + POSTGRES_USER: inventree_user + POSTGRES_PASSWORD: inventree_password + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U testuser" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + COVERAGE: true + VITE_COVERAGE: true + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + with: + persist-credentials: false + - name: Environment Setup + uses: ./.github/actions/setup + with: + npm: true + install: true + update: true + apt-dependency: gettext postgresql-client libpq-dev + pip-dependency: psycopg2 + - name: Set up test data + run: | + invoke dev.setup-test -iv + invoke int.rebuild-thumbnails + - name: Install dependencies + run: invoke int.frontend-compile --extract + - name: Cache Playwright browsers + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # pin@v4.3.0 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('src/frontend/yarn.lock') }} + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: cd src/frontend && npx playwright install --with-deps + - name: Install Playwright OS dependencies + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: cd src/frontend && npx playwright install-deps + - name: Install Sample Plugin + run: | + pip install -U inventree-plugin-creator + create-inventree-plugin --default + cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build + - name: Playwright [${{ matrix.shard }} / 4] + id: tests + run: | + cd src/frontend + npx nyc playwright test --project=chromium --shard=${{ matrix.shard }}/4 + - name: Playwright Report [${{ matrix.shard }} / 4] + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1 + if: ${{ !cancelled() && steps.tests.outcome == 'failure' }} + with: + name: playwright-report-chromium-${{ matrix.shard }} + path: src/frontend/playwright-report/ + if-no-files-found: error + retention-days: 7 + - name: Upload Coverage Artifact [${{ matrix.shard }} / 4] + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1 + id: coverage-upload + if: ${{ !cancelled() && steps.tests.outcome != 'failure' }} + with: + name: coverage-${{ matrix.shard }} + path: src/frontend/.nyc_output/ + if-no-files-found: error + include-hidden-files: true + retention-days: 1 + + # Recombine the coverage reports from the 4 shards, and upload to Codecov + coverage: + name: Merge coverage reports and upload to Codecov + runs-on: ubuntu-latest + needs: chromium + timeout-minutes: 30 + if: always() + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 + with: + persist-credentials: false + + - name: Environment Setup + uses: ./.github/actions/setup + with: + npm: true + install: false + update: false + + - name: Download Coverage Artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # pin@v8.0.1 + with: + pattern: coverage-* + path: all-coverage/ + merge-multiple: true + + - name: Merge Coverage Reports + run: | + mkdir -p .nyc_output + cp all-coverage/*.json .nyc_output/ 2>/dev/null || true + npx nyc merge .nyc_output merged-coverage.json + npx nyc report \ + --tempdir .nyc_output \ + --reporter=lcov \ + --reporter=text-summary \ + --report-dir ./coverage \ + + - name: Upload coverage reports to Codecov + if: ${{ !cancelled() && github.ref == 'refs/heads/master' }} + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # pin@v6.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: inventree/InvenTree + flags: web + files: ./coverage/lcov.info diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index f0bb426fa2..879aa99f1d 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -664,133 +664,6 @@ jobs: chmod +rw /home/runner/work/InvenTree/db.sqlite3 invoke migrate - web_ui: - name: Tests - Web UI - runs-on: ubuntu-24.04 - timeout-minutes: 60 - needs: ["code-style", "paths-filter"] - if: needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true' - services: - postgres: - image: postgres:17 - env: - POSTGRES_DB: inventree - POSTGRES_USER: inventree_user - POSTGRES_PASSWORD: inventree_password - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U testuser" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - env: - INVENTREE_DB_ENGINE: postgresql - INVENTREE_DB_NAME: inventree - INVENTREE_DB_HOST: "127.0.0.1" - INVENTREE_DB_PORT: 5432 - INVENTREE_DB_USER: inventree_user - INVENTREE_DB_PASSWORD: inventree_password - INVENTREE_DEBUG: true - INVENTREE_PLUGINS_ENABLED: false - VITE_COVERAGE_BUILD: true - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 - with: - persist-credentials: false - - name: Environment Setup - uses: ./.github/actions/setup - with: - npm: true - install: true - update: true - apt-dependency: gettext postgresql-client libpq-dev - pip-dependency: psycopg2 - - name: Set up test data - run: | - invoke dev.setup-test -iv - invoke int.rebuild-thumbnails - - name: Install dependencies - run: invoke int.frontend-compile --extract - - name: Cache Playwright browsers - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # pin@v5.0.5 - id: playwright-cache - with: - path: ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ hashFiles('src/frontend/yarn.lock') }} - - name: Install Playwright browsers - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: cd src/frontend && npx playwright install --with-deps - - name: Install Playwright OS dependencies - if: steps.playwright-cache.outputs.cache-hit == 'true' - run: cd src/frontend && npx playwright install-deps - - name: Install Sample Plugin - run: | - pip install -U inventree-plugin-creator - create-inventree-plugin --default - cd MyCustomPlugin && pip install -e . && cd frontend && npm install && npm run translate && npm run build - - name: Run Playwright tests - id: tests - run: | - cd src/frontend - cp ./tests/fixtures/playwright_custom_logo.png ../backend/InvenTree/InvenTree/static/img/playwright_custom_logo.png - cp ./tests/fixtures/playwright_custom_splash.png ../backend/InvenTree/InvenTree/static/img/playwright_custom_splash.png - invoke static - env INVENTREE_CUSTOM_SPLASH="img/playwright_custom_splash.png" INVENTREE_CUSTOM_LOGO="img/playwright_custom_logo.png" npx nyc playwright test --project=customization - npx nyc playwright test --project=chromium --project=firefox - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1 - if: ${{ !cancelled() && steps.tests.outcome == 'failure' }} - with: - name: playwright-report - path: src/frontend/playwright-report/ - retention-days: 14 - - name: Report coverage - 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@e79a6962e0d4c0c17b229090214935d2e33f8354 # pin@v6.0.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - slug: inventree/InvenTree - flags: web - - name: Upload bundler info - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - run: | - cd src/frontend - yarn install - yarn run build - - web_ui_build: - name: Build - Web UI - runs-on: ubuntu-24.04 - timeout-minutes: 60 - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2 - with: - persist-credentials: false - - name: Environment Setup - uses: ./.github/actions/setup - with: - npm: true - - name: Install dependencies - run: cd src/frontend && yarn install - - name: Build frontend - run: cd src/frontend && yarn run compile && yarn run lib && yarn run build - - name: Write version file - SHA - run: cd src/backend/InvenTree/web/static/web/.vite && echo "$GITHUB_SHA" > sha.txt - - name: Zip frontend - run: | - cd src/backend/InvenTree/web/static - zip -r frontend-build.zip web/ web/.vite - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin@v7.0.1 - with: - name: frontend-build - path: src/backend/InvenTree/web/static/web - include-hidden-files: true - zizmor: name: Security [Zizmor] runs-on: ubuntu-24.04 diff --git a/src/frontend/playwright.config.ts b/src/frontend/playwright.config.ts index 6cc8bf43f4..31ba4a4f68 100644 --- a/src/frontend/playwright.config.ts +++ b/src/frontend/playwright.config.ts @@ -3,9 +3,6 @@ import { defineConfig, devices } from '@playwright/test'; // Detect if running in CI const IS_CI = !!process.env.CI; -const MAX_WORKERS: number = 3; -const MAX_RETRIES: number = 3; - /* We optionally spin-up services based on the testing mode: * * Local Development: @@ -26,21 +23,28 @@ const MAX_RETRIES: number = 3; * - WORKERS = 1 (to avoid conflicts with HMR) */ -const BASE_URL: string = IS_CI - ? 'http://localhost:8000' - : 'http://localhost:5173'; +const BASE_URL: string = + process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:5173'; + +// If running in "production" mode, we can use multiple workers to speed up the tests +const MAX_WORKERS: number = BASE_URL.endsWith('8000') ? 3 : 1; +const MAX_RETRIES: number = IS_CI ? 1 : 2; console.log('Running Playwright Tests:'); console.log('- Base URL:', BASE_URL); +console.log('- Max Workers:', MAX_WORKERS); +console.log('- Max Retries:', MAX_RETRIES); export default defineConfig({ testDir: './tests', fullyParallel: false, timeout: 90000, forbidOnly: !!IS_CI, - retries: IS_CI ? MAX_RETRIES : 0, - workers: IS_CI ? MAX_WORKERS : 1, - reporter: IS_CI ? [['html', { open: 'never' }], ['github']] : 'list', + retries: MAX_RETRIES, + workers: MAX_WORKERS, + reporter: IS_CI + ? [['html', { open: 'never' }], ['blob'], ['github']] + : 'list', /* Configure projects for major browsers */ projects: [ @@ -57,13 +61,6 @@ export default defineConfig({ ...devices['Desktop Firefox'] }, testIgnore: /customization/ // Ignore all tests in the "customization" folder for this project - }, - { - name: 'customization', - use: { - ...devices['Desktop Firefox'] - }, - testIgnore: /pui_.*\.spec\.ts/ // Ignore all "pui_*.spec.ts" tests for this project } ], @@ -89,7 +86,9 @@ export default defineConfig({ INVENTREE_CORS_ORIGIN_ALLOW_ALL: 'True', INVENTREE_COOKIE_SAMESITE: 'False', INVENTREE_LOGIN_ATTEMPTS: '100', - INVENTREE_PLUGINS_MANDATORY: 'samplelocate' + INVENTREE_PLUGINS_MANDATORY: 'samplelocate', + INVENTREE_CUSTOM_SPLASH: 'img/playwright_custom_splash.png', + INVENTREE_CUSTOM_LOGO: 'img/playwright_custom_logo.png' }, url: 'http://localhost:8000/api/', reuseExistingServer: IS_CI, diff --git a/src/frontend/tests/baseFixtures.ts b/src/frontend/tests/baseFixtures.ts index 66bd836008..c834566f3f 100644 --- a/src/frontend/tests/baseFixtures.ts +++ b/src/frontend/tests/baseFixtures.ts @@ -2,9 +2,14 @@ import * as crypto from 'node:crypto'; import * as fs from 'node:fs'; import os from 'node:os'; import * as path from 'node:path'; -import { test as baseTest } from '@playwright/test'; +import { fileURLToPath } from 'node:url'; +import { type BrowserContext, test as baseTest } from '@playwright/test'; -const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output'); +const frontendDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..' +); +const istanbulCLIOutput = path.join(frontendDir, '.nyc_output'); const platform = os.platform(); let systemKeyVar: string; if (platform === 'darwin') { @@ -19,16 +24,16 @@ export function generateUUID(): string { return crypto.randomBytes(16).toString('hex'); } -export const test = baseTest.extend({ - context: async ({ context }, use) => { - await context.addInitScript(() => - window.addEventListener('beforeunload', () => - (window as any).collectIstanbulCoverage( - JSON.stringify((window as any).__coverage__) - ) +async function setupCoverageCollection(context: BrowserContext) { + await context.addInitScript(() => + window.addEventListener('beforeunload', () => + (window as any).collectIstanbulCoverage?.( + JSON.stringify((window as any).__coverage__) ) - ); - await fs.promises.mkdir(istanbulCLIOutput, { recursive: true }); + ) + ); + await fs.promises.mkdir(istanbulCLIOutput, { recursive: true }); + try { await context.exposeFunction( 'collectIstanbulCoverage', (coverageJSON: string) => { @@ -42,18 +47,68 @@ export const test = baseTest.extend({ ); } ); + } catch { + // already exposed on this context (e.g. called twice for same context) + } +} + +async function collectCoverageFromContext(context: BrowserContext) { + await Promise.allSettled( + context.pages().map(async (page) => { + try { + await Promise.race([ + page.evaluate(() => + (window as any).collectIstanbulCoverage?.( + JSON.stringify((window as any).__coverage__) + ) + ), + new Promise((_, reject) => + setTimeout( + () => reject(new Error('Coverage collection timeout')), + 2000 + ) + ) + ]); + } catch { + // page may already be closed or script execution can be blocked during teardown + } + }) + ); +} + +export const test = baseTest.extend<{}, {}>({ + // Wrap browser.newPage so contexts created via doCachedLogin also get coverage + browser: [ + async ({ browser }, use) => { + const origNewPage = browser.newPage.bind(browser); + (browser as any).newPage = async ( + options?: Parameters[0] + ) => { + const page = await origNewPage(options); + await setupCoverageCollection(page.context()); + return page; + }; + try { + await use(browser); + } finally { + (browser as any).newPage = origNewPage; + for (const context of browser.contexts()) { + await collectCoverageFromContext(context); + } + } + }, + { scope: 'worker' } + ], + + context: async ({ context }, use) => { + await setupCoverageCollection(context); await use(context); - for (const page of context.pages()) { - await page.evaluate(() => - (window as any).collectIstanbulCoverage( - JSON.stringify((window as any).__coverage__) - ) - ); - } + await collectCoverageFromContext(context); }, + // Ensure no errors are thrown in the console page: async ({ page }, use) => { - const messages = []; + const messages: any[] = []; page.on('console', (msg) => { const url = msg.location().url; if ( diff --git a/src/frontend/tests/customization/customization.spec.ts b/src/frontend/tests/customization/customization.spec.ts index 9b792131ba..74e9f36e85 100644 --- a/src/frontend/tests/customization/customization.spec.ts +++ b/src/frontend/tests/customization/customization.spec.ts @@ -1,11 +1,16 @@ -import test, { expect } from '@playwright/test'; +import { expect, test } from '../baseFixtures'; import { navigate } from '../helpers'; /** * Tests for user interface customization functionality. * * Note: The correct environment variables must be set for these tests to work correctly. See "playwright.config.ts" for details. - * These tests are designed to run in CI environments where specific environment variables are set to enable custom logos and splash screens. The tests verify that these customizations are correctly applied in the user interface. + * These tests are designed to run in CI environments where specific environment variables are set to enable custom logos and splash screens. + * The tests verify that these customizations are correctly applied in the user interface. + * + * If you are running these tests locally, ensure that you have the appropriate environment variables set to enable the customizations. + * You may need to modify the "webServer" configuration in "playwright.config.ts" to include the necessary environment variables for local testing. + * */ test('Customization - Splash', async ({ page }) => { diff --git a/src/frontend/tests/pages/pui_return.spec.ts b/src/frontend/tests/pages/pui_return.spec.ts index 96962d0574..5ffd09aada 100644 --- a/src/frontend/tests/pages/pui_return.spec.ts +++ b/src/frontend/tests/pages/pui_return.spec.ts @@ -1,4 +1,4 @@ -import test from '@playwright/test'; +import { test } from '../baseFixtures'; import { loadTab } from '../helpers'; import { doCachedLogin } from '../login'; diff --git a/src/frontend/tests/pui_exporting.spec.ts b/src/frontend/tests/pui_exporting.spec.ts index acf9a38345..dc32370560 100644 --- a/src/frontend/tests/pui_exporting.spec.ts +++ b/src/frontend/tests/pui_exporting.spec.ts @@ -1,4 +1,4 @@ -import test from '@playwright/test'; +import { test } from './baseFixtures'; import { stevenuser } from './defaults'; import { globalSearch, loadTab, navigate } from './helpers'; import { doCachedLogin } from './login'; diff --git a/src/frontend/tests/pui_forms.spec.ts b/src/frontend/tests/pui_forms.spec.ts index a812d42f05..38f48099c8 100644 --- a/src/frontend/tests/pui_forms.spec.ts +++ b/src/frontend/tests/pui_forms.spec.ts @@ -1,6 +1,6 @@ -/** Unit tests for form validation, rendering, etc */ -import { expect, test } from 'playwright/test'; import { createApi } from './api'; +/** Unit tests for form validation, rendering, etc */ +import { expect, test } from './baseFixtures'; import { stevenuser } from './defaults'; import { navigate } from './helpers'; import { doCachedLogin } from './login'; diff --git a/src/frontend/tests/pui_importing.spec.ts b/src/frontend/tests/pui_importing.spec.ts index abd34cb201..b7d5f64061 100644 --- a/src/frontend/tests/pui_importing.spec.ts +++ b/src/frontend/tests/pui_importing.spec.ts @@ -1,4 +1,4 @@ -import test from '@playwright/test'; +import { test } from './baseFixtures'; import { stevenuser } from './defaults'; import { doCachedLogin } from './login'; diff --git a/src/frontend/tests/pui_login.spec.ts b/src/frontend/tests/pui_login.spec.ts index 3fe8f50279..9876a88e24 100644 --- a/src/frontend/tests/pui_login.spec.ts +++ b/src/frontend/tests/pui_login.spec.ts @@ -13,7 +13,8 @@ test('Login - Failures', async ({ page }) => { await page.getByRole('button', { name: 'Log In' }).click(); await page.getByText('Login failed', { exact: true }).waitFor(); await page.getByText('Check your input and try again').first().waitFor(); - await page.locator('#login').getByRole('button').click(); + + await page.reload(); }; // Navigate to the 'login' page diff --git a/src/frontend/tests/pui_machines.spec.ts b/src/frontend/tests/pui_machines.spec.ts index 0d5987b2ab..f24da371a5 100644 --- a/src/frontend/tests/pui_machines.spec.ts +++ b/src/frontend/tests/pui_machines.spec.ts @@ -1,4 +1,4 @@ -import test from 'playwright/test'; +import { test } from './baseFixtures'; import { adminuser } from './defaults'; import { clickOnRowMenu, navigate } from './helpers'; import { doCachedLogin } from './login'; diff --git a/src/frontend/tests/pui_permissions.spec.ts b/src/frontend/tests/pui_permissions.spec.ts index f554606d85..9abc356cb1 100644 --- a/src/frontend/tests/pui_permissions.spec.ts +++ b/src/frontend/tests/pui_permissions.spec.ts @@ -2,7 +2,7 @@ * Tests for UI permissions checks */ -import test from '@playwright/test'; +import { test } from './baseFixtures'; import { adminuser, readeruser } from './defaults'; import { clickOnRowMenu, loadTab } from './helpers'; import { doCachedLogin } from './login'; diff --git a/src/frontend/tests/pui_plugins.spec.ts b/src/frontend/tests/pui_plugins.spec.ts index c6835418d1..210be09a2b 100644 --- a/src/frontend/tests/pui_plugins.spec.ts +++ b/src/frontend/tests/pui_plugins.spec.ts @@ -1,4 +1,4 @@ -import test from 'playwright/test'; +import { test } from './baseFixtures'; import { adminuser } from './defaults.js'; import { diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts index 627a1ade6e..2aa813204e 100644 --- a/src/frontend/vite.config.ts +++ b/src/frontend/vite.config.ts @@ -47,7 +47,7 @@ export default defineConfig(({ command, mode }) => { }), istanbul({ include: ['src/*', 'lib/*'], - exclude: ['node_modules', 'test/'], + exclude: ['node_modules/', 'playwright/', 'tests/'], extension: ['.js', '.ts', '.tsx'], requireEnv: true }), From bd3098028bd2971a08e5fc7513e180f69079ca51 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 5 Jun 2026 23:58:39 +0200 Subject: [PATCH 3/4] bump django (#12097) --- contrib/container/requirements.txt | 6 +++--- src/backend/requirements-3.14.txt | 6 +++--- src/backend/requirements-dev-3.14.txt | 6 +++--- src/backend/requirements-dev.txt | 6 +++--- src/backend/requirements.txt | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contrib/container/requirements.txt b/contrib/container/requirements.txt index 2054860530..e94606a50f 100644 --- a/contrib/container/requirements.txt +++ b/contrib/container/requirements.txt @@ -6,9 +6,9 @@ asgiref==3.11.1 \ # via # -c src/backend/requirements.txt # django -django==5.2.14 \ - --hash=sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2 \ - --hash=sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76 +django==5.2.15 \ + --hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \ + --hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7 # via # -c src/backend/requirements.txt # -r contrib/container/requirements.in diff --git a/src/backend/requirements-3.14.txt b/src/backend/requirements-3.14.txt index 28255c9611..e4bd2a0949 100644 --- a/src/backend/requirements-3.14.txt +++ b/src/backend/requirements-3.14.txt @@ -520,9 +520,9 @@ defusedxml==0.7.1 \ # via # -c src/backend/requirements.txt # python3-openid -django==5.2.14 \ - --hash=sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2 \ - --hash=sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76 +django==5.2.15 \ + --hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \ + --hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7 # via # -c src/backend/requirements.txt # -r src/backend/requirements.in diff --git a/src/backend/requirements-dev-3.14.txt b/src/backend/requirements-dev-3.14.txt index 0e39fa8047..974e863087 100644 --- a/src/backend/requirements-dev-3.14.txt +++ b/src/backend/requirements-dev-3.14.txt @@ -408,9 +408,9 @@ cryptography==48.0.0 \ # -c src/backend/requirements-3.14.txt # -c src/backend/requirements.txt # pdfminer-six -django==5.2.14 \ - --hash=sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2 \ - --hash=sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76 +django==5.2.15 \ + --hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \ + --hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7 # via # -c src/backend/requirements-3.14.txt # -c src/backend/requirements.txt diff --git a/src/backend/requirements-dev.txt b/src/backend/requirements-dev.txt index a7099a7df0..973a96335b 100644 --- a/src/backend/requirements-dev.txt +++ b/src/backend/requirements-dev.txt @@ -403,9 +403,9 @@ cryptography==48.0.0 \ # via # -c src/backend/requirements.txt # pdfminer-six -django==5.2.14 \ - --hash=sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2 \ - --hash=sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76 +django==5.2.15 \ + --hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \ + --hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7 # via # -c src/backend/requirements.txt # django-silk diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 6fc804d069..a65d42da69 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -501,9 +501,9 @@ defusedxml==0.7.1 \ --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 # via python3-openid -django==5.2.14 \ - --hash=sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2 \ - --hash=sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76 +django==5.2.15 \ + --hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \ + --hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7 # via # -r src/backend/requirements.in # django-allauth From d4d5a4942c4beb3a67adf933e762614989995ec4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 6 Jun 2026 08:45:02 +1000 Subject: [PATCH 4/4] Codecov tweaks (#12099) * Add missing backend dirs * Adjust frontend coverage paths * Run frontend workflow on master --- .github/workflows/frontend.yaml | 4 ++-- codecov.yml | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index 25fcfd6107..5da4c295a8 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -96,7 +96,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 60 needs: ["build", "paths-filter"] - if: needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true' + if: github.ref == 'refs/heads/master' || needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true' strategy: fail-fast: false matrix: @@ -174,7 +174,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 60 needs: ["build", "paths-filter"] - if: needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true' + if: github.ref == 'refs/heads/master' || needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true' strategy: fail-fast: false matrix: diff --git a/codecov.yml b/codecov.yml index b49179c3ed..b80efb27e7 100644 --- a/codecov.yml +++ b/codecov.yml @@ -43,8 +43,10 @@ component_management: name: Backend Apps paths: - src/backend/InvenTree/build/** + - src/backend/InvenTree/common/** - src/backend/InvenTree/company/** - src/backend/InvenTree/data_exporter/** + - src/backend/InvenTree/generic/** - src/backend/InvenTree/importer/** - src/backend/InvenTree/machine/** - src/backend/InvenTree/order/** @@ -70,7 +72,8 @@ component_management: - component_id: web name: Frontend paths: - - src/frontend/** + - src/frontend/lib/** + - src/frontend/src/** statuses: - type: project target: 68% # 90%