diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx index bee2c7b431..c0d5d6fe3c 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx @@ -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 ( - - - - - {title} - - - - {result} - + const content = ( + + + + {title} + + + + {result} + + ); + + return url ? ( + + {content} + ) : ( + content ); } diff --git a/src/frontend/src/components/dashboard/widgets/QueryDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryDashboardWidget.tsx index 527041c50a..1b8ac15558 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryDashboardWidget.tsx @@ -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 = ( + + + + {title} + + ); - return ( - - - - - {title} - - + return url ? ( + + {content} + ) : ( + content ); } diff --git a/src/frontend/src/components/items/MenuLinks.tsx b/src/frontend/src/components/items/MenuLinks.tsx index 3a8c8a1e21..9e76a1ee46 100644 --- a/src/frontend/src/components/items/MenuLinks.tsx +++ b/src/frontend/src/components/items/MenuLinks.tsx @@ -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({ + ) : item.link ? ( + { + if ( + event.button === 0 && + !event.ctrlKey && + !event.metaKey && + !event.shiftKey && + !event.altKey + ) { + beforeClick?.(); + } + }} + > + + {item.icon && ( + + )} + + {item.title} + + + ) : ( { - if (item.link) { - beforeClick?.(); - navigateToLink(item.link, navigate, event); - } else if (item.action) { + if (item.action) { beforeClick?.(); item.action(); } diff --git a/src/frontend/src/components/nav/BreadcrumbList.tsx b/src/frontend/src/components/nav/BreadcrumbList.tsx index e7d649f046..ca8d7c376d 100644 --- a/src/frontend/src/components/nav/BreadcrumbList.tsx +++ b/src/frontend/src/components/nav/BreadcrumbList.tsx @@ -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({ )} {elements.map((breadcrumb, index) => { - return ( - - breadcrumb.url && - navigateToLink(breadcrumb.url, navigate, event) - } - > - + const content = ( + + {breadcrumb.icon} + {breadcrumb.name} + + ); + const ariaLabel = `breadcrumb-${index}-${identifierString( + breadcrumb.name + )}`; + + if (breadcrumb.url === '#') { + return ( + {breadcrumb.icon} {breadcrumb.name} + ); + } + + return ( + + {content} ); })} diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx index 3e4d34fd21..81e6adb671 100644 --- a/src/frontend/src/components/nav/Header.tsx +++ b/src/frontend/src/components/nav/Header.tsx @@ -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() { } leftSection={ withIcons && tab.icon && ( {tab.icon} ) } - onClick={(event: any) => - navigateToLink(`/${tab.name}`, navigate, event) - } > - - {tab.title} - + {tab.title} ); }); @@ -329,9 +312,9 @@ function NavTabs() { - navigateToLink(nav.options.options.url, navigate, event) - } + renderRoot={(props) => ( + + )} > {nav.options.title} diff --git a/src/frontend/src/components/nav/NavigationTree.tsx b/src/frontend/src/components/nav/NavigationTree.tsx index 43640b3888..6e9adc5470 100644 --- a/src/frontend/src/components/nav/NavigationTree.tsx +++ b/src/frontend/src/components/nav/NavigationTree.tsx @@ -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({ > 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 diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx index e629b5a1be..11d9663a0f 100644 --- a/src/frontend/src/components/nav/SearchDrawer.tsx +++ b/src/frontend/src/components/nav/SearchDrawer.tsx @@ -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: - }); + const handleViewAllClick = useCallback( + (event: React.MouseEvent) => { + 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({ - - - + {resultsUrl ? ( + ( + + )} + size='sm' + variant='transparent' + radius='xs' + aria-label={`view-all-results-${query.searchKey ?? query.model}`} + > + + + ) : ( + + + + )} {query.title ?? modelInfo.label_multiple} @@ -180,7 +183,8 @@ function QueryResultGroup({ 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)} diff --git a/src/frontend/src/components/panels/PanelGroup.tsx b/src/frontend/src/components/panels/PanelGroup.tsx index 05e74d23bc..f3ff7b8d80 100644 --- a/src/frontend/src/components/panels/PanelGroup.tsx +++ b/src/frontend/src/components/panels/PanelGroup.tsx @@ -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) => ( + + )} leftSection={ onClick(event)} > - - {expanded && panel.label} - + {expanded && panel.label} diff --git a/src/frontend/tests/login.ts b/src/frontend/tests/login.ts index 9db3a95197..031e288ab4 100644 --- a/src/frontend/tests/login.ts +++ b/src/frontend/tests/login.ts @@ -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'); diff --git a/src/frontend/tests/pages/pui_dashboard.spec.ts b/src/frontend/tests/pages/pui_dashboard.spec.ts index 435d8e29cf..a6dd0e4d44 100644 --- a/src/frontend/tests/pages/pui_dashboard.spec.ts +++ b/src/frontend/tests/pages/pui_dashboard.spec.ts @@ -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(); diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index 2990d4d49f..b8b6fe07ab 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -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 diff --git a/src/frontend/tests/pages/pui_scan.spec.ts b/src/frontend/tests/pages/pui_scan.spec.ts index 8a59b1845a..58c5d6c490 100644 --- a/src/frontend/tests/pages/pui_scan.spec.ts +++ b/src/frontend/tests/pages/pui_scan.spec.ts @@ -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(); diff --git a/src/frontend/tests/pages/pui_stock.spec.ts b/src/frontend/tests/pages/pui_stock.spec.ts index 517ae4da9e..30d0e7dea3 100644 --- a/src/frontend/tests/pages/pui_stock.spec.ts +++ b/src/frontend/tests/pages/pui_stock.spec.ts @@ -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(); diff --git a/src/frontend/tests/pages/pui_transfer.spec.ts b/src/frontend/tests/pages/pui_transfer.spec.ts index 83331cd1b4..420cf4694f 100644 --- a/src/frontend/tests/pages/pui_transfer.spec.ts +++ b/src/frontend/tests/pages/pui_transfer.spec.ts @@ -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(); diff --git a/src/frontend/tests/pui_general.spec.ts b/src/frontend/tests/pui_general.spec.ts index 9e3eb2fab2..25c1feeec5 100644 --- a/src/frontend/tests/pui_general.spec.ts +++ b/src/frontend/tests/pui_general.spec.ts @@ -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(); }); diff --git a/src/frontend/tests/pui_login.spec.ts b/src/frontend/tests/pui_login.spec.ts index aacd04b9be..6251aa3029 100644 --- a/src/frontend/tests/pui_login.spec.ts +++ b/src/frontend/tests/pui_login.spec.ts @@ -44,7 +44,7 @@ const loginAndMeasure = async (page: Page): Promise => { 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'); diff --git a/src/frontend/tests/pui_modals.spec.ts b/src/frontend/tests/pui_modals.spec.ts index b476c989ef..e9810e83ad 100644 --- a/src/frontend/tests/pui_modals.spec.ts +++ b/src/frontend/tests/pui_modals.spec.ts @@ -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'); diff --git a/src/frontend/tests/pui_permissions.spec.ts b/src/frontend/tests/pui_permissions.spec.ts index 60f71fec0c..77d00351b0 100644 --- a/src/frontend/tests/pui_permissions.spec.ts +++ b/src/frontend/tests/pui_permissions.spec.ts @@ -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');