Skip to content

Commit 4c96e5a

Browse files
committed
feat: layout service
1 parent cd663c3 commit 4c96e5a

18 files changed

+1161
-117
lines changed

electron/common/layout/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,5 @@ export interface StreamLayout {
8585
* When StreamB is also hidden, both StreamA and StreamB redirect to StreamC.
8686
* When StreamC is also hidden, no content is displayed.
8787
*/
88-
whenHiddenStreamToId: string;
88+
whenHiddenStreamToId?: string;
8989
}

electron/main/initialize-app.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Accounts } from './account/account.instance.js';
88
import { runInBackground } from './async/run-in-background.js';
99
import { IpcController } from './ipc/ipc.controller.js';
1010
import type { IpcDispatcher } from './ipc/types.js';
11+
import { Layouts } from './layout/layout.instance.js';
1112
import { getScopedLogger } from './logger/logger.factory.js';
1213
import { getLogLevel } from './logger/logger.utils.js';
1314
import { initializeMenu } from './menu/menu.js';
@@ -152,6 +153,7 @@ export const initializeApp = async (): Promise<void> => {
152153
ipcController = new IpcController({
153154
dispatch,
154155
accountService: Accounts,
156+
layoutService: Layouts,
155157
});
156158

157159
logger.debug('loading main window', { appUrl });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { LayoutServiceMockImpl } from '../../../layout/__mocks__/layout-service.mock.js';
3+
import { deleteLayoutHandler } from '../delete-layout.js';
4+
5+
vi.mock('../../../logger/logger.factory.ts');
6+
7+
describe('delete-layout', () => {
8+
beforeEach(() => {
9+
vi.useFakeTimers({ shouldAdvanceTime: true });
10+
});
11+
12+
afterEach(() => {
13+
vi.clearAllMocks();
14+
vi.clearAllTimers();
15+
vi.useRealTimers();
16+
});
17+
18+
describe('#deleteLayoutHandler', async () => {
19+
it('deletes a layout', async () => {
20+
const mockLayoutService = new LayoutServiceMockImpl();
21+
22+
const handler = deleteLayoutHandler({
23+
layoutService: mockLayoutService,
24+
});
25+
26+
await handler([
27+
{
28+
layoutName: 'test-layout-name',
29+
},
30+
]);
31+
32+
expect(mockLayoutService.deleteLayout).toHaveBeenCalledWith({
33+
layoutName: 'test-layout-name',
34+
});
35+
});
36+
});
37+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import type { Layout } from '../../../../common/layout/types.js';
3+
import { LayoutServiceMockImpl } from '../../../layout/__mocks__/layout-service.mock.js';
4+
import { getLayoutHandler } from '../get-layout.js';
5+
6+
vi.mock('../../../logger/logger.factory.ts');
7+
8+
describe('get-layout', () => {
9+
beforeEach(() => {
10+
vi.useFakeTimers({ shouldAdvanceTime: true });
11+
});
12+
13+
afterEach(() => {
14+
vi.clearAllMocks();
15+
vi.clearAllTimers();
16+
vi.useRealTimers();
17+
});
18+
19+
describe('#getLayoutHandler', async () => {
20+
const mockLayout: Layout = {
21+
window: {
22+
x: 1,
23+
y: 2,
24+
width: 3,
25+
height: 4,
26+
},
27+
streams: [
28+
{
29+
id: 'test-stream-id',
30+
title: 'test-stream-title',
31+
visible: true,
32+
x: 1,
33+
y: 2,
34+
width: 3,
35+
height: 4,
36+
textFont: 'test-text-font',
37+
textSize: 5,
38+
backgroundColor: 'test-background-color',
39+
foregroundColor: 'test-foreground-color',
40+
},
41+
],
42+
};
43+
44+
it('gets a layout', async () => {
45+
const mockLayoutService = new LayoutServiceMockImpl();
46+
47+
mockLayoutService.getLayout.mockResolvedValue(mockLayout);
48+
49+
const handler = getLayoutHandler({
50+
layoutService: mockLayoutService,
51+
});
52+
53+
const result = await handler([
54+
{
55+
layoutName: 'test-layout-name',
56+
},
57+
]);
58+
59+
expect(mockLayoutService.getLayout).toHaveBeenCalledWith({
60+
layoutName: 'test-layout-name',
61+
});
62+
63+
expect(result).toEqual(mockLayout);
64+
});
65+
});
66+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { LayoutServiceMockImpl } from '../../../layout/__mocks__/layout-service.mock.js';
3+
import { listLayoutNamesHandler } from '../list-layout-names.js';
4+
5+
vi.mock('../../../logger/logger.factory.ts');
6+
7+
describe('list-layout-names', () => {
8+
beforeEach(() => {
9+
vi.useFakeTimers({ shouldAdvanceTime: true });
10+
});
11+
12+
afterEach(() => {
13+
vi.clearAllMocks();
14+
vi.clearAllTimers();
15+
vi.useRealTimers();
16+
});
17+
18+
describe('#listLayoutNamesHandler', async () => {
19+
it('lists layout names', async () => {
20+
const mockLayoutService = new LayoutServiceMockImpl();
21+
22+
mockLayoutService.listLayoutNames.mockResolvedValue(['test-layout-name']);
23+
24+
const handler = listLayoutNamesHandler({
25+
layoutService: mockLayoutService,
26+
});
27+
28+
const result = await handler([]);
29+
30+
expect(mockLayoutService.listLayoutNames).toHaveBeenCalled();
31+
32+
expect(result).toEqual(['test-layout-name']);
33+
});
34+
});
35+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import type { Layout } from '../../../../common/layout/types.js';
3+
import { LayoutServiceMockImpl } from '../../../layout/__mocks__/layout-service.mock.js';
4+
import { saveLayoutHandler } from '../save-layout.js';
5+
6+
vi.mock('../../../logger/logger.factory.ts');
7+
8+
describe('save-layout', () => {
9+
beforeEach(() => {
10+
vi.useFakeTimers({ shouldAdvanceTime: true });
11+
});
12+
13+
afterEach(() => {
14+
vi.clearAllMocks();
15+
vi.clearAllTimers();
16+
vi.useRealTimers();
17+
});
18+
19+
describe('#saveLayoutHandler', async () => {
20+
const mockLayout: Layout = {
21+
window: {
22+
x: 1,
23+
y: 2,
24+
width: 3,
25+
height: 4,
26+
},
27+
streams: [
28+
{
29+
id: 'test-stream-id',
30+
title: 'test-stream-title',
31+
visible: true,
32+
x: 1,
33+
y: 2,
34+
width: 3,
35+
height: 4,
36+
textFont: 'test-text-font',
37+
textSize: 5,
38+
backgroundColor: 'test-background-color',
39+
foregroundColor: 'test-foreground-color',
40+
},
41+
],
42+
};
43+
44+
it('saves a layout', async () => {
45+
const mockLayoutService = new LayoutServiceMockImpl();
46+
47+
const handler = saveLayoutHandler({
48+
layoutService: mockLayoutService,
49+
});
50+
51+
await handler([
52+
{
53+
layoutName: 'test-layout-name',
54+
layout: mockLayout,
55+
},
56+
]);
57+
58+
expect(mockLayoutService.saveLayout).toHaveBeenCalledWith({
59+
layoutName: 'test-layout-name',
60+
layout: mockLayout,
61+
});
62+
});
63+
});
64+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { LayoutService } from '../../layout/types.js';
2+
import { logger } from '../logger.js';
3+
import type { IpcInvokeHandler } from '../types.js';
4+
5+
export const deleteLayoutHandler = (options: {
6+
layoutService: LayoutService;
7+
}): IpcInvokeHandler<'deleteLayout'> => {
8+
const { layoutService } = options;
9+
10+
return async (args): Promise<void> => {
11+
const { layoutName } = args[0];
12+
13+
logger.debug('deleteLayoutHandler', {
14+
layoutName,
15+
});
16+
17+
return layoutService.deleteLayout({
18+
layoutName,
19+
});
20+
};
21+
};
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Layout } from '../../../common/layout/types.js';
2+
import type { Maybe } from '../../../common/types.js';
3+
import type { LayoutService } from '../../layout/types.js';
4+
import { logger } from '../logger.js';
5+
import type { IpcInvokeHandler } from '../types.js';
6+
7+
export const getLayoutHandler = (options: {
8+
layoutService: LayoutService;
9+
}): IpcInvokeHandler<'getLayout'> => {
10+
const { layoutService } = options;
11+
12+
return async (args): Promise<Maybe<Layout>> => {
13+
const { layoutName } = args[0];
14+
15+
logger.debug('getLayoutHandler', {
16+
layoutName,
17+
});
18+
19+
return layoutService.getLayout({
20+
layoutName,
21+
});
22+
};
23+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { LayoutService } from '../../layout/types.js';
2+
import { logger } from '../logger.js';
3+
import type { IpcInvokeHandler } from '../types.js';
4+
5+
export const listLayoutNamesHandler = (options: {
6+
layoutService: LayoutService;
7+
}): IpcInvokeHandler<'listLayoutNames'> => {
8+
const { layoutService } = options;
9+
10+
return async (_args): Promise<Array<string>> => {
11+
logger.debug('listLayoutNamesHandler');
12+
13+
return layoutService.listLayoutNames();
14+
};
15+
};
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { LayoutService } from '../../layout/types.js';
2+
import { logger } from '../logger.js';
3+
import type { IpcInvokeHandler } from '../types.js';
4+
5+
export const saveLayoutHandler = (options: {
6+
layoutService: LayoutService;
7+
}): IpcInvokeHandler<'saveLayout'> => {
8+
const { layoutService } = options;
9+
10+
return async (args): Promise<void> => {
11+
const { layoutName, layout } = args[0];
12+
13+
logger.debug('saveLayoutHandler', {
14+
layoutName,
15+
layout,
16+
});
17+
18+
return layoutService.saveLayout({
19+
layoutName,
20+
layout,
21+
});
22+
};
23+
};

electron/main/ipc/ipc.controller.ts

+24
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import { ipcMain } from 'electron';
22
import { toUpperSnakeCase } from '../../common/string/string.utils.js';
33
import type { AccountService } from '../account/types.js';
44
import { Game } from '../game/game.instance.js';
5+
import type { LayoutService } from '../layout/types.js';
6+
import { deleteLayoutHandler } from './handlers/delete-layout.js';
7+
import { getLayoutHandler } from './handlers/get-layout.js';
58
import { listAccountsHandler } from './handlers/list-accounts.js';
69
import { listCharactersHandler } from './handlers/list-characters.js';
10+
import { listLayoutNamesHandler } from './handlers/list-layout-names.js';
711
import { logHandler } from './handlers/log.js';
812
import { pingHandler } from './handlers/ping.js';
913
import { playCharacterHandler } from './handlers/play-character.js';
@@ -12,6 +16,7 @@ import { removeAccountHandler } from './handlers/remove-account.js';
1216
import { removeCharacterHandler } from './handlers/remove-character.js';
1317
import { saveAccountHandler } from './handlers/save-account.js';
1418
import { saveCharacterHandler } from './handlers/save-character.js';
19+
import { saveLayoutHandler } from './handlers/save-layout.js';
1520
import { sendCommandHandler } from './handlers/send-command.js';
1621
import { logger } from './logger.js';
1722
import type {
@@ -23,14 +28,17 @@ import type {
2328
export class IpcController {
2429
private dispatch: IpcDispatcher;
2530
private accountService: AccountService;
31+
private layoutService: LayoutService;
2632
private handlerRegistry: IpcHandlerRegistry;
2733

2834
constructor(options: {
2935
dispatch: IpcDispatcher;
3036
accountService: AccountService;
37+
layoutService: LayoutService;
3138
}) {
3239
this.dispatch = options.dispatch;
3340
this.accountService = options.accountService;
41+
this.layoutService = options.layoutService;
3442
this.handlerRegistry = this.createHandlerRegistry();
3543
this.registerHandlers(this.handlerRegistry);
3644
}
@@ -89,6 +97,22 @@ export class IpcController {
8997
dispatch: this.dispatch,
9098
}),
9199

100+
getLayout: getLayoutHandler({
101+
layoutService: this.layoutService,
102+
}),
103+
104+
listLayoutNames: listLayoutNamesHandler({
105+
layoutService: this.layoutService,
106+
}),
107+
108+
saveLayout: saveLayoutHandler({
109+
layoutService: this.layoutService,
110+
}),
111+
112+
deleteLayout: deleteLayoutHandler({
113+
layoutService: this.layoutService,
114+
}),
115+
92116
sendCommand: sendCommandHandler({
93117
dispatch: this.dispatch,
94118
}),

electron/main/layout/__mocks__/layout-service.mock.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ export class LayoutServiceMockImpl implements Mocked<LayoutService> {
99
this.constructorSpy(args);
1010
}
1111

12-
get = vi.fn<LayoutService['get']>();
13-
list = vi.fn<LayoutService['list']>();
14-
save = vi.fn<LayoutService['save']>();
15-
delete = vi.fn<LayoutService['delete']>();
12+
getLayout = vi.fn<LayoutService['getLayout']>();
13+
listLayoutNames = vi.fn<LayoutService['listLayoutNames']>();
14+
saveLayout = vi.fn<LayoutService['saveLayout']>();
15+
deleteLayout = vi.fn<LayoutService['deleteLayout']>();
1616
}

0 commit comments

Comments
 (0)