[UI] Link fixing (#13117)

* Use <Link> in navbar

* Use <Link> in PanelGroup

* <Link> in main navigation menu

* Update Breadcrumbs

* Refactor search drawer results

* Update NavigationTree and SearchDrawer

* Fix Link elements for dashboard widgets

* Adjust login helpers for playwright

* update playwright tests
This commit is contained in:
Oliver authored and GitHub committed 2026-10-30 10:17:51 +11:00
1 parent a0540d3185
commit e6bc21b66f
18 files changed
+283 -230

No files matched your search

@@ -1,14 +1,13 @@
import { ActionIcon, Anchor, Group, RollingNumber } from '@mantine/core';
import { IconExclamationCircle } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { type ReactNode, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { type ReactNode, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { StylishText } from '@lib/components/StylishText';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import type { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { navigateToLink } from '@lib/functions/Navigation';
import type { InvenTreeIconType } from '@lib/types/Icons';
import { useDocumentVisibility } from '@mantine/hooks';
import { useApi } from '../../../contexts/ApiContext';
@@ -32,7 +31,6 @@ function QueryCountWidget({
}>): ReactNode {
const api = useApi();
const user = useUserState();
const navigate = useNavigate();
const visibility = useDocumentVisibility();
const modelProperties = ModelInformationDict[modelType];
@@ -62,27 +60,21 @@ function QueryCountWidget({
}
});
const onFollowLink = useCallback(
(event: any) => {
if (!query.isFetched) {
return;
}
const url = useMemo(() => {
if (!modelProperties.url_overview) {
return undefined;
}
if (modelProperties.url_overview) {
let url = modelProperties.url_overview;
const searchParams = new URLSearchParams();
Object.entries(params ?? {}).forEach(([key, value]) => {
searchParams.set(key, String(value));
});
if (params) {
url += '?';
for (const key in params) {
url += `${key}=${params[key]}&`;
}
}
navigateToLink(url, navigate, event);
}
},
[query.isFetched, modelProperties, params]
);
const queryString = searchParams.toString();
return queryString
? `${modelProperties.url_overview}?${queryString}`
: modelProperties.url_overview;
}, [modelProperties.url_overview, params]);
const result: ReactNode = useMemo(() => {
if (query.isError) {
@@ -98,26 +90,32 @@ function QueryCountWidget({
}
}, [query.isFetching, query.isError, query.data]);
return (
<Anchor href='#' onClick={onFollowLink} underline='never'>
<Group
gap='xs'
wrap='nowrap'
justify='space-between'
align='center'
style={{ height: '100%' }}
>
<Group gap='xs'>
<InvenTreeIcon icon={icon ?? modelProperties.icon} />
<StylishText size='md'>{title}</StylishText>
</Group>
<Group gap='xs' wrap='nowrap' justify='space-apart'>
<Group gap='xs' wrap='nowrap' justify='right'>
{result}
</Group>
const content = (
<Group
gap='xs'
wrap='nowrap'
justify='space-between'
align='center'
style={{ height: '100%' }}
>
<Group gap='xs'>
<InvenTreeIcon icon={icon ?? modelProperties.icon} />
<StylishText size='md'>{title}</StylishText>
</Group>
<Group gap='xs' wrap='nowrap' justify='space-apart'>
<Group gap='xs' wrap='nowrap' justify='right'>
{result}
</Group>
</Group>
</Group>
);
return url ? (
<Anchor component={Link} to={url} underline='never'>
{content}
</Anchor>
) : (
content
);
}
@@ -1,11 +1,10 @@
import { Anchor, Group } from '@mantine/core';
import { type ReactNode, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { type ReactNode, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { StylishText } from '@lib/components/StylishText';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import type { ModelType } from '@lib/enums/ModelType';
import { navigateToLink } from '@lib/functions/Navigation';
import type { InvenTreeIconType } from '@lib/types/Icons';
import { InvenTreeIcon } from '../../../functions/icons';
import type { DashboardWidgetProps } from '../DashboardWidget';
@@ -24,42 +23,45 @@ function QueryWidget({
icon?: keyof InvenTreeIconType;
params: any;
}>): ReactNode {
const navigate = useNavigate();
const modelProperties = ModelInformationDict[modelType];
const onFollowLink = useCallback(
(event: any) => {
if (modelProperties.url_overview) {
let url = modelProperties.url_overview;
const url = useMemo(() => {
if (!modelProperties.url_overview) {
return undefined;
}
if (params) {
url += '?';
for (const key in params) {
url += `${key}=${params[key]}&`;
}
}
const searchParams = new URLSearchParams();
Object.entries(params ?? {}).forEach(([key, value]) => {
searchParams.set(key, String(value));
});
navigateToLink(url, navigate, event);
}
},
[modelProperties, params]
const query = searchParams.toString();
return query
? `${modelProperties.url_overview}?${query}`
: modelProperties.url_overview;
}, [modelProperties.url_overview, params]);
const content = (
<Group
gap='xs'
wrap='nowrap'
justify='space-between'
align='center'
style={{ height: '100%' }}
>
<Group gap='xs'>
<InvenTreeIcon icon={icon ?? modelProperties.icon} />
<StylishText size='md'>{title}</StylishText>
</Group>
</Group>
);
return (
<Anchor href='#' onClick={onFollowLink} underline='never'>
<Group
gap='xs'
wrap='nowrap'
justify='space-between'
align='center'
style={{ height: '100%' }}
>
<Group gap='xs'>
<InvenTreeIcon icon={icon ?? modelProperties.icon} />
<StylishText size='md'>{title}</StylishText>
</Group>
</Group>
return url ? (
<Anchor component={Link} to={url} underline='never'>
{content}
</Anchor>
) : (
content
);
}
@@ -9,10 +9,9 @@ import {
UnstyledButton
} from '@mantine/core';
import { type JSX, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { StylishText } from '@lib/components/StylishText';
import { navigateToLink } from '@lib/functions/Navigation';
import type { InvenTreeIconType } from '@lib/types/Icons';
import { InvenTreeIcon } from '../../functions/icons';
@@ -36,8 +35,6 @@ export function MenuLinks({
links: MenuLinkItem[];
beforeClick?: () => void;
}>) {
const navigate = useNavigate();
// Filter out any hidden links
const visibleLinks = useMemo(
() => links.filter((item) => !item.hidden),
@@ -80,13 +77,38 @@ export function MenuLinks({
</Text>
</Group>
</Anchor>
) : item.link ? (
<UnstyledButton
component={Link}
to={item.link}
onClick={(event) => {
if (
event.button === 0 &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey &&
!event.altKey
) {
beforeClick?.();
}
}}
>
<Group wrap='nowrap'>
{item.icon && (
<InvenTreeIcon
icon={item.icon}
iconProps={{ size: '14' }}
/>
)}
<Text fw={500} p={5}>
{item.title}
</Text>
</Group>
</UnstyledButton>
) : (
<UnstyledButton
onClick={(event) => {
if (item.link) {
beforeClick?.();
navigateToLink(item.link, navigate, event);
} else if (item.action) {
if (item.action) {
beforeClick?.();
item.action();
}
@@ -1,10 +1,9 @@
import { ActionIcon, Anchor, Breadcrumbs, Group, Text } from '@mantine/core';
import { IconMenu2 } from '@tabler/icons-react';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { identifierString } from '@lib/functions/Conversion';
import { getBaseUrl, navigateToLink } from '@lib/functions/Navigation';
export type Breadcrumb = {
icon?: React.ReactNode;
@@ -22,10 +21,6 @@ export function BreadcrumbList({
breadcrumbs: Breadcrumb[];
navCallback?: () => void;
}>) {
const navigate = useNavigate();
const baseUrl = useMemo(() => getBaseUrl(), []);
const elements = useMemo(() => {
// Limit to 7 active elements
if (breadcrumbs.length > 7) {
@@ -58,22 +53,38 @@ export function BreadcrumbList({
)}
<Breadcrumbs key='breadcrumbs' separator='>'>
{elements.map((breadcrumb, index) => {
return (
<Anchor
key={`${index}-${breadcrumb.name}`}
href={`/${baseUrl}${breadcrumb.url}`}
aria-label={`breadcrumb-${index}-${identifierString(
breadcrumb.name
)}`}
onClick={(event: any) =>
breadcrumb.url &&
navigateToLink(breadcrumb.url, navigate, event)
}
>
<Group gap={4} align='center'>
const content = (
<Group gap={4} align='center'>
{breadcrumb.icon}
<Text size='sm'>{breadcrumb.name}</Text>
</Group>
);
const ariaLabel = `breadcrumb-${index}-${identifierString(
breadcrumb.name
)}`;
if (breadcrumb.url === '#') {
return (
<Group
key={`${index}-${breadcrumb.name}`}
gap={4}
align='center'
aria-label={ariaLabel}
>
{breadcrumb.icon}
<Text size='sm'>{breadcrumb.name}</Text>
</Group>
);
}
return (
<Anchor
key={`${index}-${breadcrumb.name}`}
component={Link}
to={breadcrumb.url}
aria-label={ariaLabel}
>
{content}
</Anchor>
);
})}
+7 -24
View File
@@ -7,31 +7,22 @@ import {
Paper,
Tabs,
Text,
Tooltip,
UnstyledButton
Tooltip
} from '@mantine/core';
import { useDisclosure, useDocumentVisibility } from '@mantine/hooks';
import { IconBell, IconSearch, IconUserBolt } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { type ReactNode, useEffect, useMemo, useState } from 'react';
import {
matchPath,
useLocation,
useMatch,
useNavigate
} from 'react-router-dom';
import { Link, matchPath, useLocation, useMatch } from 'react-router-dom';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { useInvenTreeHotkeys } from '@lib/functions/Events';
import { getBaseUrl } from '@lib/functions/Navigation';
import { navigateToLink } from '@lib/functions/Navigation';
import { t } from '@lingui/core/macro';
import { useShallow } from 'zustand/react/shallow';
import { api } from '../../App';
import type { NavigationUIFeature } from '../../components/plugins/PluginUIFeatureTypes';
import { getNavTabs } from '../../defaults/links';
import { generateUrl } from '../../functions/urls';
import { usePluginUIFeature } from '../../hooks/UsePluginUIFeature';
import * as classes from '../../main.css';
import { useLocalState } from '../../states/LocalState';
@@ -256,7 +247,6 @@ export function Header() {
function NavTabs() {
const user = useUserState();
const navigate = useNavigate();
const match = useMatch(':tabName/*');
const tabValue = match?.params.tabName;
const navTabs = getNavTabs(user);
@@ -304,22 +294,15 @@ function NavTabs() {
<Tabs.Tab
value={tab.name}
key={tab.name}
renderRoot={(props) => <Link {...props} to={`/${tab.name}`} />}
leftSection={
withIcons &&
tab.icon && (
<ActionIcon variant='transparent'>{tab.icon}</ActionIcon>
)
}
onClick={(event: any) =>
navigateToLink(`/${tab.name}`, navigate, event)
}
>
<UnstyledButton
component={'a'}
href={generateUrl(`/${getBaseUrl()}/${tab.name}`)}
>
{tab.title}
</UnstyledButton>
{tab.title}
</Tabs.Tab>
);
});
@@ -329,9 +312,9 @@ function NavTabs() {
<Tabs.Tab
value={nav.options.key}
key={nav.options.key}
onClick={(event: any) =>
navigateToLink(nav.options.options.url, navigate, event)
}
renderRoot={(props) => (
<Link {...props} to={nav.options.options.url} />
)}
>
{nav.options.title}
</Tabs.Tab>
@@ -39,18 +39,14 @@ import {
useMemo,
useState
} from 'react';
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { StylishText } from '@lib/components/StylishText';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import type { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { resolveItem } from '@lib/functions/Conversion';
import {
eventModified,
getDetailUrl,
navigateToLink
} from '@lib/functions/Navigation';
import { eventModified, getDetailUrl } from '@lib/functions/Navigation';
import { t } from '@lingui/core/macro';
import { useApi } from '../../contexts/ApiContext';
import { useUserSettingsState } from '../../states/SettingsStates';
@@ -81,7 +77,6 @@ export default function NavigationTree({
children?: ReactNode;
}>) {
const api = useApi();
const navigate = useNavigate();
const treeState = useTree();
const userSettings = useUserSettingsState();
@@ -233,19 +228,6 @@ export default function NavigationTree({
[loadingNodes, allNodes, api, endpoint, childIdentifier]
);
const follow = useCallback(
(node: TreeNodeData, event?: any) => {
const url = getDetailUrl(modelType, node.value);
if (eventModified(event)) {
navigateToLink(url, navigate, event);
} else {
onClose();
navigate(url);
}
},
[modelType, navigate]
);
// In search mode use the query results directly; in browse mode use the accumulated lazy-load list
const sourceNodes: any[] = useMemo(
() => (debouncedSearch ? (query.data ?? []) : allNodes),
@@ -356,7 +338,19 @@ export default function NavigationTree({
>
<HoverCard.Target>
<Anchor
onClick={(event: any) => follow(payload.node, event)}
component={Link}
to={getDetailUrl(modelType, payload.node.value)}
onClick={(event) => {
event.stopPropagation();
if (
event.button === 0 &&
!eventModified(event) &&
!event.altKey
) {
onClose();
}
}}
aria-label={`nav-tree-item-${payload.node.value}`}
c='var(--mantine-color-text)'
>
@@ -385,7 +379,8 @@ export default function NavigationTree({
[
treeState,
childIdentifier,
follow,
modelType,
onClose,
loadingNodes,
fetchChildren,
debouncedSearch
@@ -22,7 +22,6 @@ import { useDebouncedValue } from '@mantine/hooks';
import {
IconAlertCircle,
IconBackspace,
IconExclamationCircle,
IconRefresh,
IconSearch,
IconSettings,
@@ -31,7 +30,7 @@ import {
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { type NavigateFunction, useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { Boundary } from '@lib/components/Boundary';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
@@ -40,12 +39,7 @@ import { ModelType } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import { cancelEvent } from '@lib/functions/Events';
import {
eventModified,
getDetailUrl,
navigateToLink
} from '@lib/functions/Navigation';
import { showNotification } from '@mantine/notifications';
import { eventModified, getDetailUrl } from '@lib/functions/Navigation';
import { api } from '../../App';
import { openGlobalPreview } from '../../states/PreviewDrawerState';
import { useUserSettingsState } from '../../states/SettingsStates';
@@ -75,7 +69,6 @@ type SearchQuery = {
function QueryResultGroup({
searchText,
query,
navigate,
onClose,
onRemove,
onResultClick,
@@ -83,7 +76,6 @@ function QueryResultGroup({
}: Readonly<{
searchText: string;
query: SearchQuery;
navigate: NavigateFunction;
onClose: () => void;
onRemove: (query: ModelType) => void;
onResultClick: (query: ModelType, pk: number, event: any) => void;
@@ -100,34 +92,28 @@ function QueryResultGroup({
return modelInfo.url_overview;
}, [query, modelInfo]);
// Callback function to view all results for a given query
const viewResults = useCallback(
(event: any) => {
cancelEvent(event);
const resultsUrl = useMemo(() => {
if (!overviewUrl) {
return undefined;
}
if (overviewUrl) {
// Keep the notes-search context so results found via their notes
// are also present in the full results table view
const url = `${overviewUrl}?search=${searchText}${
searchNotes ? '&search_notes=true' : ''
}`;
const params = new URLSearchParams({ search: searchText });
if (searchNotes) {
params.set('search_notes', 'true');
}
// Close drawer if opening in the same tab
if (!eventModified(event)) {
onClose();
}
return `${overviewUrl}?${params.toString()}`;
}, [overviewUrl, searchText, searchNotes]);
navigateToLink(url, navigate, event);
} else {
showNotification({
title: t`No Overview Available`,
message: t`No overview available for this model type`,
color: 'red',
icon: <IconExclamationCircle />
});
const handleViewAllClick = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
event.stopPropagation();
if (event.button === 0 && !eventModified(event) && !event.altKey) {
onClose();
}
},
[overviewUrl, searchText, searchNotes]
[onClose]
);
if (query.results.count == 0) {
@@ -140,16 +126,33 @@ function QueryResultGroup({
<Group justify='space-between'>
<Group justify='left' gap={5} wrap='nowrap'>
<Tooltip label={t`View all results`} position='top-start'>
<ActionIcon
size='sm'
variant='transparent'
radius='xs'
aria-label={`view-all-results-${query.searchKey ?? query.model}`}
disabled={!overviewUrl}
onClick={viewResults}
>
<IconTableExport />
</ActionIcon>
{resultsUrl ? (
<ActionIcon
renderRoot={(props) => (
<Link
{...props}
to={resultsUrl}
onClick={handleViewAllClick}
/>
)}
size='sm'
variant='transparent'
radius='xs'
aria-label={`view-all-results-${query.searchKey ?? query.model}`}
>
<IconTableExport />
</ActionIcon>
) : (
<ActionIcon
size='sm'
variant='transparent'
radius='xs'
aria-label={`view-all-results-${query.searchKey ?? query.model}`}
disabled
>
<IconTableExport />
</ActionIcon>
)}
</Tooltip>
<Text size='lg'>{query.title ?? modelInfo.label_multiple}</Text>
<Text size='sm' style={{ fontStyle: 'italic' }}>
@@ -180,7 +183,8 @@ function QueryResultGroup({
<Fragment key={`result-group-${query.model}-${result.pk}`}>
<Anchor
underline='never'
href={getDetailUrl(query.model, result.pk, true)}
component={Link}
to={getDetailUrl(query.model, result.pk)}
onClick={(event: any) =>
onResultClick(query.model, result.pk, event)
}
@@ -470,12 +474,17 @@ export function SearchDrawer({
onClose();
}
const navigate = useNavigate();
// Callback when one of the search results is clicked
function onResultClick(query: ModelType, pk: number, event: any) {
const targetModel = ModelInformationDict[query];
if (targetModel.url_detail == undefined) {
if (
targetModel.url_detail == undefined ||
event.button !== 0 ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey ||
event.altKey
) {
return;
}
@@ -490,14 +499,7 @@ export function SearchDrawer({
return;
}
if (eventModified(event)) {
// Keep the drawer open in this condition
} else {
closeDrawer();
}
const url = targetModel.url_detail.replace(':pk', pk.toString());
navigateToLink(url, navigate, event);
closeDrawer();
}
return (
@@ -591,7 +593,6 @@ export function SearchDrawer({
key={query.model}
searchText={searchText}
query={query}
navigate={navigate}
onClose={closeDrawer}
searchNotes={searchNotes}
onRemove={(query) => removeResults(query)}
@@ -12,8 +12,7 @@ import {
Tabs,
Text,
Title,
Tooltip,
UnstyledButton
Tooltip
} from '@mantine/core';
import {
IconExclamationCircle,
@@ -28,6 +27,7 @@ import {
useState
} from 'react';
import {
Link,
Navigate,
Route,
Routes,
@@ -45,7 +45,7 @@ import {
cancelEvent,
useInvenTreeHotkeys
} from '@lib/functions/Events';
import { eventModified, getBaseUrl } from '@lib/functions/Navigation';
import { eventModified } from '@lib/functions/Navigation';
import { navigateToLink } from '@lib/functions/Navigation';
import type {
PanelGroupType,
@@ -57,7 +57,6 @@ import { useDocumentVisibility, useWindowEvent } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import { useShallow } from 'zustand/react/shallow';
import { generateUrl } from '../../functions/urls';
import { usePluginPanels } from '../../hooks/UsePluginPanels';
import { useLocalState } from '../../states/LocalState';
import { vars } from '../../theme';
@@ -160,6 +159,9 @@ function PanelTabComponent({
key={`panel-label-${panel.name}`}
w={'100%'}
value={panel.name}
renderRoot={(props) => (
<Link {...props} to={`../${panel.name}${location.search}`} />
)}
leftSection={
<Indicator
position='top-end'
@@ -179,19 +181,7 @@ function PanelTabComponent({
onClick={(event: any) => onClick(event)}
>
<Group justify='left' gap='xs' wrap='nowrap'>
<UnstyledButton
component={'a'}
style={{
textAlign: 'left'
}}
href={generateUrl(
`/${getBaseUrl()}${location.pathname}/${panel.name}${
location.search
}`
)}
>
{expanded && panel.label}
</UnstyledButton>
{expanded && panel.label}
</Group>
</Tabs.Tab>
</Tooltip>
+2 -2
View File
@@ -47,7 +47,7 @@ export const doLogin = async (page: Page, options?: LoginOptions) => {
await page.waitForTimeout(100);
await page.waitForLoadState('networkidle');
await page.getByRole('link', { name: 'Dashboard' }).waitFor();
await page.getByRole('tab', { name: 'Dashboard' }).waitFor();
await page.getByRole('button', { name: 'navigation-menu' }).waitFor();
await page.waitForURL(/\/web(\/home)?/);
await page.waitForLoadState('networkidle');
@@ -93,7 +93,7 @@ export const doCachedLogin = async (
waitUntil: 'networkidle'
});
await page.getByRole('link', { name: 'Dashboard' }).waitFor();
await page.getByRole('tab', { name: 'Dashboard' }).waitFor();
await page.getByRole('button', { name: 'navigation-menu' }).waitFor();
await page.waitForURL(/\/web(\/home)?/);
await page.waitForLoadState('networkidle');
@@ -26,6 +26,8 @@ test('Dashboard - Basic', async ({ browser }) => {
await page.getByLabel('add-widget-ovr-so').click();
await page.getByLabel('add-widget-ovr-po').click();
await page.getByLabel('dashboard-widgets-filter-input').fill('latest parts');
await page.getByLabel('add-widget-latest-parts').click();
await page.getByLabel('dashboard-widgets-filter-clear').click();
@@ -37,6 +39,17 @@ test('Dashboard - Basic', async ({ browser }) => {
// Check that the widgets are visible
await page.getByText('Overdue Sales Orders').waitFor();
await page.getByText('Overdue Purchase Orders').waitFor();
await page.getByText('Latest parts').waitFor();
await expect(
page.getByRole('link', { name: /Overdue Sales Orders/ })
).toHaveAttribute('href', /\/web\/sales\/index\/salesorders\?overdue=true$/);
await expect(
page.getByRole('link', { name: 'Latest parts' })
).toHaveAttribute(
'href',
/\/web\/part\/category\/index\/parts\?ordering=-id$/
);
// Let's remove one of the widgets
await page.getByLabel('dashboard-menu').click();
+1 -1
View File
@@ -195,7 +195,7 @@ test('Parts - BOM', async ({ browser }) => {
await page.getByText('Robert Shuruncle').waitFor();
// Move the mouse away
await page.getByRole('link', { name: 'Bill of Materials' }).hover();
await page.getByRole('tab', { name: 'Bill of Materials' }).hover();
// Test sub-assembly row expansion (rows are inserted into the same table)
const assemblyRow = page
+1 -1
View File
@@ -38,7 +38,7 @@ test('Barcode Scanning - Basic', async ({ browser }) => {
// Navigate to the 'scan' page
await page.getByLabel('navigation-menu').click();
await page.getByRole('button', { name: 'Scan Barcode' }).click();
await page.getByRole('link', { name: 'Scan Barcode' }).click();
await page.getByText('Scan or enter barcode data').waitFor();
@@ -55,8 +55,16 @@ test('Stock - Location Tree', async ({ browser }) => {
await page.getByLabel('nav-breadcrumb-action').click();
await page.getByLabel('nav-tree-toggle-1}').click();
await expect(page.getByLabel('nav-tree-item-2')).toHaveAttribute(
'href',
/\/web\/stock\/location\/2\/?$/
);
await page.getByLabel('nav-tree-item-2').click();
await expect(
page.getByRole('link', { name: 'breadcrumb-1-factory' })
).toHaveAttribute('href', /\/web\/stock\/location\/1\/?$/);
await page.getByLabel('breadcrumb-2-storage-room-a').waitFor();
await page.getByLabel('breadcrumb-1-factory').click();
@@ -86,7 +86,7 @@ test('Transfer Order - Reference', async ({ browser }) => {
await page.getByText('Item Created').waitFor();
// go back to stock page
await page.getByRole('link', { name: 'Stock', exact: true }).click();
await page.getByRole('tab', { name: 'Stock', exact: true }).click();
await page
.getByRole('button', { name: 'action-button-add-transfer-' })
.click();
+33 -3
View File
@@ -1,4 +1,4 @@
import { test } from './baseFixtures.js';
import { expect, test } from './baseFixtures.js';
import { adminuser, stevenuser } from './defaults.js';
import { globalSearch } from './helpers.js';
import { doCachedLogin } from './login.js';
@@ -39,8 +39,36 @@ test('General - Search', async ({ browser }) => {
await page.locator('a').filter({ hasText: 'Customer D' }).first().waitFor();
await page.locator('a').filter({ hasText: 'Customer E' }).first().waitFor();
const customerResult = page
.locator('a')
.filter({ hasText: 'Customer B' })
.first();
await expect(customerResult).toHaveAttribute('href', /\/web\/company\/\d+/);
const popupPromise = page.context().waitForEvent('page');
await customerResult.click({ modifiers: ['Control'] });
const popup = await popupPromise;
await popup.waitForURL('**/web/company/**');
await popup.close();
// Click through to the "Customer" results
await page.getByRole('button', { name: 'view-all-results-customer' }).click();
const viewAllCustomers = page.getByRole('link', {
name: 'view-all-results-customer'
});
await expect(viewAllCustomers).toHaveAttribute(
'href',
/\/web\/sales\/index\/customers\?search=another\+customer/
);
const overviewPopupPromise = page.context().waitForEvent('page');
await viewAllCustomers.click({ modifiers: ['Control'] });
const overviewPopup = await overviewPopupPromise;
await overviewPopup.waitForURL(
'**/web/sales/index/customers?search=another+customer'
);
await overviewPopup.close();
await viewAllCustomers.click();
await page.waitForURL('**/sales/index/customers**');
await page.getByText('Custom table filters are active').waitFor();
@@ -58,7 +86,9 @@ test('General - Search', async ({ browser }) => {
.first()
.waitFor();
await page.getByLabel('view-all-results-manufacturerpart').click();
await page
.getByRole('link', { name: 'view-all-results-manufacturerpart' })
.click();
await page.waitForURL('**/purchasing/index/manufacturer-parts**');
await page.getByRole('cell', { name: 'RT0402BRD07100KL' }).waitFor();
});
+1 -1
View File
@@ -44,7 +44,7 @@ const loginAndMeasure = async (page: Page): Promise<number> => {
const start = Date.now();
await page.getByRole('button', { name: 'Log In' }).click();
await page.getByRole('link', { name: 'Dashboard' }).waitFor();
await page.getByRole('tab', { name: 'Dashboard' }).waitFor();
await page.getByRole('button', { name: 'navigation-menu' }).waitFor();
await page.waitForURL(/\/web(\/home)?/);
await page.waitForLoadState('networkidle');
+2 -2
View File
@@ -109,9 +109,9 @@ test('Spotlight - No Keys', async ({ browser }) => {
await page.getByText('About').first().waitFor();
await page
.getByRole('button', { name: 'Notifications', exact: true })
.getByRole('link', { name: 'Notifications', exact: true })
.waitFor();
await page.getByRole('button', { name: 'Dashboard', exact: true }).waitFor();
await page.getByRole('link', { name: 'Dashboard', exact: true }).waitFor();
// close the nav
await page.keyboard.press('Escape');
+1 -1
View File
@@ -116,7 +116,7 @@ test('Permissions - Reader', async ({ browser }) => {
// Navigate to the user / group info (via the navigation menu)
await page.getByLabel('navigation-menu').click();
await page.getByRole('button', { name: 'Users' }).click();
await page.getByRole('link', { name: 'Users' }).click();
await page.getByText('System Overview', { exact: true }).waitFor();
await loadTab(page, 'Users');
await loadTab(page, 'Groups');