Skip to content

Commit

Permalink
feat: add window persisting data (#1487)
Browse files Browse the repository at this point in the history
Signed-off-by: Svetoslav Borislavov <[email protected]>
  • Loading branch information
SvetBorislavov authored Jan 27, 2025
1 parent 53c174b commit 22cc67c
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { ipcMain, safeStorage } from 'electron';

import { USE_KEYCHAIN } from '@main/shared/constants';
import { STATIC_USER, USE_KEYCHAIN } from '@main/shared/constants';

import { addClaim, getClaims } from '@main/services/localUser/claim';
import { login, register } from '@main/services/localUser/auth';

const createChannelName = (...props) => ['safeStorage', ...props].join(':');

export const STATIC_USER = 'keychain@mode';

export default () => {
/* Safe Storage */

Expand Down
66 changes: 66 additions & 0 deletions front-end/src/main/services/windowState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Rectangle } from 'electron';

import { BrowserWindow } from 'electron';

import { STATIC_USER, WINDOW_STATE } from '@main/shared/constants';

import { getPrismaClient } from '@main/db/prisma';

/* Sets the window state*/
export const setWindowBounds = async (window: BrowserWindow): Promise<void> => {
try {
const prisma = getPrismaClient();

const claim_value = JSON.stringify(window.getBounds());

const alreadyAdded = await prisma.claim.findFirst({
where: {
claim_key: WINDOW_STATE,
user: {
email: STATIC_USER,
},
},
});

if (alreadyAdded) {
await prisma.claim.update({
where: { id: alreadyAdded?.id },
data: { claim_value: JSON.stringify(window.getBounds()) },
});
return;
}

await prisma.claim.create({
data: {
claim_key: WINDOW_STATE,
claim_value,
user: {
connect: {
email: STATIC_USER,
},
},
},
});
} catch (error) {
console.log(error);
}
};

/* Get window bounds */
export const getWindowBounds = async (): Promise<Rectangle | null> => {
try {
const prisma = getPrismaClient();
const bounds = await prisma.claim.findFirst({
where: {
claim_key: WINDOW_STATE,
user: {
email: STATIC_USER,
},
},
});
return bounds ? JSON.parse(bounds.claim_value) : null;
} catch (error) {
console.log(error);
return null;
}
};
4 changes: 4 additions & 0 deletions front-end/src/main/shared/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export const TRANSACTION_MAX_SIZE = 6144; // in bytes
export const TRANSACTION_SIGNATURE_ESTIMATED_MAX_SIZE = 100; // in bytes
export const MEMO_MAX_LENGTH = 100;

/* Keychain */
export const STATIC_USER = 'keychain@mode';

/* Encrypted keys */
export const ENCRYPTED_KEY_ALREADY_IMPORTED = 'This key is already imported';

Expand All @@ -20,6 +23,7 @@ export const USE_KEYCHAIN = 'use_keychain';
export const UPDATE_LOCATION = 'update_location';
export const MIGRATION_STARTED = 'migration_started';
export const RECOVERY_PHRASE_HASH_UPDATED = 'recovery_phrase_hash_updated';
export const WINDOW_STATE = 'window_state';

/* Transaction tabs */
export const draftsTitle = 'Drafts';
Expand Down
21 changes: 16 additions & 5 deletions front-end/src/main/windows/mainWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join } from 'path';
import { BrowserWindow, screen, session as ses } from 'electron';

import { removeListeners, sendUpdateThemeEventTo } from '@main/modules/ipcHandlers/theme';
import { getWindowBounds, setWindowBounds } from '@main/services/windowState';

async function createWindow() {
process.env.DIST_ELECTRON = join(__dirname, '..');
Expand All @@ -11,15 +12,17 @@ async function createWindow() {
? join(process.env.DIST_ELECTRON, '../public')
: process.env.DIST;

const { width, height } = screen.getPrimaryDisplay().workAreaSize;

const preload = join(__dirname, '../preload/index.js');

const session = ses.fromPartition('persist:main');

const storedBounds = await getWindowBounds();
const { width, height } = screen.getPrimaryDisplay().workAreaSize;

const mainWindow = new BrowserWindow({
width: Math.round(width * 0.9),
height: Math.round(height * 0.9),
width: storedBounds ? storedBounds.width : Math.round(width * 0.9),
height: storedBounds ? storedBounds.height : Math.round(height * 0.9),
x: storedBounds ? storedBounds.x : undefined,
y: storedBounds ? storedBounds.y : undefined,
minWidth: 960,
minHeight: 750,
webPreferences: {
Expand All @@ -40,6 +43,14 @@ async function createWindow() {
mainWindow?.show();
});

mainWindow.on('resized', async () => {
await setWindowBounds(mainWindow);
});

mainWindow.on('moved', async () => {
await setWindowBounds(mainWindow);
});

mainWindow.on('closed', () => {
removeListeners();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import { mockDeep } from 'vitest-mock-extended';

import { getIPCHandler, invokeIPCHandler } from '../../../_utils_';

import { USE_KEYCHAIN } from '@main/shared/constants';
import { STATIC_USER, USE_KEYCHAIN } from '@main/shared/constants';

import registerSafeStorageHandlers, {
STATIC_USER,
} from '@main/modules/ipcHandlers/localUser/safeStorage';
import registerSafeStorageHandlers from '@main/modules/ipcHandlers/localUser/safeStorage';

import { Claim } from '@prisma/client';
import { safeStorage } from 'electron';
Expand Down
96 changes: 96 additions & 0 deletions front-end/src/tests/main/services/windowState.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { mockDeep } from 'vitest-mock-extended';
import prisma from '@main/db/__mocks__/prisma';

import { STATIC_USER, WINDOW_STATE } from '@main/shared/constants';

import { Claim } from '@prisma/client';
import { BrowserWindow } from 'electron';
import { setWindowBounds, getWindowBounds } from '@main/services/windowState';

vi.mock('@main/db/prisma');

describe('Window state Service', () => {
const mockWindow = mockDeep<BrowserWindow>();

beforeEach(() => {
vi.resetAllMocks();
});

describe('setWindowBounds', () => {
it('should update the window bounds if the claim already exists', async () => {
const bounds = { x: 0, y: 0, width: 800, height: 600 };
mockWindow.getBounds.mockReturnValue(bounds);

prisma.claim.findFirst.mockResolvedValueOnce({
id: '1',
claim_value: JSON.stringify(bounds),
} as Claim);

await setWindowBounds(mockWindow);

expect(prisma.claim.update).toHaveBeenCalledWith({
where: { id: '1' },
data: { claim_value: JSON.stringify(bounds) },
});
});

it('should create a new claim if it does not exist', async () => {
const bounds = { x: 0, y: 0, width: 800, height: 600 };
mockWindow.getBounds.mockReturnValue(bounds);

prisma.claim.findFirst.mockResolvedValueOnce(null);

await setWindowBounds(mockWindow);

expect(prisma.claim.create).toHaveBeenCalledWith({
data: {
claim_key: WINDOW_STATE,
claim_value: JSON.stringify(bounds),
user: {
connect: {
email: STATIC_USER,
},
},
},
});
});

it('should handle errors gracefully', async () => {
const bounds = { x: 0, y: 0, width: 800, height: 600 };
mockWindow.getBounds.mockReturnValue(bounds);

prisma.claim.findFirst.mockRejectedValueOnce(new Error('Database error'));

await expect(setWindowBounds(mockWindow)).resolves.not.toThrow();
});
});

describe('getWindowBounds', () => {
it('should retrieve the window bounds if the claim exists', async () => {
const bounds = { x: 0, y: 0, width: 800, height: 600 };
prisma.claim.findFirst.mockResolvedValueOnce({
claim_value: JSON.stringify(bounds),
} as Claim);

const result = await getWindowBounds();

expect(result).toEqual(bounds);
});

it('should return null if the claim does not exist', async () => {
prisma.claim.findFirst.mockResolvedValueOnce(null);

const result = await getWindowBounds();

expect(result).toBeNull();
});

it('should handle errors gracefully', async () => {
prisma.claim.findFirst.mockRejectedValueOnce(new Error('Database error'));

const result = await getWindowBounds();

expect(result).toBeNull();
});
});
});
64 changes: 60 additions & 4 deletions front-end/src/tests/main/windows/mainWindow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import type { MockedClass, MockedObject } from 'vitest';
import { vi } from 'vitest';

import { restoreOrCreateWindow } from '@main/windows/mainWindow';

import { BrowserWindow } from 'electron';
import { getWindowBounds, setWindowBounds } from '@main/services/windowState';

vi.mock('@main/db/prisma');
vi.mock('@main/services/windowState');

/**
* Mock real electron BrowserWindow API
Expand All @@ -19,6 +22,7 @@ vi.mock('electron', () => {

const callbacks = {
'ready-to-show': vi.fn(),
close: vi.fn(),
closed: vi.fn(),
};

Expand All @@ -31,9 +35,13 @@ vi.mock('electron', () => {
bw.prototype.focus = vi.fn();
bw.prototype.restore = vi.fn();
bw.prototype.close = vi.fn(() => {
callbacks.close && callbacks.close();
callbacks.closed && callbacks.closed();
});
bw.prototype.emit = vi.fn();
bw.prototype.emit = vi.fn((e: string, ...args: any): boolean => {
callbacks[e] && callbacks[e](args);
return false;
});
bw.prototype.show = vi.fn();
bw.prototype.restore = vi.fn(() => {
callbacks['ready-to-show'] && callbacks['ready-to-show']();
Expand Down Expand Up @@ -168,7 +176,7 @@ test('Should create a new window if the previous one was destroyed', async () =>
expect(mock.instances).toHaveLength(2);
});

test('Should set differnet VITE_PUBLIC', async () => {
test('Should set different VITE_PUBLIC', async () => {
const prevVITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;

//@ts-expect-error read-only property
Expand All @@ -188,9 +196,57 @@ test('Should set differnet VITE_PUBLIC', async () => {
process.env.VITE_DEV_SERVER_URL = prevVITE_DEV_SERVER_URL;
});

test('Should send theme event after ready-to-show emitted with already shown event ', async () => {
test('Should send theme event after ready-to-show emitted with already shown event', async () => {
const window = await restoreOrCreateWindow();

window.close();
window.restore();
});

test('Should create a window with correct bounds', async () => {
const { mock } = vi.mocked(BrowserWindow);
const bounds = { x: 0, y: 0, width: 800, height: 600 };
vi.mocked(getWindowBounds).mockResolvedValueOnce(bounds);

await restoreOrCreateWindow();

expect(mock.instances).toHaveLength(1);
const instance = mock.instances[0] as MockedObject<BrowserWindow>;
expect(instance.constructor).toHaveBeenCalledWith(
expect.objectContaining({
width: bounds.width,
height: bounds.height,
x: bounds.x,
y: bounds.y,
}),
);
});

test('Should create a window with default bounds if no stored bounds', async () => {
const { mock } = vi.mocked(BrowserWindow);
vi.mocked(getWindowBounds).mockResolvedValueOnce(null);

await restoreOrCreateWindow();

expect(mock.instances).toHaveLength(1);
const instance = mock.instances[0] as MockedObject<BrowserWindow>;
expect(instance.constructor).toHaveBeenCalledWith(
expect.objectContaining({
width: expect.any(Number),
height: expect.any(Number),
}),
);
});

test('Should update window state on resize, moved', async () => {
const { mock } = vi.mocked(BrowserWindow);

await restoreOrCreateWindow();
expect(mock.instances).toHaveLength(1);
const instance = mock.instances[0] as MockedObject<BrowserWindow>;

// Simulate first close event
instance.emit('resized');
instance.emit('moved');
expect(setWindowBounds).toHaveBeenCalledTimes(2);
});

0 comments on commit 22cc67c

Please sign in to comment.