2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-03-21 11:44:42 +00:00

[API] Monitor task (#11527)

* Enhance docstring

* Return the ID of an offloaded task

* Add API endpoint for background task detail

* Add UI hook for monitoring background task  progress

* Handle queued tasks (not yet started)

* Improve UX

* Update frontend lib version

* Bump API version

* Fix notification

* Simplify UI interface

* Implement internal hook

* Fix API path sequence

* Add unit tests for task detail endpoint

* Refactor code into reusable model

* Explicit operation_id for API endpoints

* Further refactoring

* Use 200 response code

- axios does not like 202, simplify it

* Return task response for validation of part BOM

* Fix schema

* Cleanup

* Run background worker during playwright tests

- For full e2e integration testing

* Improve hooks and unit testing

* Rename custom hooks to meet react naming requirements
This commit is contained in:
Oliver
2026-03-15 14:11:22 +11:00
committed by GitHub
parent 133d254ba7
commit 6830ba5efe
20 changed files with 482 additions and 54 deletions

View File

@@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 463
INVENTREE_API_VERSION = 464
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v464 -> 2026-03-15 : https://github.com/inventree/InvenTree/pull/11527
- Add API endpoint for monitoring the progress of a particular background task
v463 -> 2026-03-12 : https://github.com/inventree/InvenTree/pull/11499
- Allow "bulk update" actions against StockItem endpoint

View File

@@ -953,6 +953,7 @@ Q_CLUSTER = {
'max_attempts': int(
get_setting('INVENTREE_BACKGROUND_MAX_ATTEMPTS', 'background.max_attempts', 5)
),
'save_limit': 1000,
'queue_limit': 50,
'catch_up': False,
'bulk': 10,

View File

@@ -161,13 +161,20 @@ def record_task_success(task_name: str):
def offload_task(
taskname, *args, force_async=False, force_sync=False, **kwargs
) -> bool:
) -> str | bool:
"""Create an AsyncTask if workers are running. This is different to a 'scheduled' task, in that it only runs once!
If workers are not running or force_sync flag, is set then the task is ran synchronously.
Arguments:
taskname: The name of the task to be run, in the format 'app.module.function'
*args: Positional arguments to be passed to the task function
force_async: If True, force the task to be offloaded (even if workers are not running)
force_sync: If True, force the task to be run synchronously (even if workers are running)
**kwargs: Keyword arguments to be passed to the task function
Returns:
bool: True if the task was offloaded (or ran), False otherwise
str | bool: Task ID if the task was offloaded, True if ran synchronously, False otherwise
"""
from InvenTree.exceptions import log_error
@@ -203,6 +210,9 @@ def offload_task(
task = AsyncTask(taskname, *args, group=group, **kwargs)
with tracer.start_as_current_span(f'async worker: {taskname}'):
task.run()
# Return the ID of the offloaded task, so that it can be tracked if needed
return task.id
except ImportError:
raise_warning(f"WARNING: '{taskname}' not offloaded - Function not found")
return False
@@ -265,6 +275,40 @@ def offload_task(
return True
def get_queued_task(task_id: str):
"""Find the task in the queue, if it exists.
Note that the OrmQ table does NOT keep the task ID as a database field,
it is instead stored in the payload data.
If there are a large number of pending tasks, this query may be inefficient,
but there is no other way to find a queued task by ID.
"""
offset = 0
limit = 500
if not task_id:
# Return early if no task ID was provided
return None
task_id = str(task_id)
from django_q.models import OrmQ
while True:
queued_tasks = OrmQ.objects.all().order_by('id')[offset : offset + limit]
if not queued_tasks:
break
for task in queued_tasks:
if task.task_id() == task_id:
return task
offset += limit
# No matching task was discovered
return None
@dataclass()
class ScheduledTask:
"""A scheduled task.

View File

@@ -17,8 +17,8 @@ from django.views.decorators.csrf import csrf_exempt
import django_filters.rest_framework.filters as rest_filters
import django_q.models
import django_q.tasks
from django_filters.rest_framework.filterset import FilterSet
from django_q.tasks import async_task
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from drf_spectacular.utils import OpenApiResponse, extend_schema
from error_report.models import Error
@@ -115,7 +115,7 @@ class WebhookView(CsrfExemptMixin, APIView):
# process data
message = self.webhook.save_data(payload, headers, request)
if self.run_async:
async_task(self._process_payload, message.id)
django_q.tasks.async_task(self._process_payload, message.id)
else:
self._process_result(
self.webhook.process_payload(message, payload, headers), message
@@ -564,13 +564,29 @@ class ErrorMessageDetail(RetrieveUpdateDestroyAPI):
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
class BackgroundTaskDetail(APIView):
"""Detail view for a single background task."""
permission_classes = [IsAuthenticatedOrReadScope]
@extend_schema(responses={200: common.serializers.TaskDetailSerializer})
def get(self, request, task_id, *args, **kwargs):
"""Fetch information regarding a particular background task ID."""
response = common.serializers.TaskDetailSerializer.from_task(task_id).data
return Response(response, status=response['http_status'])
class BackgroundTaskOverview(APIView):
"""Provides an overview of the background task queue status."""
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
serializer_class = None
@extend_schema(responses={200: common.serializers.TaskOverviewSerializer})
@extend_schema(
operation_id='background_task_overview',
responses={200: common.serializers.TaskOverviewSerializer},
)
def get(self, request, fmt=None):
"""Return information about the current status of the background task queue."""
import django_q.models as q_models
@@ -1396,6 +1412,9 @@ common_api_urls = [
name='api-scheduled-task-list',
),
path('failed/', FailedTaskList.as_view(), name='api-failed-task-list'),
path(
'<str:task_id>/', BackgroundTaskDetail.as_view(), name='api-task-detail'
),
path('', BackgroundTaskOverview.as_view(), name='api-task-overview'),
]),
),

View File

@@ -522,6 +522,78 @@ class ErrorMessageSerializer(InvenTreeModelSerializer):
read_only_fields = ['when', 'info', 'data', 'path', 'pk']
class TaskDetailSerializer(serializers.Serializer):
"""Serializer for a background task detail."""
task_id = serializers.CharField(read_only=True)
exists = serializers.BooleanField(read_only=True)
pending = serializers.BooleanField(read_only=True)
complete = serializers.BooleanField(read_only=True)
success = serializers.BooleanField(read_only=True)
http_status = serializers.IntegerField(read_only=True)
@classmethod
def from_task(cls, task_id: str | bool | None) -> 'TaskDetailSerializer':
"""Create a TaskDetailSerializer instance from a django_q Task.
Arguments:
task_id: The ID of the task to retrieve details for.
Returns:
An instance of TaskDetailSerializer with the task details.
Notes:
- If the provided task_id is None, the task has not been run, or has errored out
- If the provided task_id is a boolean, the task has been run synchronously, and the boolean value indicates success or failure
- If the provided task_id is a string, the task has been offloaded to the background worker, and the details can be from the database
"""
from InvenTree.tasks import get_queued_task
if task_id is None or type(task_id) is bool:
# If the task_id is a boolean, the task has been run synchronously
return cls({
'task_id': '',
'exists': False,
'pending': False,
'complete': task_id is not None,
'success': False if task_id is None else bool(task_id),
'http_status': 404 if task_id is None else 200,
})
# A non-boolean result indicates that the task has been offloaded to the background worker
success = django_q.models.Success.objects.filter(id=task_id).first()
failure = django_q.models.Failure.objects.filter(id=task_id).first()
task = (
success
or failure
or django_q.models.Task.objects.filter(id=task_id).first()
)
queued = False
exists = bool(success or failure or task)
if not exists:
# If the task has not been started yet, it may be present in the queue
queued = bool(get_queued_task(task_id))
complete = bool(success) or bool(failure)
# Determine the http_status code for the task
# - 200: Task exists and has been completed
# - 404: Task does not exist
http_status = 200 if exists or queued else 404
return cls({
'task_id': task_id,
'exists': exists or queued,
'pending': queued,
'complete': complete,
'success': bool(success),
'http_status': http_status,
})
class TaskOverviewSerializer(serializers.Serializer):
"""Serializer for background task overview."""

View File

@@ -1107,6 +1107,41 @@ class TaskListApiTests(InvenTreeAPITestCase):
for task in response.data:
self.assertEqual(task['name'], 'time.sleep')
def test_task_detail(self):
"""Test the BackgroundTaskDetail API endpoint."""
from InvenTree.tasks import offload_task
# Force run a task
result = offload_task('fake_module.test_task', force_sync=True)
self.assertFalse(result)
self.assertEqual(type(result), bool)
# Schedule a dummy task - and ensure it offloads to the worker
task_id = offload_task('fake_module.test_task', force_async=True)
self.assertIsNotNone(task_id)
self.assertEqual(type(task_id), str)
url = reverse('api-task-detail', kwargs={'task_id': task_id})
data = self.get(url, expected_code=200).data
self.assertEqual(data['task_id'], task_id)
self.assertTrue(data['exists'])
self.assertTrue(data['pending'])
self.assertFalse(data['complete'])
self.assertFalse(data['success'])
# Perform a lookup for a non-existent task
url = reverse('api-task-detail', kwargs={'task_id': 'doesnotexist'})
data = self.get(url, expected_code=404).data
self.assertEqual(data['task_id'], 'doesnotexist')
self.assertFalse(data['exists'])
self.assertFalse(data['pending'])
self.assertFalse(data['complete'])
self.assertFalse(data['success'])
class WebhookMessageTests(TestCase):
"""Tests for webhooks."""

View File

@@ -8,10 +8,11 @@ import django_filters.rest_framework.filters as rest_filters
from django_filters.rest_framework import DjangoFilterBackend
from django_filters.rest_framework.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.utils import extend_schema, extend_schema_field
from rest_framework import serializers
from rest_framework.response import Response
import common.serializers
import part.tasks as part_tasks
from data_exporter.mixins import DataExportViewMixin
from InvenTree.api import (
@@ -614,8 +615,18 @@ class PartValidateBOM(RetrieveUpdateAPI):
queryset = Part.objects.all()
serializer_class = part_serializers.PartBomValidateSerializer
@extend_schema(
responses={
200: common.serializers.TaskDetailSerializer,
404: common.serializers.TaskDetailSerializer,
}
)
def update(self, request, *args, **kwargs):
"""Validate the referenced BomItem instance."""
"""Validate the referenced BomItem instance.
As this task if offloaded to the background worker,
we return information about the background task which is performing the validation.
"""
part = self.get_object()
partial = kwargs.pop('partial', False)
@@ -629,7 +640,7 @@ class PartValidateBOM(RetrieveUpdateAPI):
valid = str2bool(serializer.validated_data.get('valid', False))
# BOM validation may take some time, so we offload it to a background task
offload_task(
task_id = offload_task(
part_tasks.validate_bom,
part.pk,
valid,
@@ -637,10 +648,8 @@ class PartValidateBOM(RetrieveUpdateAPI):
group='part',
)
# Re-serialize the response
serializer = self.get_serializer(part, many=False)
return Response(serializer.data)
response = common.serializers.TaskDetailSerializer.from_task(task_id).data
return Response(response, status=response['http_status'])
class PartFilter(FilterSet):

View File

@@ -2,6 +2,12 @@
This file contains historical changelog information for the InvenTree UI components library.
### 0.9.0 - March 2026
Exposes the `useMonitorBackgroundTask` hook, which allows plugins to monitor the status of a background task and display notifications when the task is complete. This is useful for plugins that offload long-running tasks to the background and want to provide feedback to the user when the task is complete.
Renames the `monitorDataOutput` hook to `useMonitorDataOutput` to better reflect the fact that this is a React hook, and to provide a more consistent naming convention for hooks in the library.
### 0.8.2 - March 2026
Bug fixes for the `monitorDataOutput` hook - https://github.com/inventree/InvenTree/pull/11458

View File

@@ -0,0 +1,119 @@
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { useDocumentVisibility } from '@mantine/hooks';
import { notifications, showNotification } from '@mantine/notifications';
import {
IconCircleCheck,
IconCircleX,
IconExclamationCircle
} from '@tabler/icons-react';
import { type QueryClient, useQuery } from '@tanstack/react-query';
import type { AxiosInstance } from 'axios';
import { useEffect, useState } from 'react';
import { queryClient } from '../../src/App';
export type MonitorBackgroundTaskProps = {
api: AxiosInstance;
queryClient?: QueryClient;
title?: string;
message: string;
errorMessage?: string;
successMessage?: string;
failureMessage?: string;
taskId?: string;
onSuccess?: () => void;
onFailure?: () => void;
onComplete?: () => void;
onError?: (error: Error) => void;
};
/**
* Hook for monitoring a background task running on the server
*/
export default function useMonitorBackgroundTask(
props: MonitorBackgroundTaskProps
) {
const visibility = useDocumentVisibility();
const [tracking, setTracking] = useState<boolean>(false);
useEffect(() => {
if (!!props.taskId) {
setTracking(true);
showNotification({
id: `background-task-${props.taskId}`,
title: props.title,
message: props.message,
loading: true,
autoClose: false,
withCloseButton: false
});
} else {
setTracking(false);
}
}, [props.taskId]);
useQuery(
{
enabled: !!props.taskId && tracking && visibility === 'visible',
refetchInterval: 500,
queryKey: ['background-task', props.taskId],
queryFn: () =>
props.api
.get(apiUrl(ApiEndpoints.task_overview, props.taskId))
.then((response) => {
const data = response?.data ?? {};
if (data.complete) {
setTracking(false);
props.onComplete?.();
notifications.update({
id: `background-task-${props.taskId}`,
title: props.title,
loading: false,
color: data.success ? 'green' : 'red',
message: response.data?.success
? (props.successMessage ?? props.message)
: (props.failureMessage ?? props.message),
icon: response.data?.success ? (
<IconCircleCheck />
) : (
<IconCircleX />
),
autoClose: 1000,
withCloseButton: true
});
if (data.success) {
props.onSuccess?.();
} else {
props.onFailure?.();
}
}
return response;
})
.catch((error) => {
console.error(
`Error fetching background task status for task ${props.taskId}:`,
error
);
setTracking(false);
props.onError?.(error);
notifications.update({
id: `background-task-${props.taskId}`,
title: props.title,
loading: false,
color: 'red',
message: props.errorMessage ?? props.message,
icon: <IconExclamationCircle color='red' />,
autoClose: 5000,
withCloseButton: true
});
})
},
queryClient
);
}

View File

@@ -9,48 +9,44 @@ import { ProgressBar } from '../components/ProgressBar';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { apiUrl } from '../functions/Api';
/**
* Hook for monitoring a data output process running on the server
*/
export default function monitorDataOutput({
api,
queryClient,
title,
hostname,
id
}: {
export type MonitorDataOutputProps = {
api: AxiosInstance;
queryClient?: QueryClient;
title: string;
hostname?: string;
id?: number;
}) {
};
/**
* Hook for monitoring a data output process running on the server
*/
export default function useMonitorDataOutput(props: MonitorDataOutputProps) {
const visibility = useDocumentVisibility();
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
if (!!id) {
if (!!props.id) {
setLoading(true);
showNotification({
id: `data-output-${id}`,
title: title,
id: `data-output-${props.id}`,
title: props.title,
loading: true,
autoClose: false,
withCloseButton: false,
message: <ProgressBar size='lg' value={0} progressLabel />
});
} else setLoading(false);
}, [id, title]);
}, [props.id, props.title]);
useQuery(
{
enabled: !!id && loading && visibility === 'visible',
enabled: !!props.id && loading && visibility === 'visible',
refetchInterval: 500,
queryKey: ['data-output', id, title],
queryKey: ['data-output', props.id, props.title],
queryFn: () =>
api
.get(apiUrl(ApiEndpoints.data_output, id))
props.api
.get(apiUrl(ApiEndpoints.data_output, props.id))
.then((response) => {
const data = response?.data ?? {};
@@ -61,21 +57,21 @@ export default function monitorDataOutput({
data?.error ?? data?.errors?.error ?? t`Process failed`;
notifications.update({
id: `data-output-${id}`,
id: `data-output-${props.id}`,
loading: false,
icon: <IconExclamationCircle />,
autoClose: 2500,
title: title,
title: props.title,
message: error,
color: 'red'
});
} else if (data.complete) {
setLoading(false);
notifications.update({
id: `data-output-${id}`,
id: `data-output-${props.id}`,
loading: false,
autoClose: 2500,
title: title,
title: props.title,
message: t`Process completed successfully`,
color: 'green',
icon: <IconCircleCheck />
@@ -83,7 +79,7 @@ export default function monitorDataOutput({
if (data.output) {
const url = data.output;
const base = hostname ?? window.location.origin;
const base = props.hostname ?? window.location.origin;
const downloadUrl = new URL(url, base);
@@ -91,7 +87,7 @@ export default function monitorDataOutput({
}
} else {
notifications.update({
id: `data-output-${id}`,
id: `data-output-${props.id}`,
loading: true,
autoClose: false,
withCloseButton: false,
@@ -110,19 +106,19 @@ export default function monitorDataOutput({
return data;
})
.catch((error: Error) => {
console.error('Error in monitorDataOutput:', error);
console.error('Error in useMonitorDataOutput:', error);
setLoading(false);
notifications.update({
id: `data-output-${id}`,
id: `data-output-${props.id}`,
loading: false,
autoClose: 2500,
title: title,
title: props.title,
message: error.message || t`Process failed`,
color: 'red'
});
return {};
})
},
queryClient
props.queryClient
);
}

View File

@@ -73,4 +73,11 @@ export {
} from './components/RowActions';
// Shared hooks
export { default as monitorDataOutput } from './hooks/MonitorDataOutput';
export {
default as useMonitorDataOutput,
type MonitorDataOutputProps
} from './hooks/MonitorDataOutput';
export {
default as useMonitorBackgroundTask,
type MonitorBackgroundTaskProps
} from './hooks/MonitorBackgroundTask';

View File

@@ -1,7 +1,7 @@
{
"name": "@inventreedb/ui",
"description": "UI components for the InvenTree project",
"version": "0.8.2",
"version": "0.9.0",
"private": false,
"type": "module",
"license": "MIT",

View File

@@ -96,6 +96,15 @@ export default defineConfig({
stdout: 'pipe',
stderr: 'pipe',
timeout: 120 * 1000
},
{
command: 'invoke worker',
env: {
INVENTREE_DEBUG: 'True',
INVENTREE_LOG_LEVEL: 'INFO',
INVENTREE_PLUGINS_ENABLED: 'True',
INVENTREE_PLUGINS_MANDATORY: 'samplelocate'
}
}
],
globalSetup: './playwright/global-setup.ts',

View File

@@ -0,0 +1,18 @@
import useMonitorBackgroundTask, {
type MonitorBackgroundTaskProps
} from '@lib/hooks/MonitorBackgroundTask';
import { useApi } from '../contexts/ApiContext';
/**
* Hook for monitoring the progress of a background task running on the server
*/
export default function useBackgroundTask(
props: Omit<MonitorBackgroundTaskProps, 'api'>
) {
const api = useApi();
return useMonitorBackgroundTask({
...props,
api: api
});
}

View File

@@ -1,4 +1,4 @@
import monitorDataOutput from '@lib/hooks/MonitorDataOutput';
import useMonitorDataOutput from '@lib/hooks/MonitorDataOutput';
import { useApi } from '../contexts/ApiContext';
import { useLocalState } from '../states/LocalState';
@@ -15,7 +15,7 @@ export default function useDataOutput({
const api = useApi();
const { getHost } = useLocalState.getState();
return monitorDataOutput({
return useMonitorDataOutput({
api: api,
title: title,
id: id,

View File

@@ -3,6 +3,7 @@ import { Accordion, Alert, Divider, Stack, Text } from '@mantine/core';
import { lazy } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react';
import { StylishText } from '../../../../components/items/StylishText';
import { errorCodeLink } from '../../../../components/nav/Alerts';
import { FactCollection } from '../../../../components/settings/FactCollection';
@@ -26,8 +27,18 @@ export default function TaskManagementPanel() {
return (
<>
{taskInfo?.is_running == false && (
<Alert title={t`Background worker not running`} color='red'>
{taskInfo?.is_running ? (
<Alert
title={t`Background worker running`}
color='green'
icon={<IconCircleCheck />}
/>
) : (
<Alert
title={t`Background worker not running`}
color='red'
icon={<IconExclamationCircle />}
>
<Text>{t`The background task manager service is not running. Contact your system administrator.`}</Text>
{errorCodeLink('INVE-W5')}
</Alert>

View File

@@ -81,6 +81,7 @@ import { useApi } from '../../contexts/ApiContext';
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
import { usePartFields } from '../../forms/PartForms';
import { useFindSerialNumberForm } from '../../forms/StockForms';
import useBackgroundTask from '../../hooks/UseBackgroundTask';
import {
useApiFormModal,
useCreateApiFormModal,
@@ -168,6 +169,17 @@ function BomValidationInformation({
refetchOnMount: true
});
const [taskId, setTaskId] = useState<string>('');
useBackgroundTask({
taskId: taskId,
message: t`Validating BOM`,
successMessage: t`BOM validated`,
onComplete: () => {
bomInformationQuery.refetch();
}
});
const validateBom = useApiFormModal({
url: ApiEndpoints.bom_validate,
method: 'PUT',
@@ -184,9 +196,14 @@ function BomValidationInformation({
<Text>{t`Do you want to validate the bill of materials for this assembly?`}</Text>
</Alert>
),
successMessage: t`Bill of materials scheduled for validation`,
onFormSuccess: () => {
bomInformationQuery.refetch();
successMessage: null,
onFormSuccess: (response: any) => {
// If the process has been offloaded to a background task
if (response.task_id) {
setTaskId(response.task_id);
} else {
bomInformationQuery.refetch();
}
}
});

View File

@@ -140,6 +140,44 @@ test('Parts - BOM', async ({ browser }) => {
await page.getByRole('button', { name: 'Close' }).click();
});
/**
* Perform BOM validation process
* Note that this is a "background task" which is monitored by the "useBackgroundTask" hook
*/
test('Parts - BOM Validation', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/107/bom' });
// Run BOM validation step
await page
.getByRole('button', { name: 'action-button-validate-bom' })
.click();
await page.getByRole('button', { name: 'Submit' }).click();
// Background task monitoring
await page.getByText('Validating BOM').waitFor();
await page.getByText('BOM validated').waitFor();
await page.getByRole('button', { name: 'bom-validation-info' }).hover();
await page.getByText('Validated By: allaccessAlly').waitFor();
// Edit line item, to ensure BOM is not valid next time around
const cell = await page.getByRole('cell', { name: 'Red paint Red Paint' });
await clickOnRowMenu(cell);
await page.getByRole('menuitem', { name: 'Edit', exact: true }).click();
const input = await page.getByRole('textbox', {
name: 'number-field-quantity'
});
const value = await input.inputValue();
const nextValue = Number.parseFloat(value) + 0.24;
await input.fill(`${nextValue.toFixed(3)}`);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('BOM item updated').waitFor();
});
test('Parts - Editing', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/104/details' });

View File

@@ -1,3 +1,4 @@
import type { Page } from '@playwright/test';
import { createApi } from './api.js';
import { expect, test } from './baseFixtures.js';
import { adminuser, allaccessuser, stevenuser } from './defaults.js';
@@ -314,6 +315,29 @@ test('Settings - Admin', async ({ browser }) => {
await page.getByRole('button', { name: 'Submit' }).click();
});
test('Settings - Admin - Background Tasks', async ({ browser }) => {
const page = await doCachedLogin(browser, {
user: adminuser,
url: 'settings/admin/background'
});
// Background worker should be running, and idle
await page.getByText('Background worker running').waitFor();
await page.getByText('Failed Tasks0').waitFor();
await page.getByText('Pending Tasks0').waitFor();
// Expand the "scheduled tasks" view
await page.getByRole('button', { name: 'Scheduled Tasks' }).click();
// Check for some expected values
await page
.getByRole('cell', { name: 'InvenTree.tasks.delete_successful_tasks' })
.waitFor();
await page
.getByRole('cell', { name: 'InvenTree.tasks.check_for_migrations' })
.waitFor();
});
test('Settings - Admin - Barcode History', async ({ browser }) => {
// Login with admin credentials
const page = await doCachedLogin(browser, {
@@ -529,7 +553,7 @@ test('Settings - Auth - Email', async ({ browser }) => {
await page.getByText('Currently no email addresses are registered').waitFor();
});
async function testColorPicker(page, ref: string) {
async function testColorPicker(page: Page, ref: string) {
const element = page.getByLabel(ref);
await element.click();
const box = (await element.boundingBox())!;
@@ -539,8 +563,7 @@ async function testColorPicker(page, ref: string) {
test('Settings - Auth - Tokens', async ({ browser }) => {
const page = await doCachedLogin(browser, {
username: 'allaccess',
password: 'nolimits',
user: allaccessuser,
url: 'settings/user/'
});