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', () => {