Rework app bar, notes layout, Settings/Admin/login restyle
Build and Push Docker Image / build-and-push (push) Failing after 20s

App bar: brand, Studies / Read Bible / Reading Plans, and one account menu
(theme, Settings, Admin, Log out). Study stage: notes get their own box above
the resources in the right column. Settings, Admin and the login page use the
reader themes; add a theme-aware danger colour.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-09-29 10:27:02 -04:00
1 parent 31abee0695
commit ecb1c701fa
22 files changed
+351 -289

No files matched your search

+74 -5
View File
@@ -76,6 +76,12 @@ async function renderApp() {
await waitFor(() => expect(screen.queryByText('Loading…')).not.toBeInTheDocument());
}
// The account menu (avatar button in the app bar) holds the email, Settings, Admin and Log out.
async function openAccountMenu(user = userEvent.setup()) {
await user.click(await screen.findByRole('button', { name: /account menu/i }));
return user;
}
async function loadChapter() {
const user = userEvent.setup();
await renderApp();
@@ -412,6 +418,7 @@ describe('Authentication', () => {
const u = userEvent.setup();
render(<App />);
await signInWithForm(u);
await openAccountMenu(u);
expect(await screen.findByText(user.email)).toBeInTheDocument();
expect(calledWith(fetchMock, 'POST', '/api/auth/login')).toBe(true);
await waitFor(() => expect(calledWith(fetchMock, 'GET', '/api/reader/ink')).toBe(true));
@@ -424,6 +431,7 @@ describe('Authentication', () => {
await signInWithForm(u);
await u.type(await screen.findByPlaceholderText('123456'), '123456');
await u.click(screen.getByRole('button', { name: /verify/i }));
await openAccountMenu(u);
expect(await screen.findByText(user.email)).toBeInTheDocument();
expect(calledWith(fetchMock, 'POST', '/api/auth/mfa/verify')).toBe(true);
});
@@ -432,8 +440,9 @@ describe('Authentication', () => {
const fetchMock = stubApi({ session: user });
const u = userEvent.setup();
render(<App />);
await openAccountMenu(u);
expect(await screen.findByText(user.email)).toBeInTheDocument();
await u.click(screen.getByRole('button', { name: /log out/i }));
await u.click(screen.getByRole('menuitem', { name: /log out/i }));
expect(await screen.findByRole('button', { name: /^sign in$/i })).toBeInTheDocument();
expect(calledWith(fetchMock, 'POST', '/api/auth/logout')).toBe(true);
});
@@ -442,7 +451,8 @@ describe('Authentication', () => {
stubApi({ session: user });
const u = userEvent.setup();
render(<App />);
await u.click(await screen.findByRole('button', { name: /settings/i }));
await openAccountMenu(u);
await u.click(await screen.findByRole('menuitem', { name: /settings/i }));
expect(await screen.findByDisplayValue('My Show')).toBeInTheDocument();
});
});
@@ -479,7 +489,8 @@ describe('Admin page', () => {
stubAdminApi(admin);
const u = userEvent.setup();
render(<App />);
await u.click(await screen.findByRole('button', { name: /admin/i }));
await openAccountMenu(u);
await u.click(await screen.findByRole('menuitem', { name: /admin/i }));
expect(await screen.findByText('student@example.com')).toBeInTheDocument();
await u.click(screen.getByRole('button', { name: /projects \(1\)/i }));
expect(await screen.findByText('Romans Study')).toBeInTheDocument();
@@ -490,7 +501,8 @@ describe('Admin page', () => {
window.confirm.mockReturnValue(true);
const u = userEvent.setup();
render(<App />);
await u.click(await screen.findByRole('button', { name: /admin/i }));
await openAccountMenu(u);
await u.click(await screen.findByRole('menuitem', { name: /admin/i }));
const row = (await screen.findByText('student@example.com')).closest('tr');
await u.click(within(row).getByRole('button', { name: /delete/i }));
await waitFor(() => expect(screen.queryByText('student@example.com')).not.toBeInTheDocument());
@@ -500,8 +512,10 @@ describe('Admin page', () => {
test('non-admins get no Admin button', async () => {
stubAdminApi({ ...admin, isAdmin: false });
render(<App />);
await openAccountMenu();
expect(await screen.findByText(admin.email)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /admin/i })).not.toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /settings/i })).toBeInTheDocument();
expect(screen.queryByRole('menuitem', { name: /admin/i })).not.toBeInTheDocument();
});
});
@@ -881,3 +895,58 @@ describe('Study workspace', () => {
expect(screen.getByText(/\d+ words · about/)).toBeInTheDocument();
});
});
// ---------------------------------------------------------------------------
// App bar and study layout
// ---------------------------------------------------------------------------
describe('App bar', () => {
test('shows the three destinations, and the account menu switches the theme', async () => {
const user = userEvent.setup();
await renderApp();
expect(screen.getByRole('button', { name: /^studies$/i })).toHaveAttribute('aria-current', 'page');
expect(screen.getByRole('button', { name: /read bible/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /reading plans/i })).toBeInTheDocument();
await openAccountMenu(user);
await user.click(screen.getByRole('button', { name: /^dark$/i }));
expect(localStorage.getItem('reader-theme')).toBe('dark');
// Local-only mode has no signed-in user, so there is no Settings/Log out to show
expect(screen.queryByRole('menuitem', { name: /log out/i })).not.toBeInTheDocument();
});
test('the account menu closes on Escape', async () => {
const user = userEvent.setup();
await renderApp();
await openAccountMenu(user);
expect(screen.getByRole('button', { name: /^paper$/i })).toBeInTheDocument();
await user.keyboard('{Escape}');
expect(screen.queryByRole('button', { name: /^paper$/i })).not.toBeInTheDocument();
});
test('the brand button and Studies return Home from another page', async () => {
const user = userEvent.setup();
await renderApp();
await user.click(screen.getByRole('button', { name: /import session list/i }));
await screen.findByRole('heading', { name: /import session list/i });
await user.click(screen.getByRole('button', { name: /bible study home/i }));
expect(await screen.findByText('My Studies')).toBeInTheDocument();
});
});
describe('Study layout', () => {
test('notes sit in their own box above the resources, not under the passage', async () => {
const u = await loadChapter();
addChunk('Paul, a servant', 'In hope of eternal');
await screen.findByText('1 created');
await u.click(screen.getByRole('button', { name: /begin studying/i }));
await findChunkCounter(1, 1);
const notes = screen.getByRole('region', { name: 'Notes' });
const resources = screen.getByRole('region', { name: 'Resources' });
expect(within(notes).getByRole('textbox', { name: 'Observe' })).toBeInTheDocument();
expect(within(resources).getByRole('tab', { name: /commentary/i })).toBeInTheDocument();
// notes come first in the right column
expect(notes.compareDocumentPosition(resources) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
// and the passage column no longer holds them
expect(within(screen.getByRole('region', { name: 'Scripture' })).queryByRole('textbox')).not.toBeInTheDocument();
});
});