Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 25 additions & 19 deletions e2e/specs/admin/requests_operations_smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,18 @@ test('@workflow-matrix @smoke admin requests: one list row opens the canonical d
});

await page.goto('/admin/requests');
await expect(page.getByTestId('admin.requests.table')).toBeVisible();
const desktopRow = page.getByTestId('admin.requests.row.registration.123');
await expect(desktopRow).not.toHaveAttribute('tabindex');
await expect(desktopRow.getByRole('link', { name: '#123' })).toBeVisible();
const mobile = page.viewportSize()!.width < 768;
const requestLink = mobile
? page.getByTestId('admin.requests.mobile.row.registration.123').getByRole('link')
: page.getByTestId('admin.requests.row.registration.123').getByRole('link', { name: '#123' });
await expect(requestLink).toBeVisible();
if (!mobile) await expect(page.getByTestId('admin.requests.row.registration.123')).not.toHaveAttribute('tabindex');

await expect.poll(() => states.includes('awaiting')).toBeTruthy();
expect(new URL(page.url()).searchParams.get('state')).toBeNull();
await expect(page.getByTestId('admin.requests.quick.awaiting')).toHaveAttribute('aria-pressed', 'true');

await desktopRow.getByRole('link', { name: '#123' }).press('Enter');
await requestLink.press('Enter');
await expect(page).toHaveURL(/\/admin\/requests\/registration\/123\?returnTo=/);
const returnTo = new URL(page.url()).searchParams.get('returnTo');
expect(new URL(returnTo ?? '/', 'https://example.test').pathname).toBe('/admin/requests');
Expand Down Expand Up @@ -234,26 +236,28 @@ for (const language of ['en', 'cs'] as const) {

await page.goto('/admin/requests/registration/124');

await expect(page.getByTestId('admin.requests.detail.metadata').locator('details')).toHaveAttribute('open', '');
const review = page.getByTestId('admin.requests.detail.review');
const summary = review.getByTestId('admin.requests.detail.risk.summary');
await expect(summary).toBeInViewport();
const summary = page.getByTestId('admin.requests.detail.risk.summary');
await expect(summary).toBeVisible();
const summaryBox = await summary.boundingBox();
const decisionBox = await review.getByTestId('admin.requests.detail.decision').boundingBox();
expect(summaryBox!.y + summaryBox!.height).toBeLessThan(decisionBox!.y);
await expect(review.getByTestId('admin.requests.detail.risk.ip')).toBeVisible();
await expect(review.getByTestId('admin.requests.detail.risk.mail')).toBeVisible();
if (page.viewportSize()!.width >= 1024) {
const detailsBox = await page.getByTestId('admin.requests.detail.registration.fields').boundingBox();
const reviewBox = await review.boundingBox();
expect(reviewBox!.x).toBeGreaterThan(detailsBox!.x + detailsBox!.width);
const detailsBox = await page.getByTestId('admin.requests.detail.registration.fields').boundingBox();
expect(summaryBox!.y).toBeGreaterThan(detailsBox!.y + detailsBox!.height);
await expect(review.getByTestId('admin.requests.detail.risk.ip')).toHaveCount(0);
await expect(review.getByTestId('admin.requests.detail.risk.mail')).toHaveCount(0);
for (const kind of ['ip', 'mail']) {
const check = page.getByTestId(`admin.requests.detail.risk.${kind}`);
await expect(check).toBeVisible();
const box = await check.boundingBox();
expect(box!.y).toBeGreaterThan(summaryBox!.y + summaryBox!.height);
}
for (const kind of ['ip', 'mail']) {
await review.getByTestId(`admin.requests.detail.risk.${kind}.details`).locator('summary').click();
await page.getByTestId(`admin.requests.detail.risk.${kind}.details`).locator('summary').click();
}
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(page.viewportSize()!.width);

await page.evaluate(() => window.scrollTo(0, 0));
await page.screenshot({ path: testInfo.outputPath('request-risk-sidebar.png'), fullPage: true });
await page.screenshot({ path: testInfo.outputPath('request-risk-below-details.png'), fullPage: true });
const copyAddress = page.getByTestId('admin.requests.detail.registration.address.map.copy');
await expect(copyAddress).toBeVisible();
const addressBox = await page.getByTestId('admin.requests.detail.registration.address.map')
Expand Down Expand Up @@ -335,10 +339,12 @@ test('@workflow-matrix @pr-smoke @pr-smoke-mobile @smoke admin requests: success
await expect.poll(osm.requestCount).toBe(1);
const metadataChevron = page.getByTestId('admin.requests.detail.metadata.chevron');
await expect(metadataChevron).toBeVisible();
await expect(metadataChevron).toHaveCSS('rotate', 'none');
await page.getByTestId('admin.requests.detail.metadata.toggle').click();
await expect(page.getByTestId('admin.requests.detail.metadata').locator('details')).toHaveAttribute('open', '');
await expect(metadataChevron).toHaveCSS('rotate', '90deg');
await page.getByTestId('admin.requests.detail.metadata.toggle').click();
await expect(page.getByTestId('admin.requests.detail.metadata').locator('details')).not.toHaveAttribute('open');
await page.getByTestId('admin.requests.detail.metadata.toggle').click();
await expect(metadataChevron).toHaveCSS('rotate', '90deg');
await page.getByTestId('admin.requests.resolve.action.approve').click();
await expect(page.getByTestId('admin.requests.resolve.modal')).toContainText(/approve request|schválit žádost/i);
await expect(page.getByTestId('admin.requests.resolve.action_select')).toHaveCount(0);
Expand Down
8 changes: 4 additions & 4 deletions src/pages/app/admin/RequestDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
requestResourceLabel,
safeRequestsReturnTo,
} from './RequestDetailModel';
import { RequestFraudChecks, RequestFraudSummary } from './RequestFraudChecks';
import { RequestFraudChecks } from './RequestFraudChecks';
import {
RequestOperationalLinks,
RequestReviewActions,
Expand Down Expand Up @@ -384,7 +384,6 @@ export function RequestDetailPage() {

<div className="grid grid-cols-1 gap-3 lg:grid-cols-3">
<aside className="min-w-0 space-y-3 lg:col-start-3 lg:row-start-1" data-testid="admin.requests.detail.review">
{reqType === 'registration' ? <RequestFraudSummary request={request as RegistrationRequest} /> : null}
<Card testId="admin.requests.detail.decision">
<CardHeader title={t('requests.detail.decision.title')} subtitle={t('requests.detail.decision.subtitle')} />
<CardBody className="space-y-4">
Expand Down Expand Up @@ -426,7 +425,6 @@ export function RequestDetailPage() {
/>
</CardBody>
</Card>
{reqType === 'registration' ? <RequestFraudChecks request={request as RegistrationRequest} /> : null}
</aside>

<section className="space-y-3 lg:col-span-2 lg:col-start-1 lg:row-start-1">
Expand All @@ -450,8 +448,10 @@ export function RequestDetailPage() {
</CardBody>
</Card>

{reqType === 'registration' ? <RequestFraudChecks request={request as RegistrationRequest} /> : null}

<Card testId="admin.requests.detail.metadata">
<details className="group">
<details key={`${reqType}:${reqId}`} className="group" open>
<summary
className="flex cursor-pointer list-none items-center gap-2 p-4 font-semibold"
data-testid="admin.requests.detail.metadata.toggle"
Expand Down
63 changes: 28 additions & 35 deletions src/pages/app/admin/RequestFraudChecks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Clock3, ShieldAlert, ShieldCheck } from 'lucide-react';
import { useI18n } from '../../../app/i18n';
import type { RegistrationRequest } from '../../../lib/api/requests';
import { Badge } from '../../../components/ui/Badge';
import { Card, CardBody } from '../../../components/ui/Card';
import { Card, CardBody, CardHeader } from '../../../components/ui/Card';
import { clsx } from '../../../components/ui/clsx';

import { fraudCheckStatus, type FraudCheckStatus } from './RequestDetailModel';
Expand Down Expand Up @@ -137,24 +137,26 @@ function CheckCard(props: {
? 'border-warn-border'
: 'border-ok-border')}
>
<div className="space-y-2 border-b border-border p-4">
<h3 className="font-semibold">{props.title}</h3>
<div className="flex flex-wrap items-center gap-2">
<Badge
variant={resultVariant}
className="text-sm font-semibold"
testId={`admin.requests.detail.risk.${props.kind}.score`}
>
{t('requests.detail.risk.score', { score: props.score ?? '—' })}
</Badge>
<Badge
variant={statusVariant(props.status)}
testId={`admin.requests.detail.risk.${props.kind}.status`}
>
{t(`requests.detail.risk.status.${props.status}`)}
</Badge>
</div>
</div>
<CardHeader
title={props.title}
actions={(
<>
<Badge
variant={resultVariant}
className="text-sm font-semibold"
testId={`admin.requests.detail.risk.${props.kind}.score`}
>
{t('requests.detail.risk.score', { score: props.score ?? '—' })}
</Badge>
<Badge
variant={statusVariant(props.status)}
testId={`admin.requests.detail.risk.${props.kind}.status`}
>
{t(`requests.detail.risk.status.${props.status}`)}
</Badge>
</>
)}
/>
<CardBody className="space-y-3">
{props.status === 'pending' ? (
<div className="text-sm text-muted">{t('requests.detail.risk.pending_help')}</div>
Expand All @@ -167,9 +169,9 @@ function CheckCard(props: {

<dl className="grid grid-cols-2 gap-x-3 gap-y-2 text-sm">
{props.summarySignals.map((signal) => (
<div key={signal.key} className="min-w-0">
<div key={signal.key}>
<dt className="text-xs text-muted">{t(signal.labelKey)}</dt>
<dd className="break-words">{signalValue(props.request[signal.key], t('common.yes'), t('common.no'))}</dd>
<dd>{signalValue(props.request[signal.key], t('common.yes'), t('common.no'))}</dd>
</div>
))}
</dl>
Expand All @@ -191,8 +193,10 @@ function CheckCard(props: {
</Card>
);
}
export function RequestFraudSummary(props: { request: RegistrationRequest }) {
export function RequestFraudChecks(props: { request: RegistrationRequest }) {
const { t } = useI18n();
const ipStatus = fraudCheckStatus(props.request.ip_checked, props.request.ip_success);
const mailStatus = fraudCheckStatus(props.request.mail_checked, props.request.mail_success);
const summary = requestFraudRiskSummary(props.request);
const SummaryIcon = summary.state === 'clear'
? ShieldCheck
Expand All @@ -208,7 +212,7 @@ export function RequestFraudSummary(props: { request: RegistrationRequest }) {
</div>
<div
className={clsx(
'flex flex-col items-start gap-3 rounded-lg border-2 px-4 py-3',
'flex flex-col gap-3 rounded-lg border-2 px-4 py-3 sm:flex-row sm:items-center',
emphasisClasses(summary.variant),
)}
data-testid="admin.requests.detail.risk.summary"
Expand Down Expand Up @@ -236,18 +240,7 @@ export function RequestFraudSummary(props: { request: RegistrationRequest }) {
</Badge>
) : null}
</div>
</section>
);
}

export function RequestFraudChecks(props: { request: RegistrationRequest }) {
const { t } = useI18n();
const ipStatus = fraudCheckStatus(props.request.ip_checked, props.request.ip_success);
const mailStatus = fraudCheckStatus(props.request.mail_checked, props.request.mail_success);

return (
<section className="space-y-3" aria-label={t('requests.detail.risk.title')}>
<div className="grid grid-cols-1 gap-3">
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
<CheckCard
request={props.request}
kind="ip"
Expand Down
Loading