Skip to content
Open
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
50 changes: 50 additions & 0 deletions apps/desktop/src/components/WindowControls.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// @vitest-environment jsdom
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { WindowControls } from './WindowControls';

const native = vi.hoisted(() => ({
maximized: false,
resized: undefined as (() => void) | undefined,
unlisten: vi.fn(),
toggleMaximize: vi.fn(),
}));

vi.mock('@tauri-apps/api/core', () => ({ isTauri: () => true }));
vi.mock('@tauri-apps/api/window', () => ({ getCurrentWindow: () => ({
isMaximized: async () => native.maximized,
onResized: async (handler: () => void) => { native.resized = handler; return native.unlisten; },
toggleMaximize: native.toggleMaximize,
minimize: vi.fn(),
close: vi.fn(),
}) }));

let root: Root;
let host: HTMLDivElement;
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
native.maximized = false;
native.resized = undefined;
native.unlisten.mockClear();
native.toggleMaximize.mockReset().mockImplementation(async () => { native.maximized = !native.maximized; });
host = document.createElement('div');
root = createRoot(host);
});
afterEach(async () => { await act(async () => root.unmount()); vi.unstubAllGlobals(); });

it('shows the restore icon after maximizing and tracks external window resizing', async () => {
await act(async () => root.render(<WindowControls onError={error => { throw Error(error); }} t={key => key} />));
const button = host.querySelector<HTMLButtonElement>('[aria-label="windowMenu.maximize"]')!;
expect(button.getAttribute('aria-pressed')).toBe('false');
expect(button.querySelector('.lucide-square')).not.toBeNull();

await act(async () => button.click());
expect(button.getAttribute('aria-pressed')).toBe('true');
expect(button.querySelector('.lucide-copy')).not.toBeNull();

await act(async () => { native.maximized = false; native.resized?.(); });
expect(button.getAttribute('aria-pressed')).toBe('false');
expect(button.querySelector('.lucide-square')).not.toBeNull();
expect(native.unlisten).not.toHaveBeenCalled();
});
38 changes: 35 additions & 3 deletions apps/desktop/src/components/WindowControls.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,49 @@
import { useEffect, useState } from "react";
import { isTauri } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { Minus, Square, X } from "lucide-react";
import { Copy, Minus, Square, X } from "lucide-react";
import type { Translate } from "../i18n";

export function WindowControls({ onError, t }: { onError: (message: string) => void; t: Translate }) {
const [maximized, setMaximized] = useState(false);

useEffect(() => {
if (!isTauri()) return;
const appWindow = getCurrentWindow();
let active = true;
let unlisten: (() => void) | undefined;
const refresh = () => {
void appWindow.isMaximized().then(value => {
if (active) setMaximized(value);
}).catch((error: unknown) => {
if (active) onError(String(error));
});
};
refresh();
void appWindow.onResized(refresh).then(stop => {
if (active) unlisten = stop;
else stop();
}).catch((error: unknown) => {
if (active) onError(String(error));
});
return () => { active = false; unlisten?.(); };
}, [onError]);

const run = (action: "minimize" | "toggleMaximize" | "close") => {
void getCurrentWindow()[action]().catch((error: unknown) => onError(String(error)));
void getCurrentWindow()[action]().then(() => {
if (action === "toggleMaximize") {
void getCurrentWindow().isMaximized().then(setMaximized).catch((error: unknown) => onError(String(error)));
}
}).catch((error: unknown) => onError(String(error)));
};
return (
<div className="window-controls">
<button type="button" className="window-control" aria-label={t("windowMenu.minimize")}
onClick={() => run("minimize")}><Minus size={16} /></button>
<button type="button" className="window-control" aria-label={t("windowMenu.maximize")}
onClick={() => run("toggleMaximize")}><Square size={13} /></button>
aria-pressed={maximized} onClick={() => run("toggleMaximize")}>
{maximized ? <Copy size={15} /> : <Square size={13} />}
</button>
<button type="button" className="window-control window-control-close" aria-label={t("windowMenu.close")}
onClick={() => run("close")}><X size={17} /></button>
</div>
Expand Down
Loading