diff --git a/src/frontend/src/components/SplashScreen.tsx b/src/frontend/src/components/SplashScreen.tsx new file mode 100644 index 0000000000..79bb9992ac --- /dev/null +++ b/src/frontend/src/components/SplashScreen.tsx @@ -0,0 +1,24 @@ +import { BackgroundImage } from '@mantine/core'; +import { generateUrl } from '../functions/urls'; +import { useServerApiState } from '../states/ApiState'; + +/** + * Render content within a "splash screen" container. + */ +export default function SplashScreen({ + children +}: { + children: React.ReactNode; +}) { + const [server] = useServerApiState((state) => [state.server]); + + if (server.customize?.splash) { + return ( + <BackgroundImage src={generateUrl(server.customize.splash)}> + {children} + </BackgroundImage> + ); + } else { + return <>{children}</>; + } +} diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index 788f205a86..c94b3cec09 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -342,7 +342,7 @@ export async function ProviderLogin( export function authApi( url: string, config: AxiosRequestConfig | undefined = undefined, - method: 'get' | 'post' | 'put' | 'delete' = 'get', + method: 'get' | 'patch' | 'post' | 'put' | 'delete' = 'get', data?: any ) { const requestConfig = config || {}; diff --git a/src/frontend/src/pages/Auth/ChangePassword.tsx b/src/frontend/src/pages/Auth/ChangePassword.tsx index 4e2087501c..0c2d47a329 100644 --- a/src/frontend/src/pages/Auth/ChangePassword.tsx +++ b/src/frontend/src/pages/Auth/ChangePassword.tsx @@ -15,6 +15,7 @@ import { notifications } from '@mantine/notifications'; import { useNavigate } from 'react-router-dom'; import { api } from '../../App'; +import SplashScreen from '../../components/SplashScreen'; import { StylishText } from '../../components/items/StylishText'; import { ProtectedRoute } from '../../components/nav/Layout'; import { LanguageContext } from '../../contexts/LanguageContext'; @@ -104,49 +105,53 @@ export default function Set_Password() { return ( <LanguageContext> <ProtectedRoute> - <Center mih='100vh'> - <Container w='md' miw={425}> - <Stack> - <StylishText size='xl'>{t`Reset Password`}</StylishText> - <Divider /> - {user.username() && ( - <Paper> - <Group> - <StylishText size='md'>{t`User`}</StylishText> - <Text>{user.username()}</Text> - </Group> - </Paper> - )} - <Divider /> - <Stack gap='xs'> - <PasswordInput - required - aria-label='password' - label={t`Current Password`} - description={t`Enter your current password`} - {...simpleForm.getInputProps('current_password')} - /> - <PasswordInput - required - aria-label='input-password-1' - label={t`New Password`} - description={t`Enter your new password`} - {...simpleForm.getInputProps('new_password1')} - /> - <PasswordInput - required - aria-label='input-password-2' - label={t`Confirm New Password`} - description={t`Confirm your new password`} - {...simpleForm.getInputProps('new_password2')} - /> - </Stack> - <Button type='submit' onClick={handleSet}> - <Trans>Confirm</Trans> - </Button> - </Stack> - </Container> - </Center> + <SplashScreen> + <Center mih='100vh'> + <Container w='md' miw={425}> + <Paper p='xl' withBorder> + <Stack> + <StylishText size='xl'>{t`Reset Password`}</StylishText> + <Divider /> + {user.username() && ( + <Paper> + <Group> + <StylishText size='md'>{t`Username`}</StylishText> + <Text>{user.username()}</Text> + </Group> + </Paper> + )} + <Divider /> + <Stack gap='xs'> + <PasswordInput + required + aria-label='password' + label={t`Current Password`} + description={t`Enter your current password`} + {...simpleForm.getInputProps('current_password')} + /> + <PasswordInput + required + aria-label='input-password-1' + label={t`New Password`} + description={t`Enter your new password`} + {...simpleForm.getInputProps('new_password1')} + /> + <PasswordInput + required + aria-label='input-password-2' + label={t`Confirm New Password`} + description={t`Confirm your new password`} + {...simpleForm.getInputProps('new_password2')} + /> + </Stack> + <Button type='submit' onClick={handleSet}> + <Trans>Confirm</Trans> + </Button> + </Stack> + </Paper> + </Container> + </Center> + </SplashScreen> </ProtectedRoute> </LanguageContext> ); diff --git a/src/frontend/src/pages/Auth/Login.tsx b/src/frontend/src/pages/Auth/Login.tsx index a4dba6c4ff..67a032ab03 100644 --- a/src/frontend/src/pages/Auth/Login.tsx +++ b/src/frontend/src/pages/Auth/Login.tsx @@ -1,16 +1,10 @@ import { t } from '@lingui/macro'; -import { - BackgroundImage, - Center, - Container, - Divider, - Paper, - Text -} from '@mantine/core'; +import { Center, Container, Divider, Paper, Text } from '@mantine/core'; import { useDisclosure, useToggle } from '@mantine/hooks'; import { useEffect, useMemo } from 'react'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { setApiDefaults } from '../../App'; +import SplashScreen from '../../components/SplashScreen'; import { AuthFormOptions } from '../../components/forms/AuthFormOptions'; import { AuthenticationForm, @@ -25,7 +19,6 @@ import { doBasicLogin, followRedirect } from '../../functions/auth'; -import { generateUrl } from '../../functions/urls'; import { useServerApiState } from '../../states/ApiState'; import { useLocalState } from '../../states/LocalState'; @@ -73,16 +66,6 @@ export default function Login() { return null; }, [server.customize]); - const SplashComponent = useMemo(() => { - const temp = server.customize?.splash; - if (temp) { - return ({ children }: { children: React.ReactNode }) => ( - <BackgroundImage src={generateUrl(temp)}>{children}</BackgroundImage> - ); - } - return ({ children }: { children: React.ReactNode }) => <>{children}</>; - }, [server.customize]); - // Data manipulation functions function ChangeHost(newHost: string | null): void { if (newHost === null) return; @@ -120,7 +103,7 @@ export default function Login() { // Main rendering block return ( - <SplashComponent> + <SplashScreen> <Center mih='100vh'> <div style={{ @@ -159,6 +142,6 @@ export default function Login() { </Container> </div> </Center> - </SplashComponent> + </SplashScreen> ); } diff --git a/src/frontend/src/pages/Auth/MFALogin.tsx b/src/frontend/src/pages/Auth/MFALogin.tsx index 4a832db3c0..7bed8b29f1 100644 --- a/src/frontend/src/pages/Auth/MFALogin.tsx +++ b/src/frontend/src/pages/Auth/MFALogin.tsx @@ -3,14 +3,16 @@ import { Button, Center, Container, + Paper, Stack, - TextInput, - Title + TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useLocation, useNavigate } from 'react-router-dom'; import { useState } from 'react'; +import SplashScreen from '../../components/SplashScreen'; +import { StylishText } from '../../components/items/StylishText'; import { LanguageContext } from '../../contexts/LanguageContext'; import { handleMfaLogin } from '../../functions/auth'; @@ -22,38 +24,40 @@ export default function MFALogin() { return ( <LanguageContext> - <Center mih='100vh'> - <Container w='md' miw={425}> - <Stack> - <Title> - <Trans>MFA Login</Trans> - </Title> - <Stack> - <TextInput - required - label={t`TOTP Code`} - name='TOTP' - description={t`Enter your TOTP or recovery code`} - {...simpleForm.getInputProps('code')} - error={loginError} - /> - </Stack> - <Button - type='submit' - onClick={() => - handleMfaLogin( - navigate, - location, - simpleForm.values, - setLoginError - ) - } - > - <Trans>Log in</Trans> - </Button> - </Stack> - </Container> - </Center> + <SplashScreen> + <Center mih='100vh'> + <Container w='md' miw={425}> + <Paper p='xl' withBorder> + <Stack> + <StylishText size='xl'>{t`Multi-Factor Login`}</StylishText> + <Stack> + <TextInput + required + label={t`TOTP Code`} + name='TOTP' + description={t`Enter your TOTP or recovery code`} + {...simpleForm.getInputProps('code')} + error={loginError} + /> + </Stack> + <Button + type='submit' + onClick={() => + handleMfaLogin( + navigate, + location, + simpleForm.values, + setLoginError + ) + } + > + <Trans>Log In</Trans> + </Button> + </Stack> + </Paper> + </Container> + </Center> + </SplashScreen> </LanguageContext> ); } diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx index 1ef92b58d3..1d23e34503 100644 --- a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx @@ -1,5 +1,6 @@ import { Trans, t } from '@lingui/macro'; import { + Accordion, Alert, Badge, Button, @@ -9,11 +10,13 @@ import { Loader, Modal, Radio, + SimpleGrid, Stack, Table, Text, TextInput, - Title + Title, + Tooltip } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { hideNotification, showNotification } from '@mantine/notifications'; @@ -43,91 +46,116 @@ export function SecurityContent() { return ( <Stack> - <Title order={5}> - <Trans>Email Addresses</Trans> - </Title> - <EmailSection /> - <Title order={5}> - <Trans>Single Sign On</Trans> - </Title> - {sso_enabled() ? ( - <ProviderSection auth_config={auth_config} /> - ) : ( - <Alert - icon={<IconAlertCircle size='1rem' />} - title={t`Not enabled`} - color='yellow' - > - <Trans>Single Sign On is not enabled for this server </Trans> - </Alert> - )} - <Title order={5}> - <Trans>Multifactor authentication</Trans> - </Title> - <MfaSection /> - <Title order={5}> - <Trans>Access Tokens</Trans> - </Title> - <TokenSection /> + <Accordion multiple defaultValue={['email', 'sso', 'mfa', 'token']}> + <Accordion.Item value='email'> + <Accordion.Control> + <StylishText size='lg'>{t`Email Addresses`}</StylishText> + </Accordion.Control> + <Accordion.Panel> + <EmailSection /> + </Accordion.Panel> + </Accordion.Item> + <Accordion.Item value='sso'> + <Accordion.Control> + <StylishText size='lg'>{t`Single Sign On`}</StylishText> + </Accordion.Control> + <Accordion.Panel> + {sso_enabled() ? ( + <ProviderSection auth_config={auth_config} /> + ) : ( + <Alert + icon={<IconAlertCircle size='1rem' />} + title={t`Not enabled`} + color='yellow' + > + <Trans>Single Sign On is not enabled for this server </Trans> + </Alert> + )} + </Accordion.Panel> + </Accordion.Item> + <Accordion.Item value='mfa'> + <Accordion.Control> + <StylishText size='lg'>{t`Multi-Factor Authentication`}</StylishText> + </Accordion.Control> + <Accordion.Panel> + <MfaSection /> + </Accordion.Panel> + </Accordion.Item> + <Accordion.Item value='token'> + <Accordion.Control> + <StylishText size='lg'>{t`Access Tokens`}</StylishText> + </Accordion.Control> + <Accordion.Panel> + <TokenSection /> + </Accordion.Panel> + </Accordion.Item> + </Accordion> </Stack> ); } function EmailSection() { - const [value, setValue] = useState<string>(''); + const [selectedEmail, setSelectedEmail] = useState<string>(''); const [newEmailValue, setNewEmailValue] = useState(''); const { isLoading, data, refetch } = useQuery({ queryKey: ['emails'], queryFn: () => authApi(apiUrl(ApiEndpoints.auth_email)).then((res) => res.data.data) }); + const emailAvailable = useMemo(() => { return data == undefined || data.length == 0; }, [data]); function runServerAction( - action: 'post' | 'put' | 'delete' = 'post', + action: 'patch' | 'post' | 'put' | 'delete' = 'post', data?: any ) { - const vals: any = data || { email: value }; - return authApi( - apiUrl(ApiEndpoints.auth_email), - undefined, - action, - vals - ).then(() => { - refetch(); - }); + const vals: any = data || { email: selectedEmail }; + return authApi(apiUrl(ApiEndpoints.auth_email), undefined, action, vals) + .then(() => { + refetch(); + }) + .catch((err) => { + hideNotification('email-error'); + + showNotification({ + id: 'email-error', + title: t`Error`, + message: t`Error while updating email`, + color: 'red' + }); + }); } if (isLoading) return <Loader />; return ( - <Grid> - <Grid.Col span={6}> - {emailAvailable ? ( - <Alert - icon={<IconAlertCircle size='1rem' />} - title={t`Not configured`} - color='yellow' - > - <Trans>Currently no email addresses are registered.</Trans> - </Alert> - ) : ( - <Radio.Group - value={value} - onChange={setValue} - name='email_accounts' - label={t`The following email addresses are associated with your account:`} - > - <Stack mt='xs'> - {data.map((email: any) => ( - <Radio - key={email.email} - value={String(email.email)} - label={ - <Group justify='space-between'> - {email.email} + <SimpleGrid cols={{ xs: 1, md: 2 }} spacing='sm'> + {emailAvailable ? ( + <Alert + icon={<IconAlertCircle size='1rem' />} + title={t`Not Configured`} + color='yellow' + > + <Trans>Currently no email addresses are registered.</Trans> + </Alert> + ) : ( + <Radio.Group + value={selectedEmail} + onChange={setSelectedEmail} + name='email_accounts' + label={t`The following email addresses are associated with your account:`} + > + <Stack mt='xs'> + {data.map((email: any) => ( + <Radio + key={email.email} + value={String(email.email)} + label={ + <Group justify='space-apart'> + {email.email} + <Group justify='right'> {email.primary && ( <Badge color='blue'> <Trans>Primary</Trans> @@ -143,53 +171,51 @@ function EmailSection() { </Badge> )} </Group> - } - /> - ))} - </Stack> - </Radio.Group> - )} - </Grid.Col> - <Grid.Col span={6}> - <Stack> - <Text> - <Trans>Add Email Address</Trans> - </Text> - <TextInput - label={t`E-Mail`} - placeholder={t`E-Mail address`} - leftSection={<IconAt />} - value={newEmailValue} - onChange={(event) => setNewEmailValue(event.currentTarget.value)} - /> - </Stack> - </Grid.Col> - <Grid.Col span={6}> - <Group> - <Button - onClick={() => - runServerAction('post', { email: value, primary: true }) - } - disabled={emailAvailable} - > - <Trans>Make Primary</Trans> - </Button> - <Button - onClick={() => runServerAction('put')} - disabled={emailAvailable} - > - <Trans>Re-send Verification</Trans> - </Button> - <Button - onClick={() => runServerAction('delete')} - disabled={emailAvailable} - > - <Trans>Remove</Trans> - </Button> - </Group> - </Grid.Col> - <Grid.Col span={6}> + </Group> + } + /> + ))} + <Group> + <Button + onClick={() => + runServerAction('patch', { + email: selectedEmail, + primary: true + }) + } + disabled={!selectedEmail} + > + <Trans>Make Primary</Trans> + </Button> + <Button + onClick={() => runServerAction('put')} + disabled={!selectedEmail} + > + <Trans>Re-send Verification</Trans> + </Button> + <Button + onClick={() => runServerAction('delete')} + disabled={!selectedEmail} + color='red' + > + <Trans>Remove</Trans> + </Button> + </Group> + </Stack> + </Radio.Group> + )} + <Stack> + <StylishText size='md'>{t`Add Email Address`}</StylishText> + <TextInput + label={t`E-Mail`} + placeholder={t`E-Mail address`} + leftSection={<IconAt />} + aria-label='email-address-input' + value={newEmailValue} + onChange={(event) => setNewEmailValue(event.currentTarget.value)} + /> <Button + aria-label='email-address-submit' onClick={() => runServerAction('post', { email: newEmailValue }).catch((err) => { if (err.status == 400) { @@ -207,8 +233,8 @@ function EmailSection() { > <Trans>Add Email</Trans> </Button> - </Grid.Col> - </Grid> + </Stack> + </SimpleGrid> ); } @@ -263,7 +289,7 @@ function ProviderSection({ {data.length == 0 ? ( <Alert icon={<IconAlertCircle size='1rem' />} - title={t`Not configured`} + title={t`Not Configured`} color='yellow' > <Trans>There are no providers connected to this account.</Trans> @@ -395,61 +421,61 @@ function MfaSection() { return ( <> <ReauthModal /> - <Grid> - <Grid.Col span={6}> - {data.length == 0 ? ( - <Alert icon={<IconAlertCircle size='1rem' />} color='yellow'> - <Trans>No factors configured</Trans> - </Alert> - ) : ( - <Table stickyHeader striped highlightOnHover withTableBorder> - <Table.Thead> - <Table.Tr> - <Table.Th> - <Trans>Type</Trans> - </Table.Th> - <Table.Th> - <Trans>Last used at</Trans> - </Table.Th> - <Table.Th> - <Trans>Created at</Trans> - </Table.Th> - <Table.Th> - <Trans>Actions</Trans> - </Table.Th> - </Table.Tr> - </Table.Thead> - <Table.Tbody>{rows}</Table.Tbody> - </Table> - )} - </Grid.Col> - <Grid.Col span={6}> - <MfaAddSection - usedFactors={usedFactors} - refetch={refetch} - showRecoveryCodes={showRecoveryCodes} - /> - <Modal - opened={recoveryCodesOpen} - onClose={() => { - refetch(); - closeRecoveryCodes(); - }} - title={t`Recovery Codes`} - centered + <SimpleGrid cols={{ xs: 1, md: 2 }} spacing='sm'> + {data.length == 0 ? ( + <Alert + title={t`Not Configured`} + icon={<IconAlertCircle size='1rem' />} + color='yellow' > - <Title order={3}> - <Trans>Unused Codes</Trans> - </Title> - <Code>{recoveryCodes?.unused_codes?.join('\n')}</Code> + <Trans>No multi-factor tokens configured for this account</Trans> + </Alert> + ) : ( + <Table stickyHeader striped highlightOnHover withTableBorder> + <Table.Thead> + <Table.Tr> + <Table.Th> + <Trans>Type</Trans> + </Table.Th> + <Table.Th> + <Trans>Last used at</Trans> + </Table.Th> + <Table.Th> + <Trans>Created at</Trans> + </Table.Th> + <Table.Th> + <Trans>Actions</Trans> + </Table.Th> + </Table.Tr> + </Table.Thead> + <Table.Tbody>{rows}</Table.Tbody> + </Table> + )} + <MfaAddSection + usedFactors={usedFactors} + refetch={refetch} + showRecoveryCodes={showRecoveryCodes} + /> + <Modal + opened={recoveryCodesOpen} + onClose={() => { + refetch(); + closeRecoveryCodes(); + }} + title={t`Recovery Codes`} + centered + > + <Title order={3}> + <Trans>Unused Codes</Trans> + </Title> + <Code>{recoveryCodes?.unused_codes?.join('\n')}</Code> - <Title order={3}> - <Trans>Used Codes</Trans> - </Title> - <Code>{recoveryCodes?.used_codes?.join('\n')}</Code> - </Modal> - </Grid.Col> - </Grid> + <Title order={3}> + <Trans>Used Codes</Trans> + </Title> + <Code>{recoveryCodes?.used_codes?.join('\n')}</Code> + </Modal> + </SimpleGrid> </> ); } @@ -554,16 +580,17 @@ function MfaAddSection({ return ( <Stack> <ReauthModal /> - <Text>Add Factor</Text> + <StylishText size='md'>{t`Add Token`}</StylishText> {possibleFactors.map((factor) => ( - <Button - key={factor.type} - onClick={factor.function} - disabled={factor.used} - variant='outline' - > - {factor.name} - </Button> + <Tooltip label={factor.description} key={factor.type}> + <Button + onClick={factor.function} + disabled={factor.used} + variant='outline' + > + {factor.name} + </Button> + </Tooltip> ))} <Modal opened={totpQrOpen} diff --git a/src/frontend/tests/helpers.ts b/src/frontend/tests/helpers.ts index a77121442c..f330c90387 100644 --- a/src/frontend/tests/helpers.ts +++ b/src/frontend/tests/helpers.ts @@ -89,6 +89,17 @@ export const navigate = async (page, url: string) => { await page.waitForLoadState('networkidle'); }; +/** + * CLick on the 'tab' element with the provided name + */ +export const loadTab = async (page, tabName) => { + await page + .getByLabel(/panel-tabs-/) + .getByRole('tab', { name: tabName }) + .click(); + await page.waitForLoadState('networkidle'); +}; + /** * Perform a 'global search' on the provided page, for the provided query text */ diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts index 6f112c882e..385dffc5a2 100644 --- a/src/frontend/tests/pages/pui_build.spec.ts +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -2,6 +2,7 @@ import { test } from '../baseFixtures.ts'; import { clearTableFilters, getRowFromCell, + loadTab, navigate, setTableChoiceFilter } from '../helpers.ts'; @@ -12,7 +13,7 @@ test('Build Order - Basic Tests', async ({ page }) => { // Navigate to the correct build order await page.getByRole('tab', { name: 'Manufacturing', exact: true }).click(); - await page.getByRole('tab', { name: 'Build Orders', exact: true }).click(); + await loadTab(page, 'Build Orders'); await clearTableFilters(page); @@ -57,11 +58,11 @@ test('Build Order - Basic Tests', async ({ page }) => { await page.getByRole('button', { name: 'Cancel' }).click(); // Click on some tabs - await page.getByRole('tab', { name: 'Attachments' }).click(); - await page.getByRole('tab', { name: 'Notes' }).click(); - await page.getByRole('tab', { name: 'Incomplete Outputs' }).click(); - await page.getByRole('tab', { name: 'Line Items' }).click(); - await page.getByRole('tab', { name: 'Allocated Stock' }).click(); + await loadTab(page, 'Attachments'); + await loadTab(page, 'Notes'); + await loadTab(page, 'Incomplete Outputs'); + await loadTab(page, 'Line Items'); + await loadTab(page, 'Allocated Stock'); // Check for expected text in the table await page.getByText('R_10R_0402_1%').waitFor(); @@ -70,7 +71,7 @@ test('Build Order - Basic Tests', async ({ page }) => { .waitFor(); // Check "test results" - await page.getByRole('tab', { name: 'Test Results' }).click(); + await loadTab(page, 'Test Results'); await page.getByText('Quantity: 25').waitFor(); await page.getByText('Continuity Checks').waitFor(); await page @@ -80,7 +81,7 @@ test('Build Order - Basic Tests', async ({ page }) => { await page.getByText('Add Test Result').waitFor(); // Click through to the "parent" build - await page.getByRole('tab', { name: 'Build Details' }).click(); + await loadTab(page, 'Build Details'); await page.getByRole('link', { name: 'BO0010' }).click(); await page .getByLabel('Build Details') @@ -119,7 +120,7 @@ test('Build Order - Build Outputs', async ({ page }) => { await doQuickLogin(page); await navigate(page, 'manufacturing/index/'); - await page.getByRole('tab', { name: 'Build Orders', exact: true }).click(); + await loadTab(page, 'Build Orders'); await clearTableFilters(page); @@ -128,7 +129,7 @@ test('Build Order - Build Outputs', async ({ page }) => { await page.getByText('Pending').first().waitFor(); await page.getByRole('cell', { name: 'BO0011' }).click(); - await page.getByRole('tab', { name: 'Incomplete Outputs' }).click(); + await loadTab(page, 'Incomplete Outputs'); // Create a new build output await page.getByLabel('action-button-add-build-output').click(); @@ -213,7 +214,7 @@ test('Build Order - Allocation', async ({ page }) => { await page.getByRole('cell', { name: 'Reel Storage', exact: true }).waitFor(); // Navigate to the "Incomplete Outputs" tab - await page.getByRole('tab', { name: 'Incomplete Outputs' }).click(); + await loadTab(page, 'Incomplete Outputs'); // Find output #7 const output7 = await page diff --git a/src/frontend/tests/pages/pui_company.spec.ts b/src/frontend/tests/pages/pui_company.spec.ts index 548ab81819..9f32e8af6a 100644 --- a/src/frontend/tests/pages/pui_company.spec.ts +++ b/src/frontend/tests/pages/pui_company.spec.ts @@ -1,5 +1,5 @@ import { test } from '../baseFixtures.js'; -import { navigate } from '../helpers.js'; +import { loadTab, navigate } from '../helpers.js'; import { doQuickLogin } from '../login.js'; test('Company', async ({ page }) => { @@ -8,23 +8,23 @@ test('Company', async ({ page }) => { await navigate(page, 'company/1/details'); await page.getByLabel('Details').getByText('DigiKey Electronics').waitFor(); await page.getByRole('cell', { name: 'https://www.digikey.com/' }).waitFor(); - await page.getByRole('tab', { name: 'Supplied Parts' }).click(); + await loadTab(page, 'Supplied Parts'); await page .getByRole('cell', { name: 'RR05P100KDTR-ND', exact: true }) .waitFor(); - await page.getByRole('tab', { name: 'Purchase Orders' }).click(); + await loadTab(page, 'Purchase Orders'); await page.getByRole('cell', { name: 'Molex connectors' }).first().waitFor(); - await page.getByRole('tab', { name: 'Stock Items' }).click(); + await loadTab(page, 'Stock Items'); await page .getByRole('cell', { name: 'Blue plastic enclosure' }) .first() .waitFor(); - await page.getByRole('tab', { name: 'Contacts' }).click(); + await loadTab(page, 'Contacts'); await page.getByRole('cell', { name: 'jimmy.mcleod@digikey.com' }).waitFor(); - await page.getByRole('tab', { name: 'Addresses' }).click(); + await loadTab(page, 'Addresses'); await page.getByRole('cell', { name: 'Carla Tunnel' }).waitFor(); - await page.getByRole('tab', { name: 'Attachments' }).click(); - await page.getByRole('tab', { name: 'Notes' }).click(); + await loadTab(page, 'Attachments'); + await loadTab(page, 'Notes'); // Let's edit the company details await page.getByLabel('action-menu-company-actions').click(); diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index 48a848bfa6..0310f8d503 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -1,5 +1,10 @@ import { test } from '../baseFixtures'; -import { clearTableFilters, getRowFromCell, navigate } from '../helpers'; +import { + clearTableFilters, + getRowFromCell, + loadTab, + navigate +} from '../helpers'; import { doQuickLogin } from '../login'; /** @@ -19,35 +24,35 @@ test('Parts - Tabs', async ({ page }) => { await page.getByPlaceholder('Search').fill('1551'); await page.getByText('1551ABK').click(); - await page.getByRole('tab', { name: 'Allocations' }).click(); - await page.getByRole('tab', { name: 'Used In' }).click(); - await page.getByRole('tab', { name: 'Pricing' }).click(); - await page.getByRole('tab', { name: 'Suppliers' }).click(); - await page.getByRole('tab', { name: 'Purchase Orders' }).click(); - await page.getByRole('tab', { name: 'Scheduling' }).click(); - await page.getByRole('tab', { name: 'Stock History' }).click(); - await page.getByRole('tab', { name: 'Attachments' }).click(); - await page.getByRole('tab', { name: 'Notes' }).click(); - await page.getByRole('tab', { name: 'Related Parts' }).click(); + await loadTab(page, 'Allocations'); + await loadTab(page, 'Used In'); + await loadTab(page, 'Pricing'); + await loadTab(page, 'Suppliers'); + await loadTab(page, 'Purchase Orders'); + await loadTab(page, 'Scheduling'); + await loadTab(page, 'Stock History'); + await loadTab(page, 'Attachments'); + await loadTab(page, 'Notes'); + await loadTab(page, 'Related Parts'); // Related Parts await page.getByText('1551ACLR').click(); - await page.getByRole('tab', { name: 'Part Details' }).click(); - await page.getByRole('tab', { name: 'Parameters' }).click(); + await loadTab(page, 'Part Details'); + await loadTab(page, 'Parameters'); await page .getByLabel('panel-tabs-part') .getByRole('tab', { name: 'Stock', exact: true }) .click(); - await page.getByRole('tab', { name: 'Allocations' }).click(); - await page.getByRole('tab', { name: 'Used In' }).click(); - await page.getByRole('tab', { name: 'Pricing' }).click(); + await loadTab(page, 'Allocations'); + await loadTab(page, 'Used In'); + await loadTab(page, 'Pricing'); await navigate(page, 'part/category/index/parts'); await page.getByText('Blue Chair').click(); - await page.getByRole('tab', { name: 'Bill of Materials' }).click(); - await page.getByRole('tab', { name: 'Build Orders' }).click(); + await loadTab(page, 'Bill of Materials'); + await loadTab(page, 'Build Orders'); }); test('Parts - Manufacturer Parts', async ({ page }) => { @@ -55,11 +60,11 @@ test('Parts - Manufacturer Parts', async ({ page }) => { await navigate(page, 'part/84/suppliers'); - await page.getByRole('tab', { name: 'Suppliers' }).click(); + await loadTab(page, 'Suppliers'); await page.getByText('Hammond Manufacturing').click(); - await page.getByRole('tab', { name: 'Parameters' }).click(); - await page.getByRole('tab', { name: 'Suppliers' }).click(); - await page.getByRole('tab', { name: 'Attachments' }).click(); + await loadTab(page, 'Parameters'); + await loadTab(page, 'Suppliers'); + await loadTab(page, 'Attachments'); await page.getByText('1551ACLR - 1551ACLR').waitFor(); }); @@ -68,11 +73,11 @@ test('Parts - Supplier Parts', async ({ page }) => { await navigate(page, 'part/15/suppliers'); - await page.getByRole('tab', { name: 'Suppliers' }).click(); + await loadTab(page, 'Suppliers'); await page.getByRole('cell', { name: 'DIG-84670-SJI' }).click(); - await page.getByRole('tab', { name: 'Received Stock' }).click(); // - await page.getByRole('tab', { name: 'Purchase Orders' }).click(); - await page.getByRole('tab', { name: 'Pricing' }).click(); + await loadTab(page, 'Received Stock'); // + await loadTab(page, 'Purchase Orders'); + await loadTab(page, 'Pricing'); await page.getByText('DIG-84670-SJI - R_550R_0805_1%').waitFor(); }); @@ -81,14 +86,14 @@ test('Parts - Locking', async ({ page }) => { // Navigate to a known assembly which is *not* locked await navigate(page, 'part/104/bom'); - await page.getByRole('tab', { name: 'Bill of Materials' }).click(); + await loadTab(page, 'Bill of Materials'); await page.getByLabel('action-button-add-bom-item').waitFor(); - await page.getByRole('tab', { name: 'Parameters' }).click(); + await loadTab(page, 'Parameters'); await page.getByLabel('action-button-add-parameter').waitFor(); // Navigate to a known assembly which *is* locked await navigate(page, 'part/100/bom'); - await page.getByRole('tab', { name: 'Bill of Materials' }).click(); + await loadTab(page, 'Bill of Materials'); await page.getByLabel('part-lock-icon').waitFor(); await page.getByText('Part is Locked', { exact: true }).waitFor(); @@ -98,7 +103,7 @@ test('Parts - Locking', async ({ page }) => { await page.getByText('In Production: 50').waitFor(); // Check the "parameters" tab also - await page.getByRole('tab', { name: 'Parameters' }).click(); + await loadTab(page, 'Parameters'); await page.getByText('Part parameters cannot be').waitFor(); }); @@ -107,7 +112,7 @@ test('Parts - Allocations', async ({ page }) => { // Let's look at the allocations for a single stock item await navigate(page, 'stock/item/324/'); - await page.getByRole('tab', { name: 'Allocations' }).click(); + await loadTab(page, 'Allocations'); await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor(); await page.getByRole('cell', { name: 'Making some blue chairs' }).waitFor(); @@ -124,7 +129,7 @@ test('Parts - Allocations', async ({ page }) => { await page.waitForTimeout(500); await page.waitForLoadState('networkidle'); - await page.getByRole('tab', { name: 'Allocations' }).click(); + await loadTab(page, 'Allocations'); await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor(); await page.getByRole('button', { name: 'Sales Order Allocations' }).waitFor(); @@ -177,7 +182,7 @@ test('Parts - Pricing (Nothing, BOM)', async ({ page }) => { await navigate(page, 'part/82/pricing'); await page.getByText('Small plastic enclosure, black').waitFor(); - await page.getByRole('tab', { name: 'Part Pricing' }).click(); + await loadTab(page, 'Part Pricing'); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByText('Last Updated').waitFor(); @@ -188,7 +193,7 @@ test('Parts - Pricing (Nothing, BOM)', async ({ page }) => { // Part with history await navigate(page, 'part/108/pricing'); await page.getByText('A chair - with blue paint').waitFor(); - await page.getByRole('tab', { name: 'Part Pricing' }).click(); + await loadTab(page, 'Part Pricing'); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByText('Last Updated').waitFor(); @@ -226,7 +231,7 @@ test('Parts - Pricing (Supplier)', async ({ page }) => { // Part await navigate(page, 'part/55/pricing'); await page.getByText('Ceramic capacitor, 100nF in').waitFor(); - await page.getByRole('tab', { name: 'Part Pricing' }).click(); + await loadTab(page, 'Part Pricing'); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByText('Last Updated').waitFor(); @@ -252,7 +257,7 @@ test('Parts - Pricing (Variant)', async ({ page }) => { // Part await navigate(page, 'part/106/pricing'); await page.getByText('A chair - available in multiple colors').waitFor(); - await page.getByRole('tab', { name: 'Part Pricing' }).click(); + await loadTab(page, 'Part Pricing'); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByText('Last Updated').waitFor(); @@ -278,7 +283,7 @@ test('Parts - Pricing (Internal)', async ({ page }) => { // Part await navigate(page, 'part/65/pricing'); await page.getByText('Socket head cap screw, M2').waitFor(); - await page.getByRole('tab', { name: 'Part Pricing' }).click(); + await loadTab(page, 'Part Pricing'); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByText('Last Updated').waitFor(); @@ -303,7 +308,7 @@ test('Parts - Pricing (Purchase)', async ({ page }) => { // Part await navigate(page, 'part/69/pricing'); await page.getByText('1.25mm Pitch, PicoBlade PCB').waitFor(); - await page.getByRole('tab', { name: 'Part Pricing' }).click(); + await loadTab(page, 'Part Pricing'); await page.getByLabel('Part Pricing').getByText('Part Pricing').waitFor(); await page.getByRole('button', { name: 'Pricing Overview' }).waitFor(); await page.getByText('Last Updated').waitFor(); diff --git a/src/frontend/tests/pages/pui_purchase_order.spec.ts b/src/frontend/tests/pages/pui_purchase_order.spec.ts index ce72489db9..11bdb5c8fe 100644 --- a/src/frontend/tests/pages/pui_purchase_order.spec.ts +++ b/src/frontend/tests/pages/pui_purchase_order.spec.ts @@ -3,6 +3,7 @@ import { clearTableFilters, clickButtonIfVisible, clickOnRowMenu, + loadTab, navigate, openFilterDrawer, setTableChoiceFilter @@ -13,7 +14,7 @@ test('Purchase Orders - List', async ({ page }) => { await doQuickLogin(page); await page.getByRole('tab', { name: 'Purchasing' }).click(); - await page.getByRole('tab', { name: 'Purchase Orders' }).click(); + await loadTab(page, 'Purchase Orders'); await clearTableFilters(page); @@ -101,29 +102,32 @@ test('Purchase Orders - General', async ({ page }) => { await doQuickLogin(page); await page.getByRole('tab', { name: 'Purchasing' }).click(); + await page.getByRole('cell', { name: 'PO0012' }).click(); await page.waitForTimeout(200); - await page.getByRole('tab', { name: 'Line Items' }).click(); - await page.getByRole('tab', { name: 'Received Stock' }).click(); - await page.getByRole('tab', { name: 'Attachments' }).click(); + await loadTab(page, 'Line Items'); + await loadTab(page, 'Received Stock'); + await loadTab(page, 'Attachments'); + await page.getByRole('tab', { name: 'Purchasing' }).click(); - await page.getByRole('tab', { name: 'Suppliers' }).click(); + await loadTab(page, 'Suppliers'); await page.getByText('Arrow', { exact: true }).click(); await page.waitForTimeout(200); - await page.getByRole('tab', { name: 'Supplied Parts' }).click(); - await page.getByRole('tab', { name: 'Purchase Orders' }).click(); - await page.getByRole('tab', { name: 'Stock Items' }).click(); - await page.getByRole('tab', { name: 'Contacts' }).click(); - await page.getByRole('tab', { name: 'Addresses' }).click(); - await page.getByRole('tab', { name: 'Attachments' }).click(); + await loadTab(page, 'Supplied Parts'); + await loadTab(page, 'Purchase Orders'); + await loadTab(page, 'Stock Items'); + await loadTab(page, 'Contacts'); + await loadTab(page, 'Addresses'); + await loadTab(page, 'Attachments'); + await page.getByRole('tab', { name: 'Purchasing' }).click(); - await page.getByRole('tab', { name: 'Manufacturers' }).click(); + await loadTab(page, 'Manufacturers'); await page.getByText('AVX Corporation').click(); await page.waitForTimeout(200); - await page.getByRole('tab', { name: 'Addresses' }).click(); + await loadTab(page, 'Addresses'); await page.getByRole('cell', { name: 'West Branch' }).click(); await page.locator('.mantine-ScrollArea-root').click(); await page @@ -151,7 +155,7 @@ test('Purchase Orders - Filters', async ({ page }) => { await doQuickLogin(page, 'reader', 'readonly'); await page.getByRole('tab', { name: 'Purchasing' }).click(); - await page.getByRole('tab', { name: 'Purchase Orders' }).click(); + await loadTab(page, 'Purchase Orders'); // Open filters drawer await openFilterDrawer(page); @@ -197,7 +201,7 @@ test('Purchase Orders - Order Parts', async ({ page }) => { // Open "Order Parts" wizard from the "Stock Items" table await page.getByRole('tab', { name: 'Stock' }).click(); - await page.getByRole('tab', { name: 'Stock Items' }).click(); + await loadTab(page, 'Stock Items'); // Select multiple stock items for (let ii = 2; ii < 7; ii += 2) { @@ -257,10 +261,10 @@ test('Purchase Orders - Receive Items', async ({ page }) => { await page.getByRole('tab', { name: 'Purchasing' }).click(); await page.getByRole('cell', { name: 'PO0014' }).click(); - await page.getByRole('tab', { name: 'Order Details' }).click(); + await loadTab(page, 'Order Details'); // Select all line items to receive - await page.getByRole('tab', { name: 'Line Items' }).click(); + await loadTab(page, 'Line Items'); await page.getByLabel('Select all records').click(); await page.waitForTimeout(200); @@ -311,7 +315,7 @@ test('Purchase Orders - Receive Items', async ({ page }) => { await page.getByRole('button', { name: 'Submit' }).click(); await page.getByText('Items received').waitFor(); - await page.getByRole('tab', { name: 'Received Stock' }).click(); + await loadTab(page, 'Received Stock'); await clearTableFilters(page); await page.getByRole('cell', { name: 'my-batch-code' }).first().waitFor(); diff --git a/src/frontend/tests/pages/pui_sales_order.spec.ts b/src/frontend/tests/pages/pui_sales_order.spec.ts index ab766899b5..0be3b233bb 100644 --- a/src/frontend/tests/pages/pui_sales_order.spec.ts +++ b/src/frontend/tests/pages/pui_sales_order.spec.ts @@ -2,6 +2,7 @@ import { test } from '../baseFixtures.ts'; import { clearTableFilters, globalSearch, + loadTab, navigate, setTableChoiceFilter } from '../helpers.ts'; @@ -13,27 +14,27 @@ test('Sales Orders - Tabs', async ({ page }) => { await navigate(page, 'sales/index/'); await page.waitForURL('**/platform/sales/**'); - await page.getByRole('tab', { name: 'Sales Orders' }).click(); + await loadTab(page, 'Sales Orders'); await page.waitForURL('**/platform/sales/index/salesorders'); - await page.getByRole('tab', { name: 'Return Orders' }).click(); + await loadTab(page, 'Return Orders'); // Customers - await page.getByRole('tab', { name: 'Customers' }).click(); + await loadTab(page, 'Customers'); await page.getByText('Customer A').click(); - await page.getByRole('tab', { name: 'Notes' }).click(); - await page.getByRole('tab', { name: 'Attachments' }).click(); - await page.getByRole('tab', { name: 'Contacts' }).click(); - await page.getByRole('tab', { name: 'Assigned Stock' }).click(); - await page.getByRole('tab', { name: 'Return Orders' }).click(); - await page.getByRole('tab', { name: 'Sales Orders' }).click(); - await page.getByRole('tab', { name: 'Contacts' }).click(); + await loadTab(page, 'Notes'); + await loadTab(page, 'Attachments'); + await loadTab(page, 'Contacts'); + await loadTab(page, 'Assigned Stock'); + await loadTab(page, 'Return Orders'); + await loadTab(page, 'Sales Orders'); + await loadTab(page, 'Contacts'); await page.getByRole('cell', { name: 'Dorathy Gross' }).waitFor(); await page .getByRole('row', { name: 'Dorathy Gross dorathy.gross@customer.com' }) .waitFor(); // Sales Order Details - await page.getByRole('tab', { name: 'Sales Orders' }).click(); + await loadTab(page, 'Sales Orders'); await clearTableFilters(page); @@ -42,30 +43,30 @@ test('Sales Orders - Tabs', async ({ page }) => { .getByLabel('Order Details') .getByText('Selling some stuff') .waitFor(); - await page.getByRole('tab', { name: 'Line Items' }).click(); - await page.getByRole('tab', { name: 'Shipments' }).click(); - await page.getByRole('tab', { name: 'Build Orders' }).click(); + await loadTab(page, 'Line Items'); + await loadTab(page, 'Shipments'); + await loadTab(page, 'Build Orders'); await page.getByText('No records found').first().waitFor(); - await page.getByRole('tab', { name: 'Attachments' }).click(); + await loadTab(page, 'Attachments'); await page.getByText('No attachments found').first().waitFor(); - await page.getByRole('tab', { name: 'Notes' }).click(); - await page.getByRole('tab', { name: 'Order Details' }).click(); + await loadTab(page, 'Notes'); + await loadTab(page, 'Order Details'); // Return Order Details await page.getByRole('link', { name: 'Customer A' }).click(); - await page.getByRole('tab', { name: 'Return Orders' }).click(); + await loadTab(page, 'Return Orders'); await page.getByRole('cell', { name: 'RMA-' }).click(); await page.getByText('RMA-0001', { exact: true }).waitFor(); - await page.getByRole('tab', { name: 'Line Items' }).click(); - await page.getByRole('tab', { name: 'Attachments' }).click(); - await page.getByRole('tab', { name: 'Notes' }).click(); + await loadTab(page, 'Line Items'); + await loadTab(page, 'Attachments'); + await loadTab(page, 'Notes'); }); test('Sales Orders - Basic Tests', async ({ page }) => { await doQuickLogin(page); await page.getByRole('tab', { name: 'Sales' }).click(); - await page.getByRole('tab', { name: 'Sales Orders' }).click(); + await loadTab(page, 'Sales Orders'); await clearTableFilters(page); @@ -104,12 +105,12 @@ test('Sales Orders - Shipments', async ({ page }) => { await doQuickLogin(page); await page.getByRole('tab', { name: 'Sales' }).click(); - await page.getByRole('tab', { name: 'Sales Orders' }).click(); + await loadTab(page, 'Sales Orders'); await clearTableFilters(page); // Click through to a particular sales order await page.getByRole('cell', { name: 'SO0006' }).first().click(); - await page.getByRole('tab', { name: 'Shipments' }).click(); + await loadTab(page, 'Shipments'); // Create a new shipment await page.getByLabel('action-button-add-shipment').click(); @@ -155,14 +156,14 @@ test('Sales Orders - Shipments', async ({ page }) => { await page.getByRole('menuitem', { name: 'View Shipment' }).click(); // Click through the various tabs - await page.getByRole('tab', { name: 'Attachments' }).click(); - await page.getByRole('tab', { name: 'Notes' }).click(); - await page.getByRole('tab', { name: 'Allocated Stock' }).click(); + await loadTab(page, 'Attachments'); + await loadTab(page, 'Notes'); + await loadTab(page, 'Allocated Stock'); // Ensure assigned items table loads correctly await page.getByRole('cell', { name: 'BATCH-001' }).first().waitFor(); - await page.getByRole('tab', { name: 'Shipment Details' }).click(); + await loadTab(page, 'Shipment Details'); // The "new" tracking number should be visible await page.getByText(tracking_number).waitFor(); @@ -171,7 +172,7 @@ test('Sales Orders - Shipments', async ({ page }) => { await page.getByRole('link', { name: 'SO0006' }).click(); // Let's try to allocate some stock - await page.getByRole('tab', { name: 'Line Items' }).click(); + await loadTab(page, 'Line Items'); await page.getByLabel('row-action-menu-1').click(); await page.getByRole('menuitem', { name: 'Allocate stock' }).click(); await page diff --git a/src/frontend/tests/pages/pui_stock.spec.ts b/src/frontend/tests/pages/pui_stock.spec.ts index b42817b4b5..97381c5d6e 100644 --- a/src/frontend/tests/pages/pui_stock.spec.ts +++ b/src/frontend/tests/pages/pui_stock.spec.ts @@ -2,6 +2,7 @@ import { test } from '../baseFixtures.js'; import { clearTableFilters, clickButtonIfVisible, + loadTab, navigate, openFilterDrawer, setTableChoiceFilter @@ -14,28 +15,28 @@ test('Stock - Basic Tests', async ({ page }) => { await navigate(page, 'stock/location/index/'); await page.waitForURL('**/platform/stock/location/**'); - await page.getByRole('tab', { name: 'Location Details' }).click(); + await loadTab(page, 'Location Details'); await page.waitForURL('**/platform/stock/location/index/details'); - await page.getByRole('tab', { name: 'Stock Items' }).click(); + await loadTab(page, 'Stock Items'); await page.getByText('1551ABK').first().click(); await page.getByRole('tab', { name: 'Stock', exact: true }).click(); await page.waitForURL('**/platform/stock/**'); - await page.getByRole('tab', { name: 'Stock Locations' }).click(); + await loadTab(page, 'Stock Locations'); await page.getByRole('cell', { name: 'Electronics Lab' }).first().click(); - await page.getByRole('tab', { name: 'Default Parts' }).click(); - await page.getByRole('tab', { name: 'Stock Locations' }).click(); - await page.getByRole('tab', { name: 'Stock Items' }).click(); - await page.getByRole('tab', { name: 'Location Details' }).click(); + await loadTab(page, 'Default Parts'); + await loadTab(page, 'Stock Locations'); + await loadTab(page, 'Stock Items'); + await loadTab(page, 'Location Details'); await navigate(page, 'stock/item/1194/details'); await page.getByText('D.123 | Doohickey').waitFor(); await page.getByText('Batch Code: BX-123-2024-2-7').waitFor(); - await page.getByRole('tab', { name: 'Stock Tracking' }).click(); - await page.getByRole('tab', { name: 'Test Data' }).click(); + await loadTab(page, 'Stock Tracking'); + await loadTab(page, 'Test Data'); await page.getByText('395c6d5586e5fb656901d047be27e1f7').waitFor(); - await page.getByRole('tab', { name: 'Installed Items' }).click(); + await loadTab(page, 'Installed Items'); }); test('Stock - Location Tree', async ({ page }) => { @@ -43,7 +44,7 @@ test('Stock - Location Tree', async ({ page }) => { await navigate(page, 'stock/location/index/'); await page.waitForURL('**/platform/stock/location/**'); - await page.getByRole('tab', { name: 'Location Details' }).click(); + await loadTab(page, 'Location Details'); await page.getByLabel('nav-breadcrumb-action').click(); await page.getByLabel('nav-tree-toggle-1}').click(); @@ -59,7 +60,7 @@ test('Stock - Filters', async ({ page }) => { await doQuickLogin(page, 'steven', 'wizardstaff'); await navigate(page, 'stock/location/index/'); - await page.getByRole('tab', { name: 'Stock Items' }).click(); + await loadTab(page, 'Stock Items'); await openFilterDrawer(page); await clickButtonIfVisible(page, 'Clear Filters'); @@ -238,7 +239,7 @@ test('Stock - Tracking', async ({ page }) => { await page.getByRole('link', { name: 'Widget Assembly # 2' }).waitFor(); // Navigate to the "stock tracking" tab - await page.getByRole('tab', { name: 'Stock Tracking' }).click(); + await loadTab(page, 'Stock Tracking'); await page.getByText('- - Factory/Office Block/Room').first().waitFor(); await page.getByRole('link', { name: 'Widget Assembly' }).waitFor(); await page.getByRole('cell', { name: 'Installed into assembly' }).waitFor(); diff --git a/src/frontend/tests/pui_plugins.spec.ts b/src/frontend/tests/pui_plugins.spec.ts index 6234421e00..887a21fe6a 100644 --- a/src/frontend/tests/pui_plugins.spec.ts +++ b/src/frontend/tests/pui_plugins.spec.ts @@ -1,6 +1,6 @@ import test from 'playwright/test'; -import { navigate } from './helpers.js'; +import { loadTab, navigate } from './helpers.js'; import { doQuickLogin } from './login.js'; import { setPluginState, setSettingState } from './settings.js'; @@ -29,19 +29,18 @@ test('Plugins - Panels', async ({ page, request }) => { await navigate(page, 'part/69/'); // Ensure basic part tab is available - await page.getByRole('tab', { name: 'Part Details' }).waitFor(); + await loadTab(page, 'Part Details'); // Allow time for the plugin panels to load (they are loaded asynchronously) await page.waitForTimeout(1000); // Check out each of the plugin panels - - await page.getByRole('tab', { name: 'Broken Panel' }).click(); + await loadTab(page, 'Broken Panel'); await page.waitForTimeout(500); await page.getByText('Error occurred while loading plugin content').waitFor(); - await page.getByRole('tab', { name: 'Dynamic Panel' }).click(); + await loadTab(page, 'Dynamic Panel'); await page.waitForTimeout(500); await page.getByText('Instance ID: 69'); @@ -49,7 +48,7 @@ test('Plugins - Panels', async ({ page, request }) => { .getByText('This panel has been dynamically rendered by the plugin system') .waitFor(); - await page.getByRole('tab', { name: 'Part Panel', exact: true }).click(); + await loadTab(page, 'Part Panel'); await page.waitForTimeout(500); await page.getByText('This content has been rendered by a custom plugin'); diff --git a/src/frontend/tests/pui_printing.spec.ts b/src/frontend/tests/pui_printing.spec.ts index 945dc7e74c..04c762416c 100644 --- a/src/frontend/tests/pui_printing.spec.ts +++ b/src/frontend/tests/pui_printing.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from './baseFixtures.js'; -import { navigate } from './helpers.js'; +import { loadTab, navigate } from './helpers.js'; import { doQuickLogin } from './login.js'; import { setPluginState } from './settings.js'; @@ -14,7 +14,7 @@ test('Label Printing', async ({ page }) => { await navigate(page, 'stock/location/index/'); await page.waitForURL('**/platform/stock/location/**'); - await page.getByRole('tab', { name: 'Stock Items' }).click(); + await loadTab(page, 'Stock Items'); // Select some labels await page.getByLabel('Select record 1', { exact: true }).click(); @@ -60,7 +60,7 @@ test('Report Printing', async ({ page }) => { // Navigate to a specific PurchaseOrder await page.getByRole('tab', { name: 'Purchasing' }).click(); - await page.getByRole('tab', { name: 'Purchase Orders' }).click(); + await loadTab(page, 'Purchase Orders'); await page.getByRole('cell', { name: 'PO0009' }).click(); @@ -98,7 +98,7 @@ test('Report Editing', async ({ page, request }) => { // Navigate to the admin center await page.getByRole('button', { name: 'admin' }).click(); await page.getByRole('menuitem', { name: 'Admin Center' }).click(); - await page.getByRole('tab', { name: 'Label Templates' }).click(); + await loadTab(page, 'Label Templates'); await page .getByRole('cell', { name: 'InvenTree Stock Item Label (' }) .click(); diff --git a/src/frontend/tests/pui_settings.spec.ts b/src/frontend/tests/pui_settings.spec.ts index deaedea25b..9afe28f219 100644 --- a/src/frontend/tests/pui_settings.spec.ts +++ b/src/frontend/tests/pui_settings.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from './baseFixtures.js'; import { apiUrl } from './defaults.js'; -import { getRowFromCell, navigate } from './helpers.js'; +import { getRowFromCell, loadTab, navigate } from './helpers.js'; import { doQuickLogin } from './login.js'; import { setSettingState } from './settings.js'; @@ -49,47 +49,47 @@ test('Settings - Admin', async ({ page }) => { // User settings await page.getByRole('button', { name: 'admin' }).click(); await page.getByRole('menuitem', { name: 'Account settings' }).click(); - await page.getByRole('tab', { name: 'Security' }).click(); + await loadTab(page, 'Security'); - await page.getByRole('tab', { name: 'Display Options' }).click(); + await loadTab(page, 'Display Options'); await page.getByText('Date Format').waitFor(); - await page.getByRole('tab', { name: 'Search' }).click(); + await loadTab(page, 'Search'); await page.getByText('Regex Search').waitFor(); - await page.getByRole('tab', { name: 'Notifications' }).click(); - await page.getByRole('tab', { name: 'Reporting' }).click(); + await loadTab(page, 'Notifications'); + await loadTab(page, 'Reporting'); await page.getByText('Inline report display').waitFor(); // System Settings await page.locator('label').filter({ hasText: 'System Settings' }).click(); await page.getByText('Base URL', { exact: true }).waitFor(); - await page.getByRole('tab', { name: 'Login' }).click(); - await page.getByRole('tab', { name: 'Barcodes' }).click(); - await page.getByRole('tab', { name: 'Notifications' }).click(); - await page.getByRole('tab', { name: 'Pricing' }).click(); - await page.getByRole('tab', { name: 'Labels' }).click(); - await page.getByRole('tab', { name: 'Reporting' }).click(); + await loadTab(page, 'Login'); + await loadTab(page, 'Barcodes'); + await loadTab(page, 'Notifications'); + await loadTab(page, 'Pricing'); + await loadTab(page, 'Labels'); + await loadTab(page, 'Reporting'); - await page.getByRole('tab', { name: 'Build Orders' }).click(); - await page.getByRole('tab', { name: 'Purchase Orders' }).click(); - await page.getByRole('tab', { name: 'Sales Orders' }).click(); - await page.getByRole('tab', { name: 'Return Orders' }).click(); + await loadTab(page, 'Build Orders'); + await loadTab(page, 'Purchase Orders'); + await loadTab(page, 'Sales Orders'); + await loadTab(page, 'Return Orders'); // Admin Center await page.getByRole('button', { name: 'admin' }).click(); await page.getByRole('menuitem', { name: 'Admin Center' }).click(); - await page.getByRole('tab', { name: 'Background Tasks' }).click(); - await page.getByRole('tab', { name: 'Error Reports' }).click(); - await page.getByRole('tab', { name: 'Currencies' }).click(); - await page.getByRole('tab', { name: 'Project Codes' }).click(); - await page.getByRole('tab', { name: 'Custom Units' }).click(); - await page.getByRole('tab', { name: 'Part Parameters' }).click(); - await page.getByRole('tab', { name: 'Category Parameters' }).click(); - await page.getByRole('tab', { name: 'Label Templates' }).click(); - await page.getByRole('tab', { name: 'Report Templates' }).click(); - await page.getByRole('tab', { name: 'Plugins' }).click(); + await loadTab(page, 'Background Tasks'); + await loadTab(page, 'Error Reports'); + await loadTab(page, 'Currencies'); + await loadTab(page, 'Project Codes'); + await loadTab(page, 'Custom Units'); + await loadTab(page, 'Part Parameters'); + await loadTab(page, 'Category Parameters'); + await loadTab(page, 'Label Templates'); + await loadTab(page, 'Report Templates'); + await loadTab(page, 'Plugins'); // Adjust some "location type" items - await page.getByRole('tab', { name: 'Location Types' }).click(); + await loadTab(page, 'Location Types'); // Edit first item ('Room') const roomCell = await page.getByRole('cell', { name: 'Room', exact: true }); @@ -166,7 +166,7 @@ test('Settings - Admin - Barcode History', async ({ page, request }) => { await page.getByRole('button', { name: 'admin' }).click(); await page.getByRole('menuitem', { name: 'Admin Center' }).click(); - await page.getByRole('tab', { name: 'Barcode Scans' }).click(); + await loadTab(page, 'Barcode Scans'); await page.waitForTimeout(500); @@ -193,8 +193,8 @@ test('Settings - Admin - Unauthorized', async ({ page }) => { await navigate(page, 'settings/user/'); await page.waitForURL('**/settings/user/**'); - await page.getByRole('tab', { name: 'Display Options' }).click(); - await page.getByRole('tab', { name: 'Account' }).click(); + await loadTab(page, 'Display Options'); + await loadTab(page, 'Account'); // Try to access global settings page await navigate(page, 'settings/system/'); @@ -205,3 +205,25 @@ test('Settings - Admin - Unauthorized', async ({ page }) => { .getByRole('button', { name: 'Return to the index page' }) .waitFor(); }); + +// Test for user auth configuration +test('Settings - Auth - Email', async ({ page }) => { + await doQuickLogin(page, 'allaccess', 'nolimits'); + await navigate(page, 'settings/user/'); + + await loadTab(page, 'Security'); + + await page.getByText('Currently no email addresses are registered').waitFor(); + await page.getByLabel('email-address-input').fill('test-email@domain.org'); + await page.getByLabel('email-address-submit').click(); + + await page.getByText('Unverified', { exact: true }).waitFor(); + await page.getByLabel('test-email@domain.').click(); + await page.getByRole('button', { name: 'Make Primary' }).click(); + await page.getByText('Primary', { exact: true }).waitFor(); + await page.getByRole('button', { name: 'Remove' }).click(); + + await page.getByText('Currently no email addresses are registered').waitFor(); + + await page.waitForTimeout(2500); +});