mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-01 03:00:54 +00:00
feat: improve user/group management actions (#9602)
* feat: improve user management actions add "open profile" actions * add lock / unlock action * add actions for password reset * submit coverage info to codecov no idea why this was turned off * bump api version * add frontend test * add backend test * fix test state * move test * fix style * fix name * hide password change if not superuser * bump playwright see https://github.com/microsoft/playwright/issues/35183 * fix test * fix test order * simplify test --------- Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
3
.github/workflows/qc_checks.yaml
vendored
3
.github/workflows/qc_checks.yaml
vendored
@ -621,17 +621,14 @@ jobs:
|
|||||||
path: src/frontend/playwright-report/
|
path: src/frontend/playwright-report/
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
- name: Report coverage
|
- name: Report coverage
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false
|
run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false
|
||||||
- name: Upload coverage reports to Codecov
|
- name: Upload coverage reports to Codecov
|
||||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # pin@v5.4.3
|
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # pin@v5.4.3
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
slug: inventree/InvenTree
|
slug: inventree/InvenTree
|
||||||
flags: web
|
flags: web
|
||||||
- name: Upload bundler info
|
- name: Upload bundler info
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 350
|
INVENTREE_API_VERSION = 351
|
||||||
|
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v351 -> 2025-06-18 : https://github.com/inventree/InvenTree/pull/9602
|
||||||
|
- Adds passwort reset API endpoint for admin users
|
||||||
|
|
||||||
v350 -> 2025-06-17 : https://github.com/inventree/InvenTree/pull/9798
|
v350 -> 2025-06-17 : https://github.com/inventree/InvenTree/pull/9798
|
||||||
- Adds "can_build" field to the part requirements API endpoint
|
- Adds "can_build" field to the part requirements API endpoint
|
||||||
- Remove "allocated" and "required" fields from the part requirements API endpoint
|
- Remove "allocated" and "required" fields from the part requirements API endpoint
|
||||||
|
@ -4,6 +4,8 @@ import datetime
|
|||||||
|
|
||||||
from django.contrib.auth import get_user, login
|
from django.contrib.auth import get_user, login
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
|
from django.contrib.auth.password_validation import password_changed, validate_password
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
from django.views.generic.base import RedirectView
|
from django.views.generic.base import RedirectView
|
||||||
@ -23,6 +25,7 @@ from InvenTree.mixins import (
|
|||||||
RetrieveAPI,
|
RetrieveAPI,
|
||||||
RetrieveUpdateAPI,
|
RetrieveUpdateAPI,
|
||||||
RetrieveUpdateDestroyAPI,
|
RetrieveUpdateDestroyAPI,
|
||||||
|
UpdateAPI,
|
||||||
)
|
)
|
||||||
from InvenTree.settings import FRONTEND_URL_BASE
|
from InvenTree.settings import FRONTEND_URL_BASE
|
||||||
from users.models import ApiToken, Owner, RuleSet, UserProfile
|
from users.models import ApiToken, Owner, RuleSet, UserProfile
|
||||||
@ -37,6 +40,7 @@ from users.serializers import (
|
|||||||
RuleSetSerializer,
|
RuleSetSerializer,
|
||||||
UserCreateSerializer,
|
UserCreateSerializer,
|
||||||
UserProfileSerializer,
|
UserProfileSerializer,
|
||||||
|
UserSetPasswordSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = structlog.get_logger('inventree')
|
logger = structlog.get_logger('inventree')
|
||||||
@ -141,6 +145,36 @@ class UserDetail(RetrieveUpdateDestroyAPI):
|
|||||||
permission_classes = [InvenTree.permissions.StaffRolePermissionOrReadOnly]
|
permission_classes = [InvenTree.permissions.StaffRolePermissionOrReadOnly]
|
||||||
|
|
||||||
|
|
||||||
|
class UserDetailSetPassword(UpdateAPI):
|
||||||
|
"""Allows superusers to set the password for a user."""
|
||||||
|
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer_class = UserSetPasswordSerializer
|
||||||
|
permission_classes = [InvenTree.permissions.IsSuperuserOrSuperScope]
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
"""Return the user object for this endpoint."""
|
||||||
|
return self.get_queryset().get(pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
"""Set the password for the user."""
|
||||||
|
user: User = serializer.instance
|
||||||
|
|
||||||
|
password: str = serializer.validated_data.get('password', None)
|
||||||
|
overwrite: bool = serializer.validated_data.get('override_warning', False)
|
||||||
|
|
||||||
|
if password:
|
||||||
|
if not overwrite:
|
||||||
|
try:
|
||||||
|
validate_password(password=password, user=user)
|
||||||
|
except ValidationError as e:
|
||||||
|
raise exceptions.ValidationError({'password': str(e)})
|
||||||
|
|
||||||
|
user.set_password(password)
|
||||||
|
password_changed(password=password, user=user)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
class MeUserDetail(RetrieveUpdateAPI, UserDetail):
|
class MeUserDetail(RetrieveUpdateAPI, UserDetail):
|
||||||
"""Detail endpoint for current user.
|
"""Detail endpoint for current user.
|
||||||
|
|
||||||
@ -467,6 +501,16 @@ user_urls = [
|
|||||||
path('', RuleSetList.as_view(), name='api-ruleset-list'),
|
path('', RuleSetList.as_view(), name='api-ruleset-list'),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
path('<int:pk>/', UserDetail.as_view(), name='api-user-detail'),
|
path(
|
||||||
|
'<int:pk>/',
|
||||||
|
include([
|
||||||
|
path(
|
||||||
|
'set-password/',
|
||||||
|
UserDetailSetPassword.as_view(),
|
||||||
|
name='api-user-set-password',
|
||||||
|
),
|
||||||
|
path('', UserDetail.as_view(), name='api-user-detail'),
|
||||||
|
]),
|
||||||
|
),
|
||||||
path('', UserList.as_view(), name='api-user-list'),
|
path('', UserList.as_view(), name='api-user-list'),
|
||||||
]
|
]
|
||||||
|
@ -360,6 +360,30 @@ class ExtendedUserSerializer(UserSerializer):
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class UserSetPasswordSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for setting a password for a user."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta options for UserSetPasswordSerializer."""
|
||||||
|
|
||||||
|
model = User
|
||||||
|
fields = ['password', 'override_warning']
|
||||||
|
|
||||||
|
password = serializers.CharField(
|
||||||
|
label=_('Password'),
|
||||||
|
help_text=_('Password for the user'),
|
||||||
|
write_only=True,
|
||||||
|
required=True,
|
||||||
|
style={'input_type': 'password'},
|
||||||
|
)
|
||||||
|
override_warning = serializers.BooleanField(
|
||||||
|
label=_('Override warning'),
|
||||||
|
help_text=_('Override the warning about password rules'),
|
||||||
|
write_only=True,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MeUserSerializer(ExtendedUserSerializer):
|
class MeUserSerializer(ExtendedUserSerializer):
|
||||||
"""API serializer specifically for the 'me' endpoint."""
|
"""API serializer specifically for the 'me' endpoint."""
|
||||||
|
|
||||||
|
@ -209,6 +209,32 @@ class UserAPITests(InvenTreeAPITestCase):
|
|||||||
self.assertEqual(len(data['permissions']), len(perms) + len(build_perms))
|
self.assertEqual(len(data['permissions']), len(perms) + len(build_perms))
|
||||||
|
|
||||||
|
|
||||||
|
class SuperuserAPITests(InvenTreeAPITestCase):
|
||||||
|
"""Tests for user API endpoints that require superuser rights."""
|
||||||
|
|
||||||
|
fixtures = ['users']
|
||||||
|
superuser = True
|
||||||
|
|
||||||
|
def test_user_password_set(self):
|
||||||
|
"""Test the set-password/ endpoint."""
|
||||||
|
user = User.objects.get(pk=2)
|
||||||
|
url = reverse('api-user-set-password', kwargs={'pk': user.pk})
|
||||||
|
|
||||||
|
# to simple password
|
||||||
|
resp = self.put(url, {'password': 1}, expected_code=400)
|
||||||
|
self.assertContains(resp, 'This password is too short', status_code=400)
|
||||||
|
|
||||||
|
# now with overwerite
|
||||||
|
resp = self.put(
|
||||||
|
url, {'password': 1, 'override_warning': True}, expected_code=200
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.data, {})
|
||||||
|
|
||||||
|
# complex enough pwd
|
||||||
|
resp = self.put(url, {'password': 'inventree'}, expected_code=200)
|
||||||
|
self.assertEqual(resp.data, {})
|
||||||
|
|
||||||
|
|
||||||
class UserTokenTests(InvenTreeAPITestCase):
|
class UserTokenTests(InvenTreeAPITestCase):
|
||||||
"""Tests for user token functionality."""
|
"""Tests for user token functionality."""
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ export enum ApiEndpoints {
|
|||||||
|
|
||||||
// User API endpoints
|
// User API endpoints
|
||||||
user_list = 'user/',
|
user_list = 'user/',
|
||||||
|
user_set_password = 'user/:id/set-password/',
|
||||||
user_me = 'user/me/',
|
user_me = 'user/me/',
|
||||||
user_profile = 'user/profile/',
|
user_profile = 'user/profile/',
|
||||||
user_roles = 'user/roles/',
|
user_roles = 'user/roles/',
|
||||||
|
@ -72,6 +72,7 @@ export type ApiFormFieldType = {
|
|||||||
| 'email'
|
| 'email'
|
||||||
| 'url'
|
| 'url'
|
||||||
| 'string'
|
| 'string'
|
||||||
|
| 'password'
|
||||||
| 'icon'
|
| 'icon'
|
||||||
| 'boolean'
|
| 'boolean'
|
||||||
| 'date'
|
| 'date'
|
||||||
|
@ -112,7 +112,7 @@
|
|||||||
"@lingui/babel-plugin-lingui-macro": "^5.3.0",
|
"@lingui/babel-plugin-lingui-macro": "^5.3.0",
|
||||||
"@lingui/cli": "^5.3.1",
|
"@lingui/cli": "^5.3.1",
|
||||||
"@lingui/macro": "^5.3.0",
|
"@lingui/macro": "^5.3.0",
|
||||||
"@playwright/test": "^1.49.1",
|
"@playwright/test": "^1.52.0",
|
||||||
"@types/node": "^22.13.14",
|
"@types/node": "^22.13.14",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.3.8",
|
"@types/react": "^18.3.8",
|
||||||
|
@ -152,6 +152,18 @@ export function ApiFormField({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'password':
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
definition={{ ...reducedDefinition, type: 'password' }}
|
||||||
|
controller={controller}
|
||||||
|
fieldName={fieldName}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={(value) => {
|
||||||
|
onKeyDown?.(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
<IconField definition={fieldDefinition} controller={controller} />
|
<IconField definition={fieldDefinition} controller={controller} />
|
||||||
|
@ -7,6 +7,8 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
|||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
|
import { getDetailUrl } from '@lib/index';
|
||||||
|
import { IconUsersGroup } from '@tabler/icons-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||||
import { EditApiForm } from '../../components/forms/ApiForm';
|
import { EditApiForm } from '../../components/forms/ApiForm';
|
||||||
@ -159,7 +161,14 @@ export function GroupTable({
|
|||||||
setSelectedGroup(record.pk);
|
setSelectedGroup(record.pk);
|
||||||
deleteGroup.open();
|
deleteGroup.open();
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
{
|
||||||
|
icon: <IconUsersGroup />,
|
||||||
|
title: t`Open Profile`,
|
||||||
|
onClick: () => {
|
||||||
|
navigate(getDetailUrl(ModelType.group, record.pk));
|
||||||
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
[user]
|
[user]
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { Accordion, Alert, LoadingOverlay, Stack, Text } from '@mantine/core';
|
import { Accordion, Alert, LoadingOverlay, Stack, Text } from '@mantine/core';
|
||||||
import { IconInfoCircle } from '@tabler/icons-react';
|
import {
|
||||||
|
IconInfoCircle,
|
||||||
|
IconKey,
|
||||||
|
IconLock,
|
||||||
|
IconLockOpen,
|
||||||
|
IconUserCircle
|
||||||
|
} from '@tabler/icons-react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
|
import { getDetailUrl } from '@lib/index';
|
||||||
import type { TableFilter } from '@lib/types/Filters';
|
import type { TableFilter } from '@lib/types/Filters';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -23,6 +30,7 @@ import {
|
|||||||
import { DetailDrawer } from '../../components/nav/DetailDrawer';
|
import { DetailDrawer } from '../../components/nav/DetailDrawer';
|
||||||
import { showApiErrorMessage } from '../../functions/notifications';
|
import { showApiErrorMessage } from '../../functions/notifications';
|
||||||
import {
|
import {
|
||||||
|
useApiFormModal,
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
useDeleteApiFormModal
|
useDeleteApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
@ -298,7 +306,44 @@ export function UserTable({
|
|||||||
setSelectedUser(record.pk);
|
setSelectedUser(record.pk);
|
||||||
deleteUser.open();
|
deleteUser.open();
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
{
|
||||||
|
icon: <IconUserCircle />,
|
||||||
|
title: t`Open Profile`,
|
||||||
|
onClick: () => {
|
||||||
|
navigate(getDetailUrl(ModelType.user, record.pk));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconKey />,
|
||||||
|
title: t`Change Password`,
|
||||||
|
color: 'blue',
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedUser(record.pk);
|
||||||
|
setPassword.open();
|
||||||
|
},
|
||||||
|
hidden: !user.isSuperuser()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconLock />,
|
||||||
|
title: t`Lock user`,
|
||||||
|
color: 'blue',
|
||||||
|
onClick: () => {
|
||||||
|
setUserActiveState(record.pk, false);
|
||||||
|
table.refreshTable();
|
||||||
|
},
|
||||||
|
hidden: !record.is_active
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconLockOpen />,
|
||||||
|
title: t`Unlock user`,
|
||||||
|
color: 'blue',
|
||||||
|
onClick: () => {
|
||||||
|
setUserActiveState(record.pk, true);
|
||||||
|
table.refreshTable();
|
||||||
|
},
|
||||||
|
hidden: record.is_active
|
||||||
|
}
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
[user]
|
[user]
|
||||||
@ -327,6 +372,18 @@ export function UserTable({
|
|||||||
successMessage: t`Added user`
|
successMessage: t`Added user`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setPassword = useApiFormModal({
|
||||||
|
url: ApiEndpoints.user_set_password,
|
||||||
|
method: 'PUT',
|
||||||
|
pk: selectedUser,
|
||||||
|
title: t`Set Password`,
|
||||||
|
fields: {
|
||||||
|
password: { field_type: 'password' },
|
||||||
|
override_warning: {}
|
||||||
|
},
|
||||||
|
successMessage: t`Password updated`
|
||||||
|
});
|
||||||
|
|
||||||
const tableActions = useMemo(() => {
|
const tableActions = useMemo(() => {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
const staff: boolean = user.isStaff() || user.isSuperuser();
|
const staff: boolean = user.isStaff() || user.isSuperuser();
|
||||||
@ -371,6 +428,7 @@ export function UserTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{editable && setPassword.modal}
|
||||||
{editable && newUser.modal}
|
{editable && newUser.modal}
|
||||||
{editable && deleteUser.modal}
|
{editable && deleteUser.modal}
|
||||||
{editable && (
|
{editable && (
|
||||||
@ -405,3 +463,21 @@ export function UserTable({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setUserActiveState(userId: number, active: boolean) {
|
||||||
|
try {
|
||||||
|
await api.patch(apiUrl(ApiEndpoints.user_list, userId), {
|
||||||
|
is_active: active
|
||||||
|
});
|
||||||
|
showNotification({
|
||||||
|
title: t`User updated`,
|
||||||
|
message: t`User updated successfully`,
|
||||||
|
color: 'green'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showApiErrorMessage({
|
||||||
|
error: error,
|
||||||
|
title: t`Error updating user`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -23,10 +23,34 @@ test('Permissions - Admin', async ({ browser, request }) => {
|
|||||||
await loadTab(page, 'Plugins');
|
await loadTab(page, 'Plugins');
|
||||||
await loadTab(page, 'Users / Access');
|
await loadTab(page, 'Users / Access');
|
||||||
|
|
||||||
// Let's create a new user
|
// Let's check creating a new user
|
||||||
await page.getByLabel('action-button-add-user').click();
|
await page.getByLabel('action-button-add-user').click();
|
||||||
await page.getByRole('button', { name: 'Submit' }).waitFor();
|
await page.getByRole('button', { name: 'Submit' }).waitFor();
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
|
||||||
|
// Change password
|
||||||
|
await page.getByRole('cell', { name: 'Ian', exact: true }).click({
|
||||||
|
button: 'right'
|
||||||
|
});
|
||||||
|
await page.getByRole('button', { name: 'Change Password' }).click();
|
||||||
|
await page.getByLabel('text-field-password').fill('123');
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByText("['This password is too short").waitFor();
|
||||||
|
await page
|
||||||
|
.locator('label')
|
||||||
|
.filter({ hasText: 'Override warning' })
|
||||||
|
.locator('div')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByText('Password updated').click();
|
||||||
|
|
||||||
|
// Open profile
|
||||||
|
await page.getByRole('cell', { name: 'Ian', exact: true }).click({
|
||||||
|
button: 'right'
|
||||||
|
});
|
||||||
|
await page.getByRole('button', { name: 'Open Profile' }).click();
|
||||||
|
await page.getByText('User: ian', { exact: true }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1311,12 +1311,12 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@octokit/openapi-types" "^22.2.0"
|
"@octokit/openapi-types" "^22.2.0"
|
||||||
|
|
||||||
"@playwright/test@^1.49.1":
|
"@playwright/test@^1.52.0":
|
||||||
version "1.49.1"
|
version "1.52.0"
|
||||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.49.1.tgz#55fa360658b3187bfb6371e2f8a64f50ef80c827"
|
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.52.0.tgz#267ec595b43a8f4fa5e444ea503689629e91a5b8"
|
||||||
integrity sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==
|
integrity sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==
|
||||||
dependencies:
|
dependencies:
|
||||||
playwright "1.49.1"
|
playwright "1.52.0"
|
||||||
|
|
||||||
"@remix-run/router@1.19.2":
|
"@remix-run/router@1.19.2":
|
||||||
version "1.19.2"
|
version "1.19.2"
|
||||||
@ -3955,17 +3955,17 @@ pkg-up@^3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up "^3.0.0"
|
find-up "^3.0.0"
|
||||||
|
|
||||||
playwright-core@1.49.1:
|
playwright-core@1.52.0:
|
||||||
version "1.49.1"
|
version "1.52.0"
|
||||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.49.1.tgz#32c62f046e950f586ff9e35ed490a424f2248015"
|
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.52.0.tgz#238f1f0c3edd4ebba0434ce3f4401900319a3dca"
|
||||||
integrity sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==
|
integrity sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==
|
||||||
|
|
||||||
playwright@1.49.1:
|
playwright@1.52.0:
|
||||||
version "1.49.1"
|
version "1.52.0"
|
||||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.49.1.tgz#830266dbca3008022afa7b4783565db9944ded7c"
|
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.52.0.tgz#26cb9a63346651e1c54c8805acfd85683173d4bd"
|
||||||
integrity sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==
|
integrity sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==
|
||||||
dependencies:
|
dependencies:
|
||||||
playwright-core "1.49.1"
|
playwright-core "1.52.0"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "2.3.2"
|
fsevents "2.3.2"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user