2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-21 08:27:38 +00:00

Generator updates (#10605)

* Form Field updates:

- Allow spec of leftSection prop
- Allow spec of rightSection prop

* Add ability to auto-fill text input with placeholder value

* Simplify stock form

* Better serial number placeholders

* Update other generator fields

* Add default placeholder to DateInput

* Enhance TextField

* Remove serial_numbers field for non-creation forms

* Update playwright tests

* Adjust playwright tests

* Further playwright adjustments

* Fix project code field for build serializer
This commit is contained in:
Oliver
2025-10-18 17:18:04 +11:00
committed by GitHub
parent a7c4f2adba
commit 72d127219f
16 changed files with 219 additions and 114 deletions

View File

@@ -33,7 +33,6 @@ from generic.states.fields import InvenTreeCustomStatusSerializerMixin
from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.serializers import ( from InvenTree.serializers import (
FilterableCharField, FilterableCharField,
FilterableIntegerField,
FilterableSerializerMixin, FilterableSerializerMixin,
InvenTreeDecimalField, InvenTreeDecimalField,
InvenTreeModelSerializer, InvenTreeModelSerializer,
@@ -164,17 +163,6 @@ class BuildSerializer(
filter_name='project_code_detail', filter_name='project_code_detail',
) )
project_code = enable_filter(
FilterableIntegerField(
allow_null=True,
required=False,
label=_('Project Code'),
help_text=_('Project code for this build order'),
),
True,
filter_name='project_code_detail',
)
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
"""Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible. """Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible.

View File

@@ -53,9 +53,12 @@ export type ApiFormFieldHeader = {
* @param error : Optional error message to display * @param error : Optional error message to display
* @param exclude : Whether to exclude the field from the submitted data * @param exclude : Whether to exclude the field from the submitted data
* @param placeholder : The placeholder text to display * @param placeholder : The placeholder text to display
* @param placeholderAutofill: Whether to allow auto-filling of the placeholder value
* @param description : The description to display for the field * @param description : The description to display for the field
* @param preFieldContent : Content to render before the field * @param preFieldContent : Content to render before the field
* @param postFieldContent : Content to render after the field * @param postFieldContent : Content to render after the field
* @param leftSection : Content to render in the left section of the field
* @param rightSection : Content to render in the right section of the field
* @param autoFill: Whether to automatically fill the field with data from the API * @param autoFill: Whether to automatically fill the field with data from the API
* @param autoFillFilters: Optional filters to apply when auto-filling the field * @param autoFillFilters: Optional filters to apply when auto-filling the field
* @param onValueChange : Callback function to call when the field value changes * @param onValueChange : Callback function to call when the field value changes
@@ -103,9 +106,12 @@ export type ApiFormFieldType = {
exclude?: boolean; exclude?: boolean;
read_only?: boolean; read_only?: boolean;
placeholder?: string; placeholder?: string;
placeholderAutofill?: boolean;
description?: string; description?: string;
preFieldContent?: JSX.Element; preFieldContent?: JSX.Element;
postFieldContent?: JSX.Element; postFieldContent?: JSX.Element;
leftSection?: JSX.Element;
rightSection?: JSX.Element;
autoFill?: boolean; autoFill?: boolean;
autoFillFilters?: any; autoFillFilters?: any;
adjustValue?: (value: any) => any; adjustValue?: (value: any) => any;

View File

@@ -74,6 +74,7 @@ export function ApiFormField({
return { return {
...fieldDefinition, ...fieldDefinition,
autoFill: undefined, autoFill: undefined,
placeholderAutofill: undefined,
autoFillFilters: undefined, autoFillFilters: undefined,
onValueChange: undefined, onValueChange: undefined,
adjustFilters: undefined, adjustFilters: undefined,
@@ -146,6 +147,7 @@ export function ApiFormField({
return ( return (
<TextField <TextField
definition={reducedDefinition} definition={reducedDefinition}
placeholderAutofill={fieldDefinition.placeholderAutofill ?? false}
controller={controller} controller={controller}
fieldName={fieldName} fieldName={fieldName}
onChange={onChange} onChange={onChange}

View File

@@ -1,4 +1,5 @@
import type { ApiFormFieldType } from '@lib/types/Forms'; import type { ApiFormFieldType } from '@lib/types/Forms';
import { t } from '@lingui/core/macro';
import { DateInput } from '@mantine/dates'; import { DateInput } from '@mantine/dates';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat'; import customParseFormat from 'dayjs/plugin/customParseFormat';
@@ -72,7 +73,7 @@ export default function DateField({
valueFormat={valueFormat} valueFormat={valueFormat}
label={definition.label} label={definition.label}
description={definition.description} description={definition.description}
placeholder={definition.placeholder} placeholder={definition.placeholder ?? t`Select date`}
leftSection={definition.icon} leftSection={definition.icon}
highlightToday highlightToday
/> />

View File

@@ -1,7 +1,15 @@
import { TextInput } from '@mantine/core'; import { t } from '@lingui/core/macro';
import { TextInput, Tooltip } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { IconX } from '@tabler/icons-react'; import { IconCopyCheck, IconX } from '@tabler/icons-react';
import { useCallback, useEffect, useId, useState } from 'react'; import {
type ReactNode,
useCallback,
useEffect,
useId,
useMemo,
useState
} from 'react';
import type { FieldValues, UseControllerReturn } from 'react-hook-form'; import type { FieldValues, UseControllerReturn } from 'react-hook-form';
/* /*
@@ -13,12 +21,14 @@ export default function TextField({
controller, controller,
fieldName, fieldName,
definition, definition,
placeholderAutofill,
onChange, onChange,
onKeyDown onKeyDown
}: Readonly<{ }: Readonly<{
controller: UseControllerReturn<FieldValues, any>; controller: UseControllerReturn<FieldValues, any>;
definition: any; definition: any;
fieldName: string; fieldName: string;
placeholderAutofill?: boolean;
onChange: (value: any) => void; onChange: (value: any) => void;
onKeyDown: (value: any) => void; onKeyDown: (value: any) => void;
}>) { }>) {
@@ -28,7 +38,7 @@ export default function TextField({
fieldState: { error } fieldState: { error }
} = controller; } = controller;
const { value } = field; const { value } = useMemo(() => field, [field]);
const [rawText, setRawText] = useState<string>(value || ''); const [rawText, setRawText] = useState<string>(value || '');
@@ -48,6 +58,44 @@ export default function TextField({
} }
}, [debouncedText]); }, [debouncedText]);
// Construct a "right section" for the text field
const textFieldRightSection: ReactNode = useMemo(() => {
if (definition.rightSection) {
// Use the specified override value
return definition.rightSection;
} else if (value) {
if (!definition.required && !definition.disabled) {
// Render a button to clear the text field
return (
<Tooltip label={t`Clear`} position='top-end'>
<IconX
aria-label={`text-field-${fieldName}-clear`}
size='1rem'
color='red'
onClick={() => onTextChange('')}
/>
</Tooltip>
);
}
} else if (
!value &&
definition.placeholder &&
placeholderAutofill &&
!definition.disabled
) {
return (
<Tooltip label={t`Accept suggested value`} position='top-end'>
<IconCopyCheck
aria-label={`text-field-${fieldName}-accept-placeholder`}
size='1rem'
color='green'
onClick={() => onTextChange(definition.placeholder)}
/>
</Tooltip>
);
}
}, [placeholderAutofill, definition, value]);
return ( return (
<TextInput <TextInput
{...definition} {...definition}
@@ -71,11 +119,7 @@ export default function TextField({
} }
onKeyDown(event.code); onKeyDown(event.code);
}} }}
rightSection={ rightSection={textFieldRightSection}
value && !definition.required ? (
<IconX size='1rem' color='red' onClick={() => onTextChange('')} />
) : null
}
/> />
); );
} }

View File

@@ -107,9 +107,8 @@ export function useBuildOrderFields({
icon: <IconTruckDelivery /> icon: <IconTruckDelivery />
}, },
batch: { batch: {
placeholder: placeholder: batchGenerator.result,
batchGenerator.result && placeholderAutofill: true,
`${t`Next batch code`}: ${batchGenerator.result}`,
value: batchCode, value: batchCode,
onValueChange: (value: any) => setBatchCode(value) onValueChange: (value: any) => setBatchCode(value)
}, },
@@ -207,14 +206,12 @@ export function useBuildOrderOutputFields({
}, },
serial_numbers: { serial_numbers: {
hidden: !trackable, hidden: !trackable,
placeholder: placeholder: serialGenerator.result && `${serialGenerator.result}+`,
serialGenerator.result && placeholderAutofill: true
`${t`Next serial number`}: ${serialGenerator.result}`
}, },
batch_code: { batch_code: {
placeholder: placeholder: batchGenerator.result,
batchGenerator.result && placeholderAutofill: true
`${t`Next batch code`}: ${batchGenerator.result}`
}, },
location: { location: {
value: location, value: location,

View File

@@ -212,23 +212,21 @@ export function useStockFields({
description: t`Enter serial numbers for new stock (or leave blank)`, description: t`Enter serial numbers for new stock (or leave blank)`,
required: false, required: false,
hidden: !create, hidden: !create,
placeholder: placeholderAutofill: true,
serialGenerator.result && placeholder: serialGenerator.result && `${serialGenerator.result}+`
`${t`Next serial number`}: ${serialGenerator.result}`
}, },
serial: { serial: {
placeholder: placeholderAutofill: true,
serialGenerator.result && placeholder: serialGenerator.result,
`${t`Next serial number`}: ${serialGenerator.result}`,
hidden: hidden:
create || create ||
partInstance.trackable == false || partInstance.trackable == false ||
(stockItem?.quantity != undefined && stockItem?.quantity != 1) (stockItem?.quantity != undefined && stockItem?.quantity != 1)
}, },
batch: { batch: {
placeholder: default: '',
batchGenerator.result && placeholderAutofill: true,
`${t`Next batch code`}: ${batchGenerator.result}` placeholder: batchGenerator.result
}, },
status_custom_key: { status_custom_key: {
label: t`Stock Status` label: t`Stock Status`
@@ -272,6 +270,10 @@ export function useStockFields({
delete fields.expiry_date; delete fields.expiry_date;
} }
if (!create) {
delete fields.serial_numbers;
}
return fields; return fields;
}, [ }, [
stockItem, stockItem,
@@ -400,9 +402,8 @@ export function useStockItemSerializeFields({
return { return {
quantity: {}, quantity: {},
serial_numbers: { serial_numbers: {
placeholder: placeholder: serialGenerator.result && `${serialGenerator.result}+`,
serialGenerator.result && placeholderAutofill: true
`${t`Next serial number`}: ${serialGenerator.result}`
}, },
destination: {} destination: {}
}; };

View File

@@ -34,7 +34,7 @@ test('Build Order - Basic Tests', async ({ browser }) => {
// Edit the build order (via keyboard shortcut) // Edit the build order (via keyboard shortcut)
await page.keyboard.press('Control+E'); await page.keyboard.press('Control+E');
await page.getByLabel('text-field-title').waitFor(); await page.getByLabel('text-field-title', { exact: true }).waitFor();
await page.getByLabel('related-field-project_code').waitFor(); await page.getByLabel('related-field-project_code').waitFor();
await page.getByRole('button', { name: 'Cancel' }).click(); await page.getByRole('button', { name: 'Cancel' }).click();
@@ -85,7 +85,9 @@ test('Build Order - Basic Tests', async ({ browser }) => {
.getByLabel('add-test-result'); .getByLabel('add-test-result');
await button.click(); await button.click();
await page.getByRole('textbox', { name: 'text-field-value' }).waitFor(); await page
.getByRole('textbox', { name: 'text-field-value', exact: true })
.waitFor();
await page.getByRole('button', { name: 'Cancel' }).click(); await page.getByRole('button', { name: 'Cancel' }).click();
// Click through to the "parent" build // Click through to the "parent" build
@@ -189,22 +191,27 @@ test('Build Order - Build Outputs', async ({ browser }) => {
await page.getByLabel('action-button-add-build-output').click(); await page.getByLabel('action-button-add-build-output').click();
await page.getByLabel('number-field-quantity').fill('5'); await page.getByLabel('number-field-quantity').fill('5');
const placeholder = await page const placeholder: string =
.getByLabel('text-field-serial_numbers') (await page
.getAttribute('placeholder'); .getByLabel('text-field-serial_numbers', { exact: true })
.getAttribute('placeholder')) || '';
expect(placeholder).toContain('Next serial number'); expect(placeholder).toContain('+');
let sn = 1; let sn = 1;
if (!!placeholder && placeholder.includes('Next serial number')) { sn = Number.parseInt(placeholder.split('+')[0].trim());
sn = Number.parseInt(placeholder.split(':')[1].trim());
}
// Generate some new serial numbers // Generate some new serial numbers
await page.getByLabel('text-field-serial_numbers').fill(`${sn}, ${sn + 1}`); await page
.getByLabel('text-field-serial_numbers', { exact: true })
.fill(`${sn}, ${sn + 1}`);
// Accept the suggested batch code
await page
.getByRole('img', { name: 'text-field-batch_code-accept-placeholder' })
.click();
await page.getByLabel('text-field-batch_code').fill('BATCH12345');
await page.getByLabel('related-field-location').click(); await page.getByLabel('related-field-location').click();
await page.getByLabel('related-field-location').fill('Reel'); await page.getByLabel('related-field-location').fill('Reel');
await page.getByText('- Electronics Lab/Reel Storage').click(); await page.getByText('- Electronics Lab/Reel Storage').click();
@@ -397,7 +404,7 @@ test('Build Order - Consume Stock', async ({ browser }) => {
await page.getByLabel('Consume Stock').getByText('5 / 35').waitFor(); await page.getByLabel('Consume Stock').getByText('5 / 35').waitFor();
await page.getByLabel('Consume Stock').getByText('5 / 40').waitFor(); await page.getByLabel('Consume Stock').getByText('5 / 40').waitFor();
await page await page
.getByRole('textbox', { name: 'text-field-notes' }) .getByRole('textbox', { name: 'text-field-notes', exact: true })
.fill('some notes here...'); .fill('some notes here...');
await page.getByRole('button', { name: 'Cancel' }).click(); await page.getByRole('button', { name: 'Cancel' }).click();
@@ -426,7 +433,7 @@ test('Build Order - Tracked Outputs', async ({ browser }) => {
const cancelBuildOutput = async (cell) => { const cancelBuildOutput = async (cell) => {
await clickOnRowMenu(cell); await clickOnRowMenu(cell);
await page.getByRole('menuitem', { name: 'Cancel' }).click(); await page.getByRole('menuitem', { name: 'Cancel' }).click();
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit', exact: true }).click();
await page.getByText('Build outputs have been cancelled').waitFor(); await page.getByText('Build outputs have been cancelled').waitFor();
}; };
@@ -444,7 +451,9 @@ test('Build Order - Tracked Outputs', async ({ browser }) => {
.getByRole('button', { name: 'action-button-add-build-output' }) .getByRole('button', { name: 'action-button-add-build-output' })
.click(); .click();
await page.getByLabel('number-field-quantity').fill('1'); await page.getByLabel('number-field-quantity').fill('1');
await page.getByLabel('text-field-serial_numbers').fill('15'); await page
.getByLabel('text-field-serial_numbers', { exact: true })
.fill('15');
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Build output created').waitFor(); await page.getByText('Build output created').waitFor();
@@ -499,7 +508,9 @@ test('Build Order - Tracked Outputs', async ({ browser }) => {
.getByRole('button', { name: 'action-button-add-build-output' }) .getByRole('button', { name: 'action-button-add-build-output' })
.click(); .click();
await page.getByLabel('number-field-quantity').fill('1'); await page.getByLabel('number-field-quantity').fill('1');
await page.getByLabel('text-field-serial_numbers').fill('16'); await page
.getByLabel('text-field-serial_numbers', { exact: true })
.fill('16');
await page await page
.locator('label') .locator('label')
.filter({ hasText: 'Auto Allocate Serial' }) .filter({ hasText: 'Auto Allocate Serial' })
@@ -560,7 +571,9 @@ test('Build Order - Duplicate', async ({ browser }) => {
await page.getByLabel('action-menu-build-order-actions-duplicate').click(); await page.getByLabel('action-menu-build-order-actions-duplicate').click();
// Ensure a new reference is suggested // Ensure a new reference is suggested
await expect(page.getByLabel('text-field-reference')).not.toBeEmpty(); await expect(
page.getByLabel('text-field-reference', { exact: true })
).not.toBeEmpty();
// Submit the duplicate request and ensure it completes // Submit the duplicate request and ensure it completes
await page.getByRole('button', { name: 'Submit' }).isEnabled(); await page.getByRole('button', { name: 'Submit' }).isEnabled();

View File

@@ -30,8 +30,10 @@ test('Company', async ({ browser }) => {
await page.getByLabel('action-menu-company-actions').click(); await page.getByLabel('action-menu-company-actions').click();
await page.getByLabel('action-menu-company-actions-edit').click(); await page.getByLabel('action-menu-company-actions-edit').click();
await page.getByLabel('text-field-name').fill(''); await page.getByLabel('text-field-name', { exact: true }).fill('');
await page.getByLabel('text-field-website').fill('invalid-website'); await page
.getByLabel('text-field-website', { exact: true })
.fill('invalid-website');
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('This field may not be blank.').waitFor(); await page.getByText('This field may not be blank.').waitFor();

View File

@@ -142,14 +142,16 @@ test('Part - Editing', async ({ browser }) => {
// Open part edit dialog // Open part edit dialog
await page.keyboard.press('Control+E'); await page.keyboard.press('Control+E');
const keywords = await page.getByLabel('text-field-keywords').inputValue(); const keywords = await page
.getByLabel('text-field-keywords', { exact: true })
.inputValue();
await page await page
.getByLabel('text-field-keywords') .getByLabel('text-field-keywords', { exact: true })
.fill(keywords ? '' : 'table furniture'); .fill(keywords ? '' : 'table furniture');
// Test URL validation // Test URL validation
await page await page
.getByRole('textbox', { name: 'text-field-link' }) .getByRole('textbox', { name: 'text-field-link', exact: true })
.fill('htxp-??QQQ++'); .fill('htxp-??QQQ++');
await page.waitForTimeout(200); await page.waitForTimeout(200);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
@@ -157,11 +159,15 @@ test('Part - Editing', async ({ browser }) => {
// Fill with an empty URL // Fill with an empty URL
const description = await page const description = await page
.getByLabel('text-field-description') .getByLabel('text-field-description', { exact: true })
.inputValue(); .inputValue();
await page.getByRole('textbox', { name: 'text-field-link' }).fill(''); await page
await page.getByLabel('text-field-description').fill(`${description}+`); .getByRole('textbox', { name: 'text-field-link', exact: true })
.fill('');
await page
.getByLabel('text-field-description', { exact: true })
.fill(`${description}+`);
await page.waitForTimeout(200); await page.waitForTimeout(200);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Item Updated').waitFor(); await page.getByText('Item Updated').waitFor();
@@ -462,8 +468,12 @@ test('Parts - Attachments', async ({ browser }) => {
// Submit a new external link // Submit a new external link
await page.getByLabel('action-button-add-external-').click(); await page.getByLabel('action-button-add-external-').click();
await page.getByLabel('text-field-link').fill('https://www.google.com'); await page
await page.getByLabel('text-field-comment').fill('a sample comment'); .getByLabel('text-field-link', { exact: true })
.fill('https://www.google.com');
await page
.getByLabel('text-field-comment', { exact: true })
.fill('a sample comment');
// Note: Text field values are debounced for 250ms // Note: Text field values are debounced for 250ms
await page.waitForTimeout(300); await page.waitForTimeout(300);
@@ -473,7 +483,9 @@ test('Parts - Attachments', async ({ browser }) => {
// Launch dialog to upload a file // Launch dialog to upload a file
await page.getByLabel('action-button-add-attachment').click(); await page.getByLabel('action-button-add-attachment').click();
await page.getByLabel('text-field-comment').fill('some comment'); await page
.getByLabel('text-field-comment', { exact: true })
.fill('some comment');
await page.getByRole('button', { name: 'Cancel' }).click(); await page.getByRole('button', { name: 'Cancel' }).click();
}); });
@@ -489,7 +501,9 @@ test('Parts - Parameters', async ({ browser }) => {
await page.getByLabel('choice-field-data').click(); await page.getByLabel('choice-field-data').click();
await page.getByRole('option', { name: 'Green' }).click(); await page.getByRole('option', { name: 'Green' }).click();
await page.getByLabel('text-field-note').fill('A custom note field'); await page
.getByLabel('text-field-note', { exact: true })
.fill('A custom note field');
// Select the "polarized" parameter template (should create a "checkbox" field) // Select the "polarized" parameter template (should create a "checkbox" field)
await page.getByLabel('related-field-template').fill('Polarized'); await page.getByLabel('related-field-template').fill('Polarized');
@@ -577,8 +591,8 @@ test('Parts - Notes', async ({ browser }) => {
// Use keyboard shortcut to "edit" the part // Use keyboard shortcut to "edit" the part
await page.keyboard.press('Control+E'); await page.keyboard.press('Control+E');
await page.getByLabel('text-field-name').waitFor(); await page.getByLabel('text-field-name', { exact: true }).waitFor();
await page.getByLabel('text-field-description').waitFor(); await page.getByLabel('text-field-description', { exact: true }).waitFor();
await page.getByLabel('related-field-category').waitFor(); await page.getByLabel('related-field-category').waitFor();
await page.getByRole('button', { name: 'Cancel' }).click(); await page.getByRole('button', { name: 'Cancel' }).click();

View File

@@ -169,13 +169,15 @@ test('Purchase Orders - General', async ({ browser }) => {
.click(); .click();
await page.getByRole('menuitem', { name: 'Edit' }).click(); await page.getByRole('menuitem', { name: 'Edit' }).click();
await page.getByLabel('text-field-title').waitFor(); await page.getByLabel('text-field-title', { exact: true }).waitFor();
await page.getByLabel('text-field-line2').waitFor(); await page.getByLabel('text-field-line2', { exact: true }).waitFor();
// Read the current value of the cell, to ensure we always *change* it! // Read the current value of the cell, to ensure we always *change* it!
const value = await page.getByLabel('text-field-line2').inputValue(); const value = await page
.getByLabel('text-field-line2', { exact: true })
.inputValue();
await page await page
.getByLabel('text-field-line2') .getByLabel('text-field-line2', { exact: true })
.fill(value == 'old' ? 'new' : 'old'); .fill(value == 'old' ? 'new' : 'old');
await page.getByRole('button', { name: 'Submit' }).isEnabled(); await page.getByRole('button', { name: 'Submit' }).isEnabled();
@@ -344,9 +346,13 @@ test('Purchase Orders - Receive Items', async ({ browser }) => {
await page.getByLabel('action-button-change-status').click(); await page.getByLabel('action-button-change-status').click();
await page.getByLabel('action-button-add-note').click(); await page.getByLabel('action-button-add-note').click();
await page.getByLabel('text-field-batch_code').fill('my-batch-code'); await page
await page.getByLabel('text-field-packaging').fill('bucket'); .getByLabel('text-field-batch_code', { exact: true })
await page.getByLabel('text-field-note').fill('The quick brown fox'); .fill('my-batch-code');
await page.getByLabel('text-field-packaging', { exact: true }).fill('bucket');
await page
.getByLabel('text-field-note', { exact: true })
.fill('The quick brown fox');
await page.getByLabel('choice-field-status').click(); await page.getByLabel('choice-field-status').click();
await page.getByRole('option', { name: 'Destroyed' }).click(); await page.getByRole('option', { name: 'Destroyed' }).click();
@@ -371,7 +377,9 @@ test('Purchase Orders - Duplicate', async ({ browser }) => {
await page.getByLabel('action-menu-order-actions-duplicate').click(); await page.getByLabel('action-menu-order-actions-duplicate').click();
// Ensure a new reference is suggested // Ensure a new reference is suggested
await expect(page.getByLabel('text-field-reference')).not.toBeEmpty(); await expect(
page.getByLabel('text-field-reference', { exact: true })
).not.toBeEmpty();
// Submit the duplicate request and ensure it completes // Submit the duplicate request and ensure it completes
await page.getByRole('button', { name: 'Submit' }).isEnabled(); await page.getByRole('button', { name: 'Submit' }).isEnabled();

View File

@@ -120,8 +120,12 @@ test('Sales Orders - Shipments', async ({ browser }) => {
// Create a new shipment // Create a new shipment
await page.getByLabel('action-button-add-shipment').click(); await page.getByLabel('action-button-add-shipment').click();
await page.getByLabel('text-field-tracking_number').fill('1234567890'); await page
await page.getByLabel('text-field-invoice_number').fill('9876543210'); .getByLabel('text-field-tracking_number', { exact: true })
.fill('1234567890');
await page
.getByLabel('text-field-invoice_number', { exact: true })
.fill('9876543210');
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
// Expected field error // Expected field error
@@ -140,7 +144,7 @@ test('Sales Orders - Shipments', async ({ browser }) => {
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
let tracking_number = await page let tracking_number = await page
.getByLabel('text-field-tracking_number') .getByLabel('text-field-tracking_number', { exact: true })
.inputValue(); .inputValue();
if (!tracking_number) { if (!tracking_number) {
@@ -154,7 +158,9 @@ test('Sales Orders - Shipments', async ({ browser }) => {
} }
// Change the tracking number // Change the tracking number
await page.getByLabel('text-field-tracking_number').fill(tracking_number); await page
.getByLabel('text-field-tracking_number', { exact: true })
.fill(tracking_number);
await page.waitForTimeout(250); await page.waitForTimeout(250);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
@@ -217,7 +223,9 @@ test('Sales Orders - Duplicate', async ({ browser }) => {
await page.getByLabel('action-menu-order-actions-duplicate').click(); await page.getByLabel('action-menu-order-actions-duplicate').click();
// Ensure a new reference is suggested // Ensure a new reference is suggested
await expect(page.getByLabel('text-field-reference')).not.toBeEmpty(); await expect(
page.getByLabel('text-field-reference', { exact: true })
).not.toBeEmpty();
// Submit the duplicate request and ensure it completes // Submit the duplicate request and ensure it completes
await page.getByRole('button', { name: 'Submit' }).isEnabled(); await page.getByRole('button', { name: 'Submit' }).isEnabled();

View File

@@ -130,7 +130,9 @@ test('Stock - Serial Numbers', async ({ browser }) => {
await page.getByLabel('action-button-add-stock-item').click(); await page.getByLabel('action-button-add-stock-item').click();
// Initially fill with invalid serial/quantity combinations // Initially fill with invalid serial/quantity combinations
await page.getByLabel('text-field-serial_numbers').fill('200-250'); await page
.getByLabel('text-field-serial_numbers', { exact: true })
.fill('200-250');
await page.getByLabel('number-field-quantity').fill('10'); await page.getByLabel('number-field-quantity').fill('10');
// Add delay to account to field debounce // Add delay to account to field debounce
@@ -171,7 +173,7 @@ test('Stock - Serial Navigation', async ({ browser }) => {
await page.getByLabel('action-menu-stock-actions').click(); await page.getByLabel('action-menu-stock-actions').click();
await page.getByLabel('action-menu-stock-actions-search').click(); await page.getByLabel('action-menu-stock-actions-search').click();
await page.getByLabel('text-field-serial').fill('359'); await page.getByLabel('text-field-serial', { exact: true }).fill('359');
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
// Start at serial 359 // Start at serial 359
@@ -183,7 +185,7 @@ test('Stock - Serial Navigation', async ({ browser }) => {
await page.getByText('358', { exact: true }).first().waitFor(); await page.getByText('358', { exact: true }).first().waitFor();
await page.getByLabel('action-button-find-serial').click(); await page.getByLabel('action-button-find-serial').click();
await page.getByLabel('text-field-serial').fill('200'); await page.getByLabel('text-field-serial', { exact: true }).fill('200');
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Serial Number: 200').waitFor(); await page.getByText('Serial Number: 200').waitFor();
@@ -201,10 +203,15 @@ test('Stock - Serialize', async ({ browser }) => {
// Check for expected placeholder value // Check for expected placeholder value
await expect( await expect(
page.getByRole('textbox', { name: 'text-field-serial_numbers' }) page.getByRole('textbox', {
).toHaveAttribute('placeholder', 'Next serial number: 365'); name: 'text-field-serial_numbers',
exact: true
})
).toHaveAttribute('placeholder', '365+');
await page.getByLabel('text-field-serial_numbers').fill('200-250'); await page
.getByLabel('text-field-serial_numbers', { exact: true })
.fill('200-250');
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
@@ -212,7 +219,9 @@ test('Stock - Serialize', async ({ browser }) => {
.getByText('Number of unique serial numbers (51) must match quantity (100)') .getByText('Number of unique serial numbers (51) must match quantity (100)')
.waitFor(); .waitFor();
await page.getByLabel('text-field-serial_numbers').fill('1, 2, 3'); await page
.getByLabel('text-field-serial_numbers', { exact: true })
.fill('1, 2, 3');
await page.waitForTimeout(250); await page.waitForTimeout(250);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();

View File

@@ -22,7 +22,7 @@ test('Forms - Stock Item Validation', async ({ browser }) => {
await page.getByText('Valid part must be supplied').waitFor(); await page.getByText('Valid part must be supplied').waitFor();
// Adjust other field - the errors should persist // Adjust other field - the errors should persist
await page.getByLabel('text-field-batch').fill('BATCH-123'); await page.getByLabel('text-field-batch', { exact: true }).fill('BATCH-123');
await page.waitForTimeout(250); await page.waitForTimeout(250);
await page.getByText('Valid part must be supplied').waitFor(); await page.getByText('Valid part must be supplied').waitFor();
@@ -53,14 +53,16 @@ test('Forms - Stock Item Validation', async ({ browser }) => {
await page.getByLabel('action-menu-stock-item-actions-edit').click(); await page.getByLabel('action-menu-stock-item-actions-edit').click();
await page.getByLabel('number-field-purchase_price').fill('-1'); await page.getByLabel('number-field-purchase_price').fill('-1');
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Errors exist for one or more form fields').waitFor(); await page.getByText('Errors exist for one or more form fields').waitFor();
await page await page
.getByText('Ensure this value is greater than or equal to 0') .getByText('Ensure this value is greater than or equal to 0')
.waitFor(); .waitFor();
// Check the error message still persists after editing a different field // Check the error message still persists after editing a different field
await page.getByLabel('text-field-packaging').fill('a box'); await page.getByLabel('text-field-packaging', { exact: true }).fill('a box');
await page.waitForTimeout(250); await page.waitForTimeout(250);
await page await page
.getByText('Ensure this value is greater than or equal to 0') .getByText('Ensure this value is greater than or equal to 0')
@@ -87,7 +89,9 @@ test('Forms - Supplier Validation', async ({ browser }) => {
await page.waitForURL('**/purchasing/index/**'); await page.waitForURL('**/purchasing/index/**');
await page.getByLabel('action-button-add-company').click(); await page.getByLabel('action-button-add-company').click();
await page.getByLabel('text-field-website').fill('not-a-website'); await page
.getByLabel('text-field-website', { exact: true })
.fill('not-a-website');
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
@@ -98,7 +102,9 @@ test('Forms - Supplier Validation', async ({ browser }) => {
await page.getByText('Enter a valid URL.').waitFor(); await page.getByText('Enter a valid URL.').waitFor();
// Fill out another field, expect that the errors persist // Fill out another field, expect that the errors persist
await page.getByLabel('text-field-description').fill('A description'); await page
.getByLabel('text-field-description', { exact: true })
.fill('A description');
await page.waitForTimeout(250); await page.waitForTimeout(250);
await page.getByText('This field may not be blank.').waitFor(); await page.getByText('This field may not be blank.').waitFor();
await page.getByText('Enter a valid URL.').waitFor(); await page.getByText('Enter a valid URL.').waitFor();
@@ -108,9 +114,9 @@ test('Forms - Supplier Validation', async ({ browser }) => {
// Fill with good data // Fill with good data
await page await page
.getByLabel('text-field-website') .getByLabel('text-field-website', { exact: true })
.fill('https://www.test-website.co.uk'); .fill('https://www.test-website.co.uk');
await page.getByLabel('text-field-name').fill(supplierName); await page.getByLabel('text-field-name', { exact: true }).fill(supplierName);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('A description').first().waitFor(); await page.getByText('A description').first().waitFor();
@@ -122,7 +128,7 @@ test('Forms - Supplier Validation', async ({ browser }) => {
await navigate(page, 'purchasing/index/suppliers'); await navigate(page, 'purchasing/index/suppliers');
await page.waitForURL('**/purchasing/index/**'); await page.waitForURL('**/purchasing/index/**');
await page.getByLabel('action-button-add-company').click(); await page.getByLabel('action-button-add-company').click();
await page.getByLabel('text-field-name').fill(supplierName); await page.getByLabel('text-field-name', { exact: true }).fill(supplierName);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
// Is prevented, due to uniqueness requirements // Is prevented, due to uniqueness requirements

View File

@@ -270,16 +270,20 @@ test('Settings - Admin', async ({ browser }) => {
await roomRow.getByLabel(/row-action-menu-/i).click(); await roomRow.getByLabel(/row-action-menu-/i).click();
await page.getByRole('menuitem', { name: 'Edit' }).click(); await page.getByRole('menuitem', { name: 'Edit' }).click();
await expect(page.getByLabel('text-field-name')).toHaveValue('Room'); await expect(page.getByLabel('text-field-name', { exact: true })).toHaveValue(
'Room'
);
// Toggle the "description" field // Toggle the "description" field
const oldDescription = await page const oldDescription = await page
.getByLabel('text-field-description') .getByLabel('text-field-description', { exact: true })
.inputValue(); .inputValue();
const newDescription = `${oldDescription} (edited)`; const newDescription = `${oldDescription} (edited)`;
await page.getByLabel('text-field-description').fill(newDescription); await page
.getByLabel('text-field-description', { exact: true })
.fill(newDescription);
await page.waitForTimeout(500); await page.waitForTimeout(500);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
@@ -293,19 +297,21 @@ test('Settings - Admin', async ({ browser }) => {
await boxRow.getByLabel(/row-action-menu-/i).click(); await boxRow.getByLabel(/row-action-menu-/i).click();
await page.getByRole('menuitem', { name: 'Edit' }).click(); await page.getByRole('menuitem', { name: 'Edit' }).click();
await expect(page.getByLabel('text-field-name')).toHaveValue('Box (Large)'); await expect(page.getByLabel('text-field-name', { exact: true })).toHaveValue(
await expect(page.getByLabel('text-field-description')).toHaveValue( 'Box (Large)'
'Large cardboard box'
); );
await expect(
page.getByLabel('text-field-description', { exact: true })
).toHaveValue('Large cardboard box');
await page.getByRole('button', { name: 'Cancel' }).click(); await page.getByRole('button', { name: 'Cancel' }).click();
// Edit first item again (revert values) // Edit first item again (revert values)
await roomRow.getByLabel(/row-action-menu-/i).click(); await roomRow.getByLabel(/row-action-menu-/i).click();
await page.getByRole('menuitem', { name: 'Edit' }).click(); await page.getByRole('menuitem', { name: 'Edit' }).click();
await page.getByLabel('text-field-name').fill('Room'); await page.getByLabel('text-field-name', { exact: true }).fill('Room');
await page.waitForTimeout(500); await page.waitForTimeout(500);
await page await page
.getByLabel('text-field-description') .getByLabel('text-field-description', { exact: true })
.fill(newDescription.replaceAll(' (edited)', '')); .fill(newDescription.replaceAll(' (edited)', ''));
await page.waitForTimeout(500); await page.waitForTimeout(500);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();

View File

@@ -50,25 +50,25 @@ test('Tables - Pagination', async ({ browser }) => {
// Expected pagination size is 25 // Expected pagination size is 25
// Note: Due to other tests, there may be more than 25 items in the list // Note: Due to other tests, there may be more than 25 items in the list
await page.getByText(/1 - 25 \/ 2[2|8]/).waitFor(); await page.getByText(/1 - 25 \/ \d+/).waitFor();
await page.getByRole('button', { name: 'Next page' }).click(); await page.getByRole('button', { name: 'Next page' }).click();
await page.getByText(/26 - 2[7|8] \/ 2[7|8]/).waitFor(); await page.getByText(/26 - \d+ \/ \d+/).waitFor();
// Set page size to 10 // Set page size to 10
await page.getByRole('button', { name: '25' }).click(); await page.getByRole('button', { name: '25' }).click();
await page.getByRole('menuitem', { name: '10', exact: true }).click(); await page.getByRole('menuitem', { name: '10', exact: true }).click();
await page.getByText(/1 - 10 \/ 2[7|8]/).waitFor(); await page.getByText(/1 - 10 \/ \d+/).waitFor();
await page.getByRole('button', { name: '3' }).click(); await page.getByRole('button', { name: '3' }).click();
await page.getByText(/21 - 2[7|8] \/ 2[7|8]/).waitFor(); await page.getByText(/21 - \d+ \/ \d+/).waitFor();
await page.getByRole('button', { name: 'Previous page' }).click(); await page.getByRole('button', { name: 'Previous page' }).click();
await page.getByText(/11 - 20 \/ 2[7|8]/).waitFor(); await page.getByText(/11 - 20 \/ \d+/).waitFor();
// Set page size back to 25 // Set page size back to 25
await page.getByRole('button', { name: '10' }).click(); await page.getByRole('button', { name: '10' }).click();
await page.getByRole('menuitem', { name: '25', exact: true }).click(); await page.getByRole('menuitem', { name: '25', exact: true }).click();
await page.getByText(/1 - 25 \/ 2[7|8]/).waitFor(); await page.getByText(/1 - 25 \/ \d+/).waitFor();
}); });
test('Tables - Columns', async ({ browser }) => { test('Tables - Columns', async ({ browser }) => {