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
25 changes: 25 additions & 0 deletions apps/web/src/__tests__/demo-banner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ describe('DemoBanner', () => {
expect(screen.getByRole('dialog', { name: 'Demo Information' })).toBeTruthy();
});

it('should brand the demo information with the Open Data Capture logo and name, as on the public site', () => {
render(<DemoBanner onLogin={vi.fn()} />);
const branding = within(screen.getByRole('dialog')).getByTestId('demo-dialog-branding');
expect(branding.querySelector('svg')).toBeTruthy();
expect(branding.textContent).toBe('Open Data Capture');
});

it('should close the demo information when dismissed, so the login form is reachable', () => {
render(<DemoBanner onLogin={vi.fn()} />);
screen.getByRole('dialog');
Expand All @@ -43,4 +50,22 @@ describe('DemoBanner', () => {
}
expect(onLogin.mock.calls).toEqual(DEMO_USERS.map(({ password, username }) => [{ password, username }]));
});

it('should log in from a click anywhere in the row, so the whole row is the target and not just its button', () => {
const onLogin = vi.fn();
render(<DemoBanner onLogin={onLogin} />);
for (const { username } of DEMO_USERS) {
const cells = within(screen.getByRole('row', { name: new RegExp(username) })).getAllByRole('cell');
fireEvent.click(cells[0]!);
}
expect(onLogin.mock.calls).toEqual(DEMO_USERS.map(({ password, username }) => [{ password, username }]));
});

it('should log in once for a click on the row button, so the row and the button do not both fire', () => {
const onLogin = vi.fn();
render(<DemoBanner onLogin={onLogin} />);
const { password, username } = DEMO_USERS[0]!;
fireEvent.click(within(screen.getByRole('row', { name: new RegExp(username) })).getByRole('button'));
expect(onLogin).toHaveBeenCalledExactlyOnceWith({ password, username });
});
});
26 changes: 17 additions & 9 deletions apps/web/src/components/DemoBanner/DemoBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { snakeToCamelCase } from '@douglasneuroinformatics/libjs';
import { Card, Dialog, Table, Tooltip } from '@douglasneuroinformatics/libui/components';
import { useTranslation } from '@douglasneuroinformatics/libui/hooks';
import { DEMO_USERS } from '@opendatacapture/demo';
import { Logo } from '@opendatacapture/react-core';
import type { $LoginCredentials } from '@opendatacapture/schemas/auth';
import { InfoIcon, LogInIcon } from 'lucide-react';

Expand Down Expand Up @@ -38,6 +39,12 @@ export const DemoBanner = ({ onLogin }: DemoBannerProps) => {
event.preventDefault();
}}
>
<div className="mb-4 flex h-12 items-center px-1" data-testid="demo-dialog-branding">
<Logo className="h-full w-auto" variant="auto" />
<span className="ml-3 whitespace-nowrap text-xl font-semibold tracking-tight text-slate-700 dark:text-slate-300">
Open Data Capture
</span>
</div>
<Dialog.Header className="w-full whitespace-break-spaces px-1">
<Dialog.Title className="mb-2">{t('auth.demo.info')}</Dialog.Title>
<Dialog.Description className="text-pretty text-left text-xs sm:text-sm">
Expand All @@ -47,7 +54,8 @@ export const DemoBanner = ({ onLogin }: DemoBannerProps) => {
<Card className="text-muted-foreground w-full overflow-hidden rounded-md text-xs tracking-tighter sm:tracking-tight">
<Table className="overflow-x-scroll">
<Table.Header>
<Table.Row>
{/* Only a body row is clickable, so cancel the hover highlight `Table.Row` applies by default. */}
<Table.Row className="hover:bg-transparent">
<Table.Head className="text-foreground p-3 px-2.5 text-xs sm:px-3.5 sm:text-sm">
{t('auth.demo.username')}
</Table.Head>
Expand All @@ -62,7 +70,11 @@ export const DemoBanner = ({ onLogin }: DemoBannerProps) => {
</Table.Header>
<Table.Body>
{DEMO_USERS.map((user) => (
<Table.Row key={user.username}>
<Table.Row
className="cursor-pointer"
key={user.username}
onClick={() => onLogin({ password: user.password, username: user.username })}
>
<Table.Cell className="p-3 px-2.5 text-xs sm:px-3.5 sm:text-sm">{user.username}</Table.Cell>
<Table.Cell className="p-3 px-2.5 text-xs sm:px-3.5 sm:text-sm">
{user.groupNames.map((name) => (
Expand All @@ -75,14 +87,10 @@ export const DemoBanner = ({ onLogin }: DemoBannerProps) => {
{t(`common.${snakeToCamelCase(user.basePermissionLevel!)}`)}
</Table.Cell>
<Table.Cell className="p-3 px-2.5 sm:px-3.5">
{/* The trigger carries no handler: its click bubbles to the row, so pointer and
keyboard reach `onLogin` by the one path. */}
<Tooltip delayDuration={500}>
<Tooltip.Trigger
className="h-9 w-9"
size="icon"
type="button"
variant="ghost"
onClick={() => onLogin({ password: user.password, username: user.username })}
>
<Tooltip.Trigger className="h-9 w-9" size="icon" type="button" variant="ghost">
<LogInIcon />
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
Expand Down
9 changes: 8 additions & 1 deletion testing/src/pages/auth/login.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ import { RootPage } from '../__root.page';
export class LoginPage extends RootPage {
readonly _requiresAuth = false;
readonly demoDialog: Locator;
/** One row per demo account, each with a button that logs straight in as it. */
readonly demoDialogBranding: Locator;
readonly demoTableHeaderRow: Locator;
/**
* One row per demo account. A click anywhere in the row logs straight in as it; the row itself is
* not focusable, so the button inside it is the keyboard route to the same action.
*/
readonly demoUserRows: Locator;
readonly loginForm: Locator;

constructor(page: Page) {
super(page);
this.demoDialog = page.getByTestId('demo-dialog');
this.demoDialogBranding = this.demoDialog.getByTestId('demo-dialog-branding');
this.demoTableHeaderRow = this.demoDialog.getByRole('row').filter({ has: page.getByRole('columnheader') });
this.demoUserRows = this.demoDialog.getByRole('row').filter({ has: page.getByRole('cell') });
this.loginForm = page.getByTestId('login-form');
}
Expand Down
33 changes: 33 additions & 0 deletions testing/src/specs/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Locator } from '@playwright/test';

import { expect, test } from '../support/fixtures';

test.describe('authentication', () => {
Expand All @@ -21,6 +23,12 @@ test.describe('authentication', () => {
await expect(loginPage.demoDialog).toBeVisible();
});

test('should brand the demo information with the Open Data Capture logo and name', async ({ getPageModel }) => {
const loginPage = await getPageModel('/auth/login');
await expect(loginPage.demoDialogBranding.locator('svg')).toBeVisible();
await expect(loginPage.demoDialogBranding).toHaveText('Open Data Capture');
});

test('should reveal the login form once the demo information is dismissed', async ({ getPageModel }) => {
const loginPage = await getPageModel('/auth/login');
await expect(loginPage.demoDialog).toBeVisible();
Expand All @@ -45,6 +53,31 @@ test.describe('authentication', () => {
await loginPage.expect.toHaveURL('/dashboard');
await expect(page.getByTestId('user-dropup-trigger')).toHaveText(username);
});

test('should log in from a click on the row itself, not only on its button', async ({ getPageModel, page }) => {
const loginPage = await getPageModel('/auth/login');
const demoUserRow = loginPage.demoUserRows.filter({ hasText: 'Group Manager' }).last();
const usernameCell = demoUserRow.getByRole('cell').first();
const username = await usernameCell.innerText();
await usernameCell.click();

await loginPage.expect.toHaveURL('/dashboard');
await expect(page.getByTestId('user-dropup-trigger')).toHaveText(username);
});

test('should highlight only the rows that log in, so the header does not look clickable', async ({
getPageModel
}) => {
const loginPage = await getPageModel('/auth/login');
const backgroundColor = (row: Locator) => row.evaluate((element) => getComputedStyle(element).backgroundColor);
const restingColor = await backgroundColor(loginPage.demoTableHeaderRow);

await loginPage.demoTableHeaderRow.hover();
expect(await backgroundColor(loginPage.demoTableHeaderRow)).toBe(restingColor);

await loginPage.demoUserRows.first().hover();
expect(await backgroundColor(loginPage.demoUserRows.first())).not.toBe(restingColor);
});
});

test.describe('invalid credentials', () => {
Expand Down
Loading