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
4 changes: 4 additions & 0 deletions e2e-tests/tests/isolated/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ npx playwright test --config e2e-tests/playwright.config.ts --project=isolated e
## Subdirectories

- `comment-assignment/` — tests for assigning comments to users
- `navigation-history/` — tests for back/forward reference history navigation
- `overlay/` — tests for the project-switch transition overlay
- `scroll-groups/` — tests for scroll-group synchronization between scripture editors
- `title-bar/` — tests for title bar layout, e.g. reserved space for native window controls
- `verse-navigation/` — tests for verse navigation keyboard shortcuts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { test, expect } from '../../../fixtures/cdp.fixture';
import { waitForAppReady } from '../../../fixtures/helpers';

/** Window Controls Overlay API isn't in TS's DOM lib yet. */
type NavigatorWithWindowControlsOverlay = Navigator & {
windowControlsOverlay?: { getTitlebarAreaRect(): DOMRect };
};

test.describe('Title bar reserved space', () => {
// titleBarOverlay (main.ts) — and navigator.windowControlsOverlay with it — is Windows-only.
// Skip must be called at describe/test scope; the equivalent call inside beforeAll is a silent
// Playwright no-op, so both tests would otherwise run (and fail) on macOS/Linux.
test.skip(process.platform !== 'win32', 'titleBarOverlay only applies on win32 (see main.ts)');

test('profile button stays clear of the OS-reserved title bar area', async ({ mainPage }) => {
await waitForAppReady(mainPage);

const overlayRight = await mainPage.evaluate(() => {
// Widening navigator to the non-standard API above
// eslint-disable-next-line no-type-assertion/no-type-assertion
const { windowControlsOverlay } = navigator as NavigatorWithWindowControlsOverlay;
return windowControlsOverlay?.getTitlebarAreaRect().right;
});
expect(overlayRight).toBeDefined();

const trigger = mainPage.locator('[data-testid="user-profile-popover-trigger"]');
await expect(trigger).toBeVisible();
const triggerBox = await trigger.boundingBox();
expect(triggerBox).not.toBeNull();
if (!triggerBox || overlayRight === undefined) return;

Comment thread
imnasnainaec marked this conversation as resolved.
// A plain visibility check would not have caught the original bug: the button was DOM-visible
// the whole time, just painted over by the native (non-DOM) OS buttons on top of it.
expect(triggerBox.x + triggerBox.width).toBeLessThanOrEqual(overlayRight);
});

test('clicking the profile button opens its popover', async ({ mainPage }) => {
await waitForAppReady(mainPage);

const trigger = mainPage.locator('[data-testid="user-profile-popover-trigger"]');
await trigger.click();

await expect(mainPage.locator('[data-testid="user-profile-name"]')).toBeVisible();
});
});
112 changes: 111 additions & 1 deletion src/renderer/components/platform-bible-toolbar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { vi } from 'vitest';
import React from 'react';
import { useScrollGroupScrRef, useSetting } from '@renderer/hooks/papi-hooks';
import { useNavigationTargetWebView } from '@renderer/hooks/use-navigation-target-web-view.hook';
import { useWindowControlsOverlay } from '@renderer/hooks/use-window-controls-overlay.hook';
import { ResolvedWebView } from '@renderer/services/navigation-target.util';
import { updateWebViewDefinitionSync } from '@renderer/services/web-view.service-host';
import { sendCommand } from '@shared/services/command.service';
Expand Down Expand Up @@ -60,6 +61,10 @@ vi.mock('@renderer/hooks/use-navigation-target-web-view.hook', () => ({
useNavigationTargetWebView: vi.fn((): ResolvedWebView | undefined => undefined),
}));

vi.mock('@renderer/hooks/use-window-controls-overlay.hook', () => ({
useWindowControlsOverlay: vi.fn((): DOMRect | undefined => undefined),
}));

vi.mock('@renderer/services/web-view.service-host', () => ({
updateWebViewDefinitionSync: vi.fn(() => true),
}));
Expand Down Expand Up @@ -129,14 +134,18 @@ vi.mock('platform-bible-react', async (importOriginal) => {
const actual = await importOriginal<typeof import('platform-bible-react')>();
return {
...actual,
// `className` is captured on a testid'd wrapper so tests can assert whether the static
// OS-reserved-space class is applied, without depending on the real Toolbar's DOM structure.
Toolbar: ({
className,
configAreaChildren,
children,
}: {
className?: string;
configAreaChildren?: React.ReactNode;
children?: React.ReactNode;
}) => (
<div>
<div data-testid="toolbar-root" className={className}>
<div data-testid="toolbar-config-area">{configAreaChildren}</div>
<div data-testid="toolbar-main-area">{children}</div>
</div>
Expand Down Expand Up @@ -669,3 +678,104 @@ describe('PlatformBibleToolbar — scroll group write-back to the resolved targe
expect(vi.mocked(updateWebViewDefinitionSync)).not.toHaveBeenCalled();
});
});

describe('PlatformBibleToolbar — title bar reserved space', () => {
const mockSendCommandForOS = (osPlatform: string) => {
vi.mocked(sendCommand).mockImplementation(
// sendCommand has a complex generic signature; cast is required for the mock implementation
// eslint-disable-next-line no-type-assertion/no-type-assertion, @typescript-eslint/no-explicit-any
(async (commandName: string) => {
if (commandName === 'platformGetResources.isSendReceiveAvailable') return true;
if (commandName === 'platform.getOSPlatform') return osPlatform;
if (commandName === 'platform.isFullScreen') return false;
return undefined;
// sendCommand has a complex generic signature; cast is required for the mock implementation
// eslint-disable-next-line no-type-assertion/no-type-assertion, @typescript-eslint/no-explicit-any
}) as any,
);
};

beforeEach(() => {
vi.clearAllMocks();
// clearAllMocks() does not reset a prior test's mockReturnValue (see precedent above), so
// restore the default explicitly
vi.mocked(useWindowControlsOverlay).mockReturnValue(undefined);
});

it('reserves the live-measured overlay width plus breathing room on Windows, and does not also apply the static class', async () => {
vi.mocked(useWindowControlsOverlay).mockReturnValue(
new DOMRect(0, 0, window.innerWidth - 150, 32),
);
mockSendCommandForOS('win32');

render(<PlatformBibleToolbar />);

await waitFor(() => {
// OS controls area width 150px + 4px breathing room (RESERVED_SPACE_BREATHING_ROOM_PX)
expect(screen.getByTestId('toolbar-reserved-space-wrapper')).toHaveStyle({
paddingRight: '154px',
Comment thread
merchako marked this conversation as resolved.
});
});
expect(screen.getByTestId('toolbar-root')).not.toHaveClass('tw:pe-[calc(138px+1rem)]');
// Toolbar's own container has an unconditional border and tw:px-4 (16px end padding); when the
// wrapper above reserves the trailing space, Toolbar's own border must be dropped entirely (not
// just the end side) and its own end-side padding suppressed, or the border stops short at
// Toolbar's narrower edge instead of enclosing the reserved strip, and the wrapper's live
// measurement stacks on top of the 16px, over-reserving space.
expect(screen.getByTestId('toolbar-root')).toHaveClass('tw:border-0');
expect(screen.getByTestId('toolbar-root')).toHaveClass('tw:pe-0');
// The wrapper carries an equivalent border itself (as a layout-neutral box-shadow, not an
// actual border — see the toolbarReservedSpaceStyle comment in the component), so the outline
// encloses the full toolbar-plus-reserved-space region on every side instead of stopping short
// at Toolbar's narrower edge.
expect(screen.getByTestId('toolbar-reserved-space-wrapper')).toHaveStyle({
boxShadow: 'inset 0 0 0 1px var(--border)',
});
});

it('reserves space on the left when the live-measured gap is on the left (e.g., RTL locales)', async () => {
// left = 150, right = window.innerWidth: the gap sits on the left instead of the right.
vi.mocked(useWindowControlsOverlay).mockReturnValue(
new DOMRect(150, 0, window.innerWidth - 150, 32),
);
mockSendCommandForOS('win32');

render(<PlatformBibleToolbar />);

await waitFor(() => {
// 150px measured gap + 4px breathing room (RESERVED_SPACE_BREATHING_ROOM_PX)
expect(screen.getByTestId('toolbar-reserved-space-wrapper')).toHaveStyle({
paddingLeft: '154px',
});
});
});

it('applies no inline override while the overlay geometry is not yet known, falling back to the static class', async () => {
mockSendCommandForOS('win32');

render(<PlatformBibleToolbar />);

await waitFor(() => {
expect(screen.getByTestId('user-profile-popover-stub')).toBeInTheDocument();
});
expect(screen.getByTestId('toolbar-reserved-space-wrapper')).not.toHaveAttribute('style');
expect(screen.getByTestId('toolbar-root')).toHaveClass('tw:pe-[calc(138px+1rem)]');
expect(screen.getByTestId('toolbar-root')).not.toHaveClass('tw:border-0');
expect(screen.getByTestId('toolbar-root')).not.toHaveClass('tw:pe-0');
});

it('does not reserve space on macOS regardless of overlay geometry, keeping the static traffic-lights class', async () => {
vi.mocked(useWindowControlsOverlay).mockReturnValue(new DOMRect(0, 0, 700, 32));
mockSendCommandForOS('darwin');

render(<PlatformBibleToolbar />);

await waitFor(() => {
expect(screen.getByTestId('user-profile-popover-stub')).toBeInTheDocument();
});
expect(screen.getByTestId('toolbar-reserved-space-wrapper')).not.toHaveAttribute('style');
expect(screen.getByTestId('toolbar-root')).toHaveClass('tw:ps-[85px]');
expect(screen.getByTestId('toolbar-root')).not.toHaveClass('tw:border-0');
expect(screen.getByTestId('toolbar-root')).not.toHaveClass('tw:pe-0');
});
});
Loading
Loading