diff --git a/apps/web/src/__tests__/demo-banner.test.tsx b/apps/web/src/__tests__/demo-banner.test.tsx
index 5bbf34e6d..acbb035de 100644
--- a/apps/web/src/__tests__/demo-banner.test.tsx
+++ b/apps/web/src/__tests__/demo-banner.test.tsx
@@ -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();
+ 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();
screen.getByRole('dialog');
@@ -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();
+ 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();
+ const { password, username } = DEMO_USERS[0]!;
+ fireEvent.click(within(screen.getByRole('row', { name: new RegExp(username) })).getByRole('button'));
+ expect(onLogin).toHaveBeenCalledExactlyOnceWith({ password, username });
+ });
});
diff --git a/apps/web/src/components/DemoBanner/DemoBanner.tsx b/apps/web/src/components/DemoBanner/DemoBanner.tsx
index 4228fdc38..f32ad6346 100644
--- a/apps/web/src/components/DemoBanner/DemoBanner.tsx
+++ b/apps/web/src/components/DemoBanner/DemoBanner.tsx
@@ -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';
@@ -38,6 +39,12 @@ export const DemoBanner = ({ onLogin }: DemoBannerProps) => {
event.preventDefault();
}}
>
+
+
+
+ Open Data Capture
+
+
{t('auth.demo.info')}
@@ -47,7 +54,8 @@ export const DemoBanner = ({ onLogin }: DemoBannerProps) => {
-
+ {/* Only a body row is clickable, so cancel the hover highlight `Table.Row` applies by default. */}
+
{t('auth.demo.username')}
@@ -62,7 +70,11 @@ export const DemoBanner = ({ onLogin }: DemoBannerProps) => {
{DEMO_USERS.map((user) => (
-
+ onLogin({ password: user.password, username: user.username })}
+ >
{user.username}
{user.groupNames.map((name) => (
@@ -75,14 +87,10 @@ export const DemoBanner = ({ onLogin }: DemoBannerProps) => {
{t(`common.${snakeToCamelCase(user.basePermissionLevel!)}`)}
+ {/* The trigger carries no handler: its click bubbles to the row, so pointer and
+ keyboard reach `onLogin` by the one path. */}
- onLogin({ password: user.password, username: user.username })}
- >
+
diff --git a/testing/src/pages/auth/login.page.ts b/testing/src/pages/auth/login.page.ts
index 652e5a534..68733dc22 100644
--- a/testing/src/pages/auth/login.page.ts
+++ b/testing/src/pages/auth/login.page.ts
@@ -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');
}
diff --git a/testing/src/specs/auth.spec.ts b/testing/src/specs/auth.spec.ts
index 7c74aeb40..f9ef1aacd 100644
--- a/testing/src/specs/auth.spec.ts
+++ b/testing/src/specs/auth.spec.ts
@@ -1,3 +1,5 @@
+import type { Locator } from '@playwright/test';
+
import { expect, test } from '../support/fixtures';
test.describe('authentication', () => {
@@ -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();
@@ -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', () => {