Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/__mocks__/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ BrowserWindow.fromWebContents = jest.fn( () => ( {
BrowserWindow.getAllWindows = jest.fn( () => [] );
BrowserWindow.getFocusedWindow = jest.fn();

const eventHandlers: { [ key: string ]: Array< ( ...args: any[] ) => void > } = {};
BrowserWindow.prototype.on = jest.fn( ( event: string, handler: ( ...args: any[] ) => void ) => {
if ( ! eventHandlers[ event ] ) {
eventHandlers[ event ] = [];
}
eventHandlers[ event ].push( handler );
} );
BrowserWindow.prototype.emit = jest.fn( ( event: string, ...args: any[] ) => {
const handlers = eventHandlers[ event ] || [];
handlers.forEach( ( handler ) => handler( ...args ) );
} );

export const Menu = {
buildFromTemplate: jest.fn(),
setApplicationMenu: jest.fn(),
Expand Down
14 changes: 10 additions & 4 deletions src/components/mac-titlebar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useFullscreen } from '../hooks/use-fullscreen';
import { cx } from '../lib/cx';
import { isWindowFrameRtl } from '../lib/is-window-frame-rtl';

Expand All @@ -8,15 +9,20 @@ export default function MacTitlebar( {
className?: string;
children?: React.ReactNode;
} ) {
const isFullscreen = useFullscreen();
const isRtl = isWindowFrameRtl();

return (
<div
className={ cx(
// Leave space for window controls depending on which side they are on
// Take into account the "chrome" padding, the position of which depends on the language direction
isWindowFrameRtl() &&
'transition-[padding] duration-500 ease-in-out',
! isFullscreen &&
isRtl &&
'ltr:pr-window-controls-width-excl-chrome-mac ltr:pl-chrome rtl:pr-window-controls-width-mac rtl:-ml-chrome',
! isWindowFrameRtl() &&
! isFullscreen &&
! isRtl &&
'ltr:pl-window-controls-width-mac rtl:pl-window-controls-width-excl-chrome-mac rtl:pr-chrome',
isFullscreen && 'ltr:pl-4 rtl:pr-4',
className
) }
>
Expand Down
80 changes: 80 additions & 0 deletions src/components/tests/mac-titlebar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { render } from '@testing-library/react';
import { useFullscreen } from '../../hooks/use-fullscreen';
import { isWindowFrameRtl } from '../../lib/is-window-frame-rtl';
import MacTitlebar from '../mac-titlebar';

jest.mock( '../../hooks/use-fullscreen' );
jest.mock( '../../lib/is-window-frame-rtl' );

const FULLSCREEN_CLASSES = {
ltr: 'ltr:pl-4',
rtl: 'rtl:pr-4',
} as const;

const NON_FULLSCREEN_CLASSES = {
ltr: {
normal: 'ltr:pl-window-controls-width-mac',
chrome: 'rtl:pl-window-controls-width-excl-chrome-mac rtl:pr-chrome',
},
rtl: {
normal:
'ltr:pr-window-controls-width-excl-chrome-mac ltr:pl-chrome rtl:pr-window-controls-width-mac rtl:-ml-chrome',
},
} as const;

describe( 'MacTitlebar', () => {
beforeEach( () => {
jest.clearAllMocks();
( useFullscreen as jest.Mock ).mockReturnValue( false );
( isWindowFrameRtl as jest.Mock ).mockReturnValue( false );
} );

it( 'should render with correct padding in non-fullscreen LTR mode', () => {
( useFullscreen as jest.Mock ).mockReturnValue( false );
( isWindowFrameRtl as jest.Mock ).mockReturnValue( false );

const { container } = render( <MacTitlebar /> );
const titlebar = container.firstChild;

expect( titlebar ).toHaveClass( NON_FULLSCREEN_CLASSES.ltr.normal );
expect( titlebar ).toHaveClass( NON_FULLSCREEN_CLASSES.ltr.chrome );
expect( titlebar ).not.toHaveClass( FULLSCREEN_CLASSES.ltr );
expect( titlebar ).not.toHaveClass( FULLSCREEN_CLASSES.rtl );
} );

it( 'should render with correct padding in non-fullscreen RTL mode', () => {
( useFullscreen as jest.Mock ).mockReturnValue( false );
( isWindowFrameRtl as jest.Mock ).mockReturnValue( true );

const { container } = render( <MacTitlebar /> );
const titlebar = container.firstChild;

expect( titlebar ).toHaveClass( NON_FULLSCREEN_CLASSES.rtl.normal );
expect( titlebar ).not.toHaveClass( FULLSCREEN_CLASSES.ltr );
expect( titlebar ).not.toHaveClass( FULLSCREEN_CLASSES.rtl );
} );

it( 'should render with minimal padding in fullscreen mode', () => {
( useFullscreen as jest.Mock ).mockReturnValue( true );

const { container } = render( <MacTitlebar /> );
const titlebar = container.firstChild;

expect( titlebar ).toHaveClass( FULLSCREEN_CLASSES.ltr );
expect( titlebar ).toHaveClass( FULLSCREEN_CLASSES.rtl );
expect( titlebar ).not.toHaveClass( NON_FULLSCREEN_CLASSES.ltr.normal );
expect( titlebar ).not.toHaveClass( NON_FULLSCREEN_CLASSES.rtl.normal );
} );

it( 'should render children', () => {
const { getByText } = render( <MacTitlebar>Test Content</MacTitlebar> );
expect( getByText( 'Test Content' ) ).toBeInTheDocument();
} );

it( 'should apply additional className if provided', () => {
const { container } = render( <MacTitlebar className="custom-class" /> );
const titlebar = container.firstChild;

expect( titlebar ).toHaveClass( 'custom-class' );
} );
} );
78 changes: 78 additions & 0 deletions src/hooks/tests/use-fullscreen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { renderHook, waitFor } from '@testing-library/react';
import { act } from 'react';
import { getIpcApi } from '../../lib/get-ipc-api';
import { useFullscreen } from '../use-fullscreen';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks for adding tests!

import { useIpcListener } from '../use-ipc-listener';

jest.mock( '../../lib/get-ipc-api' );
jest.mock( '../use-ipc-listener' );

const mockIpcApi = {
isFullscreen: jest.fn(),
};

( getIpcApi as jest.Mock ).mockReturnValue( mockIpcApi );

describe( 'useFullscreen', () => {
beforeEach( () => {
jest.clearAllMocks();
} );

it( 'should initialize with false and update when isFullscreen resolves', async () => {
mockIpcApi.isFullscreen.mockResolvedValue( true );
const { result } = renderHook( () => useFullscreen() );

expect( result.current ).toBe( false );

await waitFor( () => {
expect( mockIpcApi.isFullscreen ).toHaveBeenCalledTimes( 1 );
} );

expect( result.current ).toBe( true );
} );

it( 'should update state when receiving window-fullscreen-change event', async () => {
mockIpcApi.isFullscreen.mockResolvedValue( false );
let eventHandler: ( _: unknown, fullscreen: boolean ) => void = () => undefined;
( useIpcListener as jest.Mock ).mockImplementation( ( _channel, handler ) => {
eventHandler = handler;
} );

const { result } = renderHook( () => useFullscreen() );

await waitFor( () => {
expect( mockIpcApi.isFullscreen ).toHaveBeenCalledTimes( 1 );
} );

await act( async () => {
eventHandler( null, true );
} );

expect( result.current ).toBe( true );

await act( async () => {
eventHandler( null, false );
} );

expect( result.current ).toBe( false );
} );

it( 'should not update state if component is unmounted', async () => {
let eventHandler: ( _: unknown, fullscreen: boolean ) => void = () => undefined;
( useIpcListener as jest.Mock ).mockImplementation( ( _channel, handler ) => {
eventHandler = handler;
} );

const { result, unmount } = renderHook( () => useFullscreen() );

expect( result.current ).toBe( false );

unmount();

await act( async () => {
eventHandler( null, true );
} );

expect( result.current ).toBe( false );
} );
} );
27 changes: 27 additions & 0 deletions src/hooks/use-fullscreen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useState, useEffect } from 'react';
import { getIpcApi } from '../lib/get-ipc-api';
import { useIpcListener } from './use-ipc-listener';

export function useFullscreen() {
const [ isFullscreen, setIsFullscreen ] = useState( false );

useEffect( () => {
let mounted = true;
getIpcApi()
.isFullscreen()
.then( ( fullscreen ) => {
if ( mounted ) {
setIsFullscreen( fullscreen );
}
} );
return () => {
mounted = false;
};
}, [] );

useIpcListener( 'window-fullscreen-change', ( _, fullscreen: boolean ) => {
setIsFullscreen( fullscreen );
} );

return isFullscreen;
}
9 changes: 9 additions & 0 deletions src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { sortSites } from './lib/sort-sites';
import { installSqliteIntegration, keepSqliteIntegrationUpdated } from './lib/sqlite-versions';
import * as windowsHelpers from './lib/windows-helpers';
import { getLogsFilePath, writeLogToFile, type LogLevel } from './logging';
import { withMainWindow } from './main-window';
import { popupMenu, setupMenu } from './menu';
import { SiteServer, createSiteWorkingDirectory } from './site-server';
import { DEFAULT_SITE_PATH, getResourcesPath, getSiteThumbnailPath } from './storage/paths';
Expand Down Expand Up @@ -1092,3 +1093,11 @@ export async function getFileContent( event: IpcMainInvokeEvent, filePath: strin

return fs.readFileSync( filePath );
}

export async function isFullscreen( _event: IpcMainInvokeEvent ): Promise< boolean > {
let isFullscreen = false;
withMainWindow( ( window ) => {
isFullscreen = window.isFullScreen();
} );
return isFullscreen;
}
8 changes: 8 additions & 0 deletions src/main-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ export function createMainWindow(): BrowserWindow {
mainWindow = null;
} );

mainWindow.on( 'enter-full-screen', () => {
mainWindow?.webContents.send( 'window-fullscreen-change', true );
} );

mainWindow.on( 'leave-full-screen', () => {
mainWindow?.webContents.send( 'window-fullscreen-change', false );
} );

return mainWindow;
}

Expand Down
2 changes: 2 additions & 0 deletions src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const api: IpcApi = {
clearSyncOperation: ( id: string ) => ipcRenderer.invoke( 'clearSyncOperation', id ),
getPathForFile: webUtils.getPathForFile,
getFileContent: ( filePath: string ) => ipcRenderer.invoke( 'getFileContent', filePath ),
isFullscreen: () => ipcRenderer.invoke( 'isFullscreen' ),
};

contextBridge.exposeInMainWorld( 'ipcApi', api );
Expand All @@ -119,6 +120,7 @@ const allowedChannels = [
'theme-details-updating',
'on-import',
'on-export',
'window-fullscreen-change',
] as const;

contextBridge.exposeInMainWorld( 'ipcListener', {
Expand Down
30 changes: 29 additions & 1 deletion src/tests/ipc-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
*/
import { shell, IpcMainInvokeEvent } from 'electron';
import fs from 'fs';
import { createSite, startServer } from '../ipc-handlers';
import { createSite, startServer, isFullscreen } from '../ipc-handlers';
import { isEmptyDir, pathExists } from '../lib/fs-utils';
import { keepSqliteIntegrationUpdated } from '../lib/sqlite-versions';
import { withMainWindow } from '../main-window';
import { SiteServer, createSiteWorkingDirectory } from '../site-server';

jest.mock( 'fs' );
Expand All @@ -14,6 +15,7 @@ jest.mock( '../lib/fs-utils' );
jest.mock( '../site-server' );
jest.mock( '../lib/sqlite-versions' );
jest.mock( '../../vendor/wp-now/src/download' );
jest.mock( '../main-window' );

( SiteServer.create as jest.Mock ).mockImplementation( ( details ) => ( {
start: jest.fn(),
Expand Down Expand Up @@ -93,3 +95,29 @@ describe( 'startServer', () => {
expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( mockSitePath );
} );
} );

describe( 'isFullscreen', () => {
it( 'should return false when window is not in fullscreen', async () => {
( withMainWindow as jest.Mock ).mockImplementationOnce( ( callback ) => {
callback( {
isFullScreen: () => false,
} );
} );

const result = await isFullscreen( mockIpcMainInvokeEvent );

expect( result ).toBe( false );
} );

it( 'should return true when window is in fullscreen', async () => {
( withMainWindow as jest.Mock ).mockImplementationOnce( ( callback ) => {
callback( {
isFullScreen: () => true,
} );
} );

const result = await isFullscreen( mockIpcMainInvokeEvent );

expect( result ).toBe( true );
} );
} );
32 changes: 32 additions & 0 deletions src/tests/main-window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,35 @@ describe( 'withMainWindow', () => {
expect( callback ).toHaveBeenCalledWith( expect.any( BrowserWindow ) );
} );
} );

describe( 'fullscreen events', () => {
let createdWindow: BrowserWindow;

beforeEach( () => {
createdWindow = createMainWindow();
} );

afterEach( () => {
__resetMainWindow();
} );

it( 'sends fullscreen-change event when entering fullscreen', () => {
const mockSend = jest.fn();
createdWindow.webContents.send = mockSend;

// Simulate entering fullscreen
createdWindow.emit( 'enter-full-screen' );

expect( mockSend ).toHaveBeenCalledWith( 'window-fullscreen-change', true );
} );

it( 'sends fullscreen-change event when leaving fullscreen', () => {
const mockSend = jest.fn();
createdWindow.webContents.send = mockSend;

// Simulate leaving fullscreen
createdWindow.emit( 'leave-full-screen' );

expect( mockSend ).toHaveBeenCalledWith( 'window-fullscreen-change', false );
} );
} );