diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 7d8846a882..edd864942c 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 454 +INVENTREE_API_VERSION = 455 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v455 -> 2026-02-19 : https://github.com/inventree/InvenTree/pull/11383 + - Adds "exists_for_model_id" filter to ParameterTemplate API endpoint + - Adds "exists_for_related_model" filter to ParameterTemplate API endpoint + - Adds "exists_for_related_model_id" filter to ParameterTemplate API endpoint + v454 -> 2026-02-19 : https://github.com/inventree/InvenTree/pull/11379 - Adds "purchase_price" ordering option to StockItem API endpoint diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index 5f53fcf3b0..eccd885d98 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -5,7 +5,7 @@ import json.decoder from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError +from django.core.exceptions import FieldDoesNotExist, ValidationError from django.db.models import Q from django.http import JsonResponse from django.http.response import HttpResponse @@ -35,6 +35,7 @@ import common.filters import common.models import common.serializers import InvenTree.conversion +import InvenTree.models import InvenTree.ready from common.icons import get_icon_packs from common.settings import get_global_setting @@ -839,7 +840,13 @@ class ParameterTemplateFilter(FilterSet): content_type = common.filters.determine_content_type(value) if not content_type: - return queryset.none() + raise ValidationError({ + 'exists_for_model': 'Invalid model type provided - unable to determine content type' + }) + + # If the 'filter_exists_for_model_id' filter is applied, defer to that + if self.request.query_params.get('exists_for_model_id', None): + return queryset queryset = queryset.prefetch_related('parameters') @@ -853,6 +860,160 @@ class ParameterTemplateFilter(FilterSet): # Return only those ParameterTemplates which have at least one Parameter for the given model type return queryset.filter(parameter_count__gt=0) + exists_for_model_id = rest_filters.NumberFilter( + method='filter_exists_for_model_id', label='Exists For Model ID' + ) + + def filter_exists_for_model_id(self, queryset, name, value): + """Filter queryset to include only ParameterTemplates which have at least one Parameter for the given model type and model id. + + Notes: + - This filter can only be applied if the 'exists_for_model' filter is also applied, as the model_id is only meaningful in the context of a particular model type. + + Reference: https://github.com/inventree/InvenTree/issues/11381 + """ + exists_for_model = self.request.query_params.get('exists_for_model', None) + + if not exists_for_model: + raise ValidationError({ + 'exists_for_model': 'Invalid model type provided - unable to determine content type' + }) + + content_type = common.filters.determine_content_type(exists_for_model) + + if not content_type: + raise ValidationError({ + 'exists_for_model': 'Invalid model type provided - unable to determine content type' + }) + + model_class = content_type.model_class() + + # Try to find the model instance + try: + instance = model_class.objects.get(pk=value) + except (model_class.DoesNotExist, ValueError): + # If the model instance does not exist, then we can return an empty queryset + raise ValidationError({ + 'exists_for_model_id': 'Invalid model id provided - no such instance for the given model type' + }) + + # If the provided model is a "tree" structure, then we should also include any child objects in the filter + if isinstance(instance, InvenTree.models.InvenTreeTree): + id_values = list( + instance.get_descendants(include_self=True).values_list('pk', flat=True) + ) + else: + id_values = [instance.pk] + + # Now, filter against model type and model id + queryset = queryset.prefetch_related('parameters') + + filters = {'model_type': content_type, 'model_id__in': id_values} + + # Annotate the queryset to determine which ParameterTemplates have at least one Parameter defined + queryset = queryset.annotate( + parameter_count=SubqueryCount('parameters', filter=Q(**filters)) + ) + + return queryset.filter(parameter_count__gt=0) + + exists_for_related_model = rest_filters.CharFilter( + method='filter_exists_for_related_model', label='Exists For Related Model' + ) + + def filter_exists_for_related_model(self, queryset, name, value): + """Filter applied to map parameter templates to a particular model relation against the target model. + + For instance, specify 'category' to filter part parameters which exist for any part in that category. + + Note: + - This filter has no effect on its own + - It requires the 'exists_for_model' filter to be applied (to specify the base model) + - It requires the 'exists_for_related_model_id' filter to be applied also (to specify the related model id) + """ + return queryset + + exists_for_related_model_id = rest_filters.NumberFilter( + method='filter_exists_for_related_model_id', label='Exists For Model ID' + ) + + def filter_exists_for_related_model_id(self, queryset, name, value): + """Filter queryset to include only ParameterTemplates which have at least one Parameter for the given related model type and model id. + + Notes: + - This filter can only be applied if the 'exists_for_model' filter is also applied, as the model_id is only meaningful in the context of a base model + - This filter can only be applied if the 'exists_for_related_model' filter is also applied, as the related model id is only meaningful in the context of a particular model relation + + Example: To filter part parameters which have at least one parameter defined for any part in category 5, you could apply the following filters: + - exists_for_model=part + - exists_for_related_model=category + - exists_for_related_model_id=5 + """ + model = self.request.query_params.get('exists_for_model', None) + related_model = self.request.query_params.get('exists_for_related_model', None) + + if not model or not related_model: + raise ValidationError({ + 'exists_for_model': 'Invalid model type provided - unable to determine content type' + }) + + # Determine content type for the base model, to ensure they are valid + model_type = common.filters.determine_content_type(model) + + if not model_type: + return queryset.none() + + # Determine the model class for the 'related' model + try: + related_model_field = model_type.model_class()._meta.get_field( + related_model + ) + except FieldDoesNotExist: + raise ValidationError({ + 'exists_for_related_model': 'Invalid related model - no such field on the base model' + }) + if related_model_field := model_type.model_class()._meta.get_field( + related_model + ): + related_model_class = related_model_field.related_model + else: + # Return an empty queryset if the provided related model is invalid + return queryset.none() + + # Find all instances of the related model which match the provided related model id + try: + related_instance = related_model_class.objects.get(pk=value) + except (related_model_class.DoesNotExist, ValueError): + return queryset.none() + + # Account for potential tree structure in the related model + if isinstance(related_instance, InvenTree.models.InvenTreeTree): + related_instances = list( + related_instance.get_descendants(include_self=True).values_list( + 'pk', flat=True + ) + ) + else: + related_instances = [related_instance.pk] + + # Next, find all instances of the base model which are related to the related model instances + model_instances = model_type.model_class().objects.filter(**{ + f'{related_model}__in': related_instances + }) + model_instance_ids = list(model_instances.values_list('pk', flat=True)) + + # Now, filter against model type and model id + queryset = queryset.prefetch_related('parameters') + + filters = {'model_type': model_type, 'model_id__in': model_instance_ids} + + # Annotate the queryset to determine which ParameterTemplates have at least one Parameter defined + queryset = queryset.annotate( + parameter_count=SubqueryCount('parameters', filter=Q(**filters)) + ) + + return queryset.filter(parameter_count__gt=0) + class ParameterTemplateMixin: """Mixin class for ParameterTemplate views.""" diff --git a/src/backend/InvenTree/common/test_api.py b/src/backend/InvenTree/common/test_api.py index 4e77eacf85..aba7e70afe 100644 --- a/src/backend/InvenTree/common/test_api.py +++ b/src/backend/InvenTree/common/test_api.py @@ -237,6 +237,217 @@ class ParameterAPITests(InvenTreeAPITestCase): f'Incorrect number of templates for model "{model_name}"', ) + def test_template_extended_filters(self): + """Unit testing for more complex filters on the ParameterTemplate endpoint. + + Ref: https://github.com/inventree/InvenTree/pull/11383 + + In these tests we will filter by complex model relations. + """ + from part.models import Part, PartCategory + + # Create some part categories + cat_mech = PartCategory.objects.create( + name='Mechanical', description='Mechanical components' + ) + cat_elec = PartCategory.objects.create( + name='Electronics', description='Electronic components' + ) + cat_pass = PartCategory.objects.create( + name='Passive', description='Passive electronic components', parent=cat_elec + ) + cat_res = PartCategory.objects.create( + name='Resistors', description='Resistor components', parent=cat_pass + ) + cat_cap = PartCategory.objects.create( + name='Capacitors', description='Capacitor components', parent=cat_pass + ) + + # Create some parts + capacitors = [ + Part.objects.create( + name=f'Capacitor {ii}', description='A capacitor', category=cat_cap + ) + for ii in range(5) + ] + + resistors = [ + Part.objects.create( + name=f'Resistor {ii}', description='A resistor', category=cat_res + ) + for ii in range(5) + ] + + # Create some ParameterTemplates which relate to the category of the part + resistance = common.models.ParameterTemplate.objects.create( + name='Resistance', description='The resistance of a part', units='Ohms' + ) + + capacitance = common.models.ParameterTemplate.objects.create( + name='Capacitance', description='The capacitance of a part', units='Farads' + ) + + tolerance = common.models.ParameterTemplate.objects.create( + name='Tolerance', description='The tolerance of a part', units='%' + ) + + for idx, resistor in enumerate(resistors): + common.models.Parameter.objects.create( + template=resistance, + model_type=resistor.get_content_type(), + model_id=resistor.pk, + data=f'{10 * (idx + 1)}k', + ) + + common.models.Parameter.objects.create( + template=tolerance, + model_type=resistor.get_content_type(), + model_id=resistor.pk, + data=f'{idx + 1}%', + ) + + for idx, capacitor in enumerate(capacitors): + common.models.Parameter.objects.create( + template=capacitance, + model_type=capacitor.get_content_type(), + model_id=capacitor.pk, + data=f'{10 * (idx + 1)}uF', + ) + + common.models.Parameter.objects.create( + template=tolerance, + model_type=capacitor.get_content_type(), + model_id=capacitor.pk, + data=f'{5 * (idx + 1)}%', + ) + + # Ensure that we have the expected number of templates and parameters created for testing + self.assertEqual(common.models.ParameterTemplate.objects.count(), 3) + self.assertEqual(common.models.Parameter.objects.count(), 20) + + # Now, we have some data - let's apply some filtering + url = reverse('api-parameter-template-list') + + # Return *all* results, without filters + response = self.get(url) + self.assertEqual(len(response.data), 3) + + # Filter by 'exists_for_model' + for model_name, count in { + 'part.part': 3, + 'part': 3, + 'company': 0, + 'build': 0, + }.items(): + response = self.get(url, data={'exists_for_model': model_name}) + n = len(response.data) + self.assertEqual( + n, + count, + f'Incorrect number of templates ({n}) for model "{model_name}"', + ) + + # Filter by 'exists_for_model' and 'exists_for_model_id' + res = resistors[0] + response = self.get( + url, data={'exists_for_model': 'part.part', 'exists_for_model_id': res.pk} + ) + + self.assertEqual(len(response.data), 2) + pk_list = [t['pk'] for t in response.data] + self.assertIn(resistance.pk, pk_list) + self.assertIn(tolerance.pk, pk_list) + + cap = capacitors[0] + response = self.get( + url, data={'exists_for_model': 'part.part', 'exists_for_model_id': cap.pk} + ) + self.assertEqual(len(response.data), 2) + pk_list = [t['pk'] for t in response.data] + self.assertIn(capacitance.pk, pk_list) + self.assertIn(tolerance.pk, pk_list) + + # Filter by 'exists_for_related_model' (test the "capacitor" relationship) + + # Check the 'capacitor' category + response = self.get( + url, + data={ + 'exists_for_model': 'part.part', + 'exists_for_related_model': 'category', + 'exists_for_related_model_id': cat_cap.pk, + }, + ) + + self.assertEqual(len(response.data), 2) + pk_list = [t['pk'] for t in response.data] + self.assertIn(capacitance.pk, pk_list) + self.assertIn(tolerance.pk, pk_list) + + # Check the 'electronics' category - this should return all parameters + response = self.get( + url, + data={ + 'exists_for_model': 'part.part', + 'exists_for_related_model': 'category', + 'exists_for_related_model_id': cat_elec.pk, + }, + ) + self.assertEqual(len(response.data), 3) + pk_list = [t['pk'] for t in response.data] + self.assertIn(resistance.pk, pk_list) + self.assertIn(capacitance.pk, pk_list) + self.assertIn(tolerance.pk, pk_list) + + # Check the 'mechanical' category - this should return no parameters + response = self.get( + url, + data={ + 'exists_for_model': 'part.part', + 'exists_for_related_model': 'category', + 'exists_for_related_model_id': cat_mech.pk, + }, + ) + + self.assertEqual(len(response.data), 0) + + def test_invalid_filters(self): + """Test error messages for invalid filter combinations.""" + url = reverse('api-parameter-template-list') + + # Invalid 'exists_for_model' value + response = self.get( + url, {'exists_for_model': 'asdf---invalid---model'}, expected_code=400 + ) + + self.assertIn( + 'Invalid model type provided', str(response.data['exists_for_model']) + ) + + # Invalid 'exists_for_model_id' value + for model_id in ['not_an_integer', -1, 9999]: + response = self.get( + url, + {'exists_for_model': 'part.part', 'exists_for_model_id': model_id}, + expected_code=400, + ) + + # Invalid 'exists_for_related_model' value + response = self.get( + url, + { + 'exists_for_model': 'part', + 'exists_for_related_model': 'invalid_field', + 'exists_for_related_model_id': 1, + }, + expected_code=400, + ) + + self.assertIn( + 'no such field on the base model', + str(response.data['exists_for_related_model']), + ) + def test_parameter_api(self): """Test Parameter API functionality.""" # Create a simple part to test with diff --git a/src/backend/requirements-3.14.txt b/src/backend/requirements-3.14.txt index 76a113354e..85a0aae243 100644 --- a/src/backend/requirements-3.14.txt +++ b/src/backend/requirements-3.14.txt @@ -1659,9 +1659,9 @@ pynacl==1.6.2 \ # via # -c src/backend/requirements.txt # paramiko -pypdf==6.7.1 \ - --hash=sha256:6b7a63be5563a0a35d54c6d6b550d75c00b8ccf36384be96365355e296e6b3b0 \ - --hash=sha256:a02ccbb06463f7c334ce1612e91b3e68a8e827f3cee100b9941771e6066b094e +pypdf==6.6.2 \ + --hash=sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016 \ + --hash=sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba # via # -c src/backend/requirements.txt # -r src/backend/requirements.in diff --git a/src/frontend/src/tables/general/ParametricDataTable.tsx b/src/frontend/src/tables/general/ParametricDataTable.tsx index 279f68e90b..ead614ba7e 100644 --- a/src/frontend/src/tables/general/ParametricDataTable.tsx +++ b/src/frontend/src/tables/general/ParametricDataTable.tsx @@ -101,12 +101,18 @@ function ParameterCell({ */ export default function ParametricDataTable({ modelType, + modelId, + relatedModel, + relatedModelId, endpoint, queryParams, customFilters, customColumns }: { modelType: ModelType; + modelId?: number; + relatedModel?: string; + relatedModelId?: number; endpoint: ApiEndpoints | string; queryParams?: Record; customFilters?: TableFilter[]; @@ -125,8 +131,12 @@ export default function ParametricDataTable({ .get(apiUrl(ApiEndpoints.parameter_template_list), { params: { active: true, + ordering: 'name', for_model: modelType, - exists_for_model: modelType + exists_for_model: modelType, + exists_for_model_id: modelId ?? undefined, + exists_for_related_model: relatedModel ?? undefined, + exists_for_related_model_id: relatedModelId ?? undefined } }) .then((response) => response.data); diff --git a/src/frontend/src/tables/part/ParametricPartTable.tsx b/src/frontend/src/tables/part/ParametricPartTable.tsx index f99e3ee668..2eb1be8b03 100644 --- a/src/frontend/src/tables/part/ParametricPartTable.tsx +++ b/src/frontend/src/tables/part/ParametricPartTable.tsx @@ -56,6 +56,8 @@ export default function ParametricPartTable({ return ( { await page.waitForTimeout(50); await page.waitForLoadState('networkidle'); }; + +// Display the 'table' view +export const showTableView = async (page: Page) => { + await page + .getByRole('button', { name: 'segmented-icon-control-table' }) + .click(); + await page.waitForLoadState('networkidle'); +}; + +// Display the 'parameteric' view +export const showParametricView = async (page: Page) => { + await page + .getByRole('button', { name: 'segmented-icon-control-parametric' }) + .click(); + await page.waitForLoadState('networkidle'); +}; + +// Display the 'calendar' view +export const showCalendarView = async (page: Page) => { + await page + .getByRole('button', { name: 'segmented-icon-control-calendar' }) + .click(); + await page.waitForLoadState('networkidle'); +}; + +// Check for an expected number of columns in the visible table +export const expectTableColumnCount = async (page: Page, count: number) => { + const columns = page.locator('table thead tr th'); + await expect(columns).toHaveCount(count); +}; diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts index beabf72c0e..8f5321b231 100644 --- a/src/frontend/tests/pages/pui_build.spec.ts +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -7,7 +7,10 @@ import { getRowFromCell, loadTab, navigate, - setTableChoiceFilter + setTableChoiceFilter, + showCalendarView, + showParametricView, + showTableView } from '../helpers.ts'; import { doCachedLogin } from '../login.ts'; @@ -17,18 +20,10 @@ test('Build - Index', async ({ browser }) => { await loadTab(page, 'Build Orders'); // Ensure all data views are available - await page - .getByRole('button', { name: 'segmented-icon-control-parametric' }) - .click(); - - await page - .getByRole('button', { name: 'segmented-icon-control-calendar' }) - .click(); + await showParametricView(page); + await showCalendarView(page); await page.getByRole('button', { name: 'action-button-next-month' }).click(); - - await page - .getByRole('button', { name: 'segmented-icon-control-table' }) - .click(); + await showTableView(page); }); test('Build Order - Basic Tests', async ({ browser }) => { diff --git a/src/frontend/tests/pages/pui_company.spec.ts b/src/frontend/tests/pages/pui_company.spec.ts index 044f6852f5..d2e27cbb0e 100644 --- a/src/frontend/tests/pages/pui_company.spec.ts +++ b/src/frontend/tests/pages/pui_company.spec.ts @@ -1,5 +1,10 @@ import { test } from '../baseFixtures.js'; -import { clickOnParamFilter, loadTab, navigate } from '../helpers.js'; +import { + clickOnParamFilter, + loadTab, + navigate, + showParametricView +} from '../helpers.js'; import { doCachedLogin } from '../login.js'; test('Company', async ({ browser }) => { @@ -49,9 +54,7 @@ test('Company - Parameters', async ({ browser }) => { }); // Show parametric view - await page - .getByRole('button', { name: 'segmented-icon-control-parametric' }) - .click(); + await showParametricView(page); // Filter by "payment terms" parameter value await clickOnParamFilter(page, 'Payment Terms'); diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index 4fd6dce101..eef7e89e91 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -4,10 +4,13 @@ import { clickOnParamFilter, clickOnRowMenu, deletePart, + expectTableColumnCount, getRowFromCell, loadTab, navigate, - setTableChoiceFilter + setTableChoiceFilter, + showParametricView, + showTableView } from '../helpers'; import { doCachedLogin } from '../login'; import { setPluginState, setSettingState } from '../settings'; @@ -499,6 +502,58 @@ test('Parts - Attachments', async ({ browser }) => { await page.getByRole('button', { name: 'Cancel' }).click(); }); +test('Parts - Parameters by Category', async ({ browser }) => { + const page = await doCachedLogin(browser, { url: 'part/category/4/parts' }); + + await showParametricView(page); + + // Check for expected parameter columns + for (const col of [ + 'Total Stock', + 'Capacitance [F]', + 'Power [W]', + 'Resistance [ohms]' + ]) { + await page.getByRole('button', { name: col }).waitFor(); + } + + await expectTableColumnCount(page, 9); + + // Now let's go to the "resistors" category + await navigate(page, 'part/category/5/parts'); + await showParametricView(page); + + // Fewer parameter templates displayed here + await expectTableColumnCount(page, 7); + + // Check for expected parameter columns + for (const col of [ + 'Total Stock', + 'Tolerance [percent]', + 'Power [W]', + 'Resistance [ohms]' + ]) { + await page.getByRole('button', { name: col }).waitFor(); + } + + // Finally, let's go to the "capacitors" category, which has a different set of parameter templates + await navigate(page, 'part/category/6/parts'); + await showParametricView(page); + await expectTableColumnCount(page, 7); + + for (const col of [ + 'Total Stock', + 'Tolerance [percent]', + 'Polarized', + 'Capacitance [F]' + ]) { + await page.getByRole('button', { name: col }).waitFor(); + } + + // Reset to the table view + await showTableView(page); +}); + test('Parts - Parameters', async ({ browser }) => { const page = await doCachedLogin(browser, { url: 'part/69/parameters' }); @@ -562,10 +617,8 @@ test('Parts - Parameter Filtering', async ({ browser }) => { const page = await doCachedLogin(browser, { url: 'part/' }); await loadTab(page, 'Parts', true); - await page - .getByRole('button', { name: 'segmented-icon-control-parametric' }) - .click(); + await showParametricView(page); await clearTableFilters(page); // All parts should be available (no filters applied) diff --git a/src/frontend/tests/pages/pui_purchase_order.spec.ts b/src/frontend/tests/pages/pui_purchase_order.spec.ts index 218fa0526a..f78fa30a02 100644 --- a/src/frontend/tests/pages/pui_purchase_order.spec.ts +++ b/src/frontend/tests/pages/pui_purchase_order.spec.ts @@ -9,7 +9,10 @@ import { loadTab, navigate, openFilterDrawer, - setTableChoiceFilter + setTableChoiceFilter, + showCalendarView, + showParametricView, + showTableView } from '../helpers.ts'; import { doCachedLogin } from '../login.ts'; @@ -18,26 +21,14 @@ test('Purchasing - Index', async ({ browser }) => { // Purchase Orders tab await loadTab(page, 'Purchase Orders'); - await page - .getByRole('button', { name: 'segmented-icon-control-parametric' }) - .click(); - - await page - .getByRole('button', { name: 'segmented-icon-control-calendar' }) - .click(); - await page.getByRole('button', { name: 'calendar-select-month' }).waitFor(); - await page - .getByRole('button', { name: 'segmented-icon-control-table' }) - .click(); + await showParametricView(page); + await showCalendarView(page); + await showTableView(page); // Suppliers tab await loadTab(page, 'Suppliers'); - await page - .getByRole('button', { name: 'segmented-icon-control-parametric' }) - .click(); - await page - .getByRole('button', { name: 'segmented-icon-control-table' }) - .click(); + await showParametricView(page); + await showTableView(page); // Check for expected values await clearTableFilters(page); @@ -45,12 +36,8 @@ test('Purchasing - Index', async ({ browser }) => { // Supplier parts tab await loadTab(page, 'Supplier Parts'); - await page - .getByRole('button', { name: 'segmented-icon-control-parametric' }) - .click(); - await page - .getByRole('button', { name: 'segmented-icon-control-table' }) - .click(); + await showParametricView(page); + await showTableView(page); // Check for expected values await clearTableFilters(page); @@ -60,12 +47,8 @@ test('Purchasing - Index', async ({ browser }) => { // Manufacturers tab await loadTab(page, 'Manufacturers'); - await page - .getByRole('button', { name: 'segmented-icon-control-parametric' }) - .click(); - await page - .getByRole('button', { name: 'segmented-icon-control-table' }) - .click(); + await showParametricView(page); + await showTableView(page); // Check for expected values await clearTableFilters(page); @@ -76,12 +59,8 @@ test('Purchasing - Index', async ({ browser }) => { // Manufacturer parts tab await loadTab(page, 'Manufacturer Parts'); - await page - .getByRole('button', { name: 'segmented-icon-control-parametric' }) - .click(); - await page - .getByRole('button', { name: 'segmented-icon-control-table' }) - .click(); + await showParametricView(page); + await showTableView(page); // Check for expected values await clearTableFilters(page); diff --git a/src/frontend/tests/pages/pui_sales_order.spec.ts b/src/frontend/tests/pages/pui_sales_order.spec.ts index f909ea99ed..749c23684e 100644 --- a/src/frontend/tests/pages/pui_sales_order.spec.ts +++ b/src/frontend/tests/pages/pui_sales_order.spec.ts @@ -5,7 +5,10 @@ import { clickOnRowMenu, globalSearch, loadTab, - setTableChoiceFilter + setTableChoiceFilter, + showCalendarView, + showParametricView, + showTableView } from '../helpers.ts'; import { doCachedLogin } from '../login.ts'; @@ -18,15 +21,9 @@ test('Sales Orders - Tabs', async ({ browser }) => { await loadTab(page, 'Sales Orders'); await page.waitForURL('**/web/sales/index/salesorders'); - await page - .getByRole('button', { name: 'segmented-icon-control-parametric' }) - .click(); - await page - .getByRole('button', { name: 'segmented-icon-control-calendar' }) - .click(); - await page - .getByRole('button', { name: 'segmented-icon-control-table' }) - .click(); + await showParametricView(page); + await showCalendarView(page); + await showTableView(page); // Pending Shipments panel await loadTab(page, 'Pending Shipments'); @@ -37,25 +34,15 @@ test('Sales Orders - Tabs', async ({ browser }) => { await loadTab(page, 'Return Orders'); await page.getByRole('cell', { name: 'NOISE-COMPLAINT' }).waitFor(); - await page - .getByRole('button', { name: 'segmented-icon-control-parametric' }) - .click(); - await page - .getByRole('button', { name: 'segmented-icon-control-calendar' }) - .click(); - await page - .getByRole('button', { name: 'segmented-icon-control-table' }) - .click(); + await showParametricView(page); + await showCalendarView(page); + await showTableView(page); // Customers await loadTab(page, 'Customers'); - await page - .getByRole('button', { name: 'segmented-icon-control-parametric' }) - .click(); - await page - .getByRole('button', { name: 'segmented-icon-control-table' }) - .click(); + await showParametricView(page); + await showTableView(page); await page.getByText('Customer A').click(); await loadTab(page, 'Notes');