chore: SDA-2799: format all files with prettier (#1181)

* format all files with prettier

* chore: format the problematic files

* chore: fix few more linting issues
This commit is contained in:
Vishwas Shashidhar 2021-01-29 15:53:22 +05:30 committed by GitHub
parent 91dba95cbd
commit e9773721e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 16403 additions and 13827 deletions

View File

@ -23,6 +23,7 @@
"demo": "run-os",
"demo:win32": "npm run prebuild && cross-env ELECTRON_DEV=true electron . --url=file:///src/demo/index.html",
"demo:darwin": "npm run prebuild && cross-env ELECTRON_DEV=true electron . --url=file://$(pwd)/src/demo/index.html",
"format": "pretty-quick",
"lint": "run-s lint:*",
"lint:project": "tslint --project tsconfig.json",
"lint:spec": "tslint --project tsconfig.spec.json",
@ -189,7 +190,7 @@
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
"pre-commit": "pretty-quick --staged && npm run lint"
}
}
}

View File

@ -10,200 +10,203 @@ const isReady: boolean = true;
const version: string = '4.0.0';
interface IApp {
commandLine: any;
getAppPath(): string;
getPath(type: string): string;
getName(): string;
isReady(): boolean;
getVersion(): string;
on(eventName: any, cb: any): void;
once(eventName: any, cb: any): void;
setPath(value: string, path: string): void;
setLoginItemSettings(settings: { openAtLogin: boolean, path: string }): void;
getLoginItemSettings(options?: { path: string, args: string[] }): ILoginItemSettings;
setAppLogsPath(): void;
commandLine: any;
getAppPath(): string;
getPath(type: string): string;
getName(): string;
isReady(): boolean;
getVersion(): string;
on(eventName: any, cb: any): void;
once(eventName: any, cb: any): void;
setPath(value: string, path: string): void;
setLoginItemSettings(settings: { openAtLogin: boolean; path: string }): void;
getLoginItemSettings(options?: {
path: string;
args: string[];
}): ILoginItemSettings;
setAppLogsPath(): void;
}
interface ILoginItemSettings {
openAtLogin: boolean;
openAtLogin: boolean;
}
interface IIpcMain {
on(event: any, cb: any): void;
send(event: any, cb: any): void;
on(event: any, cb: any): void;
send(event: any, cb: any): void;
}
interface IIpcRenderer {
sendSync(event: any, cb: any): any;
on(eventName: any, cb: any): void;
send(event: any, ...cb: any[]): void;
removeListener(eventName: any, cb: any): void;
once(eventName: any, cb: any): void;
sendSync(event: any, cb: any): any;
on(eventName: any, cb: any): void;
send(event: any, ...cb: any[]): void;
removeListener(eventName: any, cb: any): void;
once(eventName: any, cb: any): void;
}
interface IPowerMonitor {
getSystemIdleTime(): void;
getSystemIdleTime(): void;
}
const pathToConfigDir = (): string => {
if (isWindowsOS) {
return path.join(__dirname, '/../..') as string;
} else {
return path.join(__dirname, '/..') as string;
}
if (isWindowsOS) {
return path.join(__dirname, '/../..') as string;
} else {
return path.join(__dirname, '/..') as string;
}
};
// electron app mock...
export const app: IApp = {
getAppPath: pathToConfigDir,
getPath: (type) => {
if (type === 'exe') {
return path.join(pathToConfigDir(), executableName);
}
if (type === 'userData') {
return path.join(pathToConfigDir(), '/../config');
}
return pathToConfigDir();
},
getName: () => appName,
isReady: () => isReady,
getVersion: () => version,
on: (event, cb) => {
ipcEmitter.on(event, cb);
},
setPath: () => jest.fn(),
commandLine: {
appendSwitch: jest.fn(),
},
once: (eventName, cb) => {
ipcEmitter.on(eventName, cb);
},
setLoginItemSettings: () => jest.fn(),
getLoginItemSettings: (): ILoginItemSettings => {
return { openAtLogin: true };
},
setAppLogsPath: (): void => {
return;
},
getAppPath: pathToConfigDir,
getPath: (type) => {
if (type === 'exe') {
return path.join(pathToConfigDir(), executableName);
}
if (type === 'userData') {
return path.join(pathToConfigDir(), '/../config');
}
return pathToConfigDir();
},
getName: () => appName,
isReady: () => isReady,
getVersion: () => version,
on: (event, cb) => {
ipcEmitter.on(event, cb);
},
setPath: () => jest.fn(),
commandLine: {
appendSwitch: jest.fn(),
},
once: (eventName, cb) => {
ipcEmitter.on(eventName, cb);
},
setLoginItemSettings: () => jest.fn(),
getLoginItemSettings: (): ILoginItemSettings => {
return { openAtLogin: true };
},
setAppLogsPath: (): void => {
return;
},
};
// simple ipc mocks for render and main process ipc using
// nodes' EventEmitter
export const ipcMain: IIpcMain = {
on: (event, cb) => {
ipcEmitter.on(event, cb);
},
send: (event, args) => {
const senderEvent = {
sender: {
send: (eventSend, arg) => {
ipcEmitter.emit(eventSend, arg);
},
},
};
ipcEmitter.emit(event, senderEvent, args);
},
on: (event, cb) => {
ipcEmitter.on(event, cb);
},
send: (event, args) => {
const senderEvent = {
sender: {
send: (eventSend, arg) => {
ipcEmitter.emit(eventSend, arg);
},
},
};
ipcEmitter.emit(event, senderEvent, args);
},
};
export const powerMonitor: IPowerMonitor = {
getSystemIdleTime: jest.fn().mockReturnValue(mockIdleTime),
getSystemIdleTime: jest.fn().mockReturnValue(mockIdleTime),
};
export const ipcRenderer: IIpcRenderer = {
sendSync: (event, args) => {
const listeners = ipcEmitter.listeners(event);
if (listeners.length > 0) {
const listener = listeners[0];
const eventArg = {};
listener(eventArg, args);
return eventArg;
}
return null;
},
send: (event, ...args) => {
const senderEvent = {
sender: {
send: (eventSend, ...arg) => {
ipcEmitter.emit(eventSend, ...arg);
},
},
preventDefault: jest.fn(),
};
ipcEmitter.emit(event, senderEvent, ...args);
},
on: (eventName, cb) => {
ipcEmitter.on(eventName, cb);
},
removeListener: (eventName, cb) => {
ipcEmitter.removeListener(eventName, cb);
},
once: (eventName, cb) => {
ipcEmitter.on(eventName, cb);
},
sendSync: (event, args) => {
const listeners = ipcEmitter.listeners(event);
if (listeners.length > 0) {
const listener = listeners[0];
const eventArg = {};
listener(eventArg, args);
return eventArg;
}
return null;
},
send: (event, ...args) => {
const senderEvent = {
sender: {
send: (eventSend, ...arg) => {
ipcEmitter.emit(eventSend, ...arg);
},
},
preventDefault: jest.fn(),
};
ipcEmitter.emit(event, senderEvent, ...args);
},
on: (eventName, cb) => {
ipcEmitter.on(eventName, cb);
},
removeListener: (eventName, cb) => {
ipcEmitter.removeListener(eventName, cb);
},
once: (eventName, cb) => {
ipcEmitter.on(eventName, cb);
},
};
export const shell = {
openExternal: jest.fn(),
openExternal: jest.fn(),
};
// tslint:disable-next-line:variable-name
export const Menu = {
buildFromTemplate: jest.fn(),
setApplicationMenu: jest.fn(),
buildFromTemplate: jest.fn(),
setApplicationMenu: jest.fn(),
};
export const crashReporter = {
start: jest.fn(),
start: jest.fn(),
};
const getCurrentWindow = jest.fn(() => {
return {
isFullScreen: jest.fn(() => {
return false;
}),
isMaximized: jest.fn(() => {
return false;
}),
on: jest.fn(),
removeListener: jest.fn(),
isDestroyed: jest.fn(() => {
return false;
}),
close: jest.fn(),
maximize: jest.fn(),
minimize: jest.fn(),
unmaximize: jest.fn(),
setFullScreen: jest.fn(),
};
return {
isFullScreen: jest.fn(() => {
return false;
}),
isMaximized: jest.fn(() => {
return false;
}),
on: jest.fn(),
removeListener: jest.fn(),
isDestroyed: jest.fn(() => {
return false;
}),
close: jest.fn(),
maximize: jest.fn(),
minimize: jest.fn(),
unmaximize: jest.fn(),
setFullScreen: jest.fn(),
};
});
const clipboard = {
write: jest.fn(),
readTest: jest.fn(() => {
return '';
}),
write: jest.fn(),
readTest: jest.fn(() => {
return '';
}),
};
export const dialog = {
showMessageBox: jest.fn(),
showErrorBox: jest.fn(),
showMessageBox: jest.fn(),
showErrorBox: jest.fn(),
};
// tslint:disable-next-line:variable-name
export const BrowserWindow = {
getFocusedWindow: jest.fn(() => {
return {
isDestroyed: jest.fn(() => false),
};
}),
fromWebContents: (arg) => arg,
getAllWindows: jest.fn(() => []),
getFocusedWindow: jest.fn(() => {
return {
isDestroyed: jest.fn(() => false),
};
}),
fromWebContents: (arg) => arg,
getAllWindows: jest.fn(() => []),
};
export const session = {
defaultSession: {
clearCache: jest.fn(),
},
defaultSession: {
clearCache: jest.fn(),
},
};
export const remote = {
app,
getCurrentWindow,
clipboard,
app,
getCurrentWindow,
clipboard,
};

View File

@ -4,70 +4,70 @@ import AboutApp from '../src/renderer/components/about-app';
import { ipcRenderer, remote } from './__mocks__/electron';
describe('about app', () => {
const aboutAppDataLabel = 'about-app-data';
const aboutDataMock = {
sbeVersion: '1',
userConfig: {},
globalConfig: {},
cloudConfig: {},
finalConfig: {},
appName: 'Symphony',
versionLocalised: 'Version',
buildNumber: '4.x.x',
hostname: 'N/A',
sfeVersion: 'N/A',
sfeClientType: '1.5',
sdaVersion: '3.8.0',
sdaBuildNumber: '0',
electronVersion: '3.1.11',
chromeVersion: '66.789',
v8Version: '6.7.8',
nodeVersion: '10.12',
openSslVersion: '1.2.3',
zlibVersion: '4.5.6',
uvVersion: '7.8',
aresVersion: '9.10',
httpParserVersion: '11.12',
swiftSearchVersion: '1.55.3-beta.1',
swiftSearchSupportedVersion: 'N/A',
};
const onLabelEvent = 'on';
const removeListenerLabelEvent = 'removeListener';
const aboutAppDataLabel = 'about-app-data';
const aboutDataMock = {
sbeVersion: '1',
userConfig: {},
globalConfig: {},
cloudConfig: {},
finalConfig: {},
appName: 'Symphony',
versionLocalised: 'Version',
buildNumber: '4.x.x',
hostname: 'N/A',
sfeVersion: 'N/A',
sfeClientType: '1.5',
sdaVersion: '3.8.0',
sdaBuildNumber: '0',
electronVersion: '3.1.11',
chromeVersion: '66.789',
v8Version: '6.7.8',
nodeVersion: '10.12',
openSslVersion: '1.2.3',
zlibVersion: '4.5.6',
uvVersion: '7.8',
aresVersion: '9.10',
httpParserVersion: '11.12',
swiftSearchVersion: '1.55.3-beta.1',
swiftSearchSupportedVersion: 'N/A',
};
const onLabelEvent = 'on';
const removeListenerLabelEvent = 'removeListener';
it('should render correctly', () => {
const wrapper = shallow(React.createElement(AboutApp));
expect(wrapper).toMatchSnapshot();
});
it('should render correctly', () => {
const wrapper = shallow(React.createElement(AboutApp));
expect(wrapper).toMatchSnapshot();
});
it('should call `about-app-data` event when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(AboutApp));
expect(spy).toBeCalledWith(aboutAppDataLabel, expect.any(Function));
});
it('should call `about-app-data` event when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(AboutApp));
expect(spy).toBeCalledWith(aboutAppDataLabel, expect.any(Function));
});
it('should remove listener `about-app-data` when component is unmounted', () => {
const spyMount = jest.spyOn(ipcRenderer, onLabelEvent);
const spyUnmount = jest.spyOn(ipcRenderer, removeListenerLabelEvent);
const wrapper = shallow(React.createElement(AboutApp));
expect(spyMount).toBeCalledWith(aboutAppDataLabel, expect.any(Function));
wrapper.unmount();
expect(spyUnmount).toBeCalledWith(aboutAppDataLabel, expect.any(Function));
});
it('should remove listener `about-app-data` when component is unmounted', () => {
const spyMount = jest.spyOn(ipcRenderer, onLabelEvent);
const spyUnmount = jest.spyOn(ipcRenderer, removeListenerLabelEvent);
const wrapper = shallow(React.createElement(AboutApp));
expect(spyMount).toBeCalledWith(aboutAppDataLabel, expect.any(Function));
wrapper.unmount();
expect(spyUnmount).toBeCalledWith(aboutAppDataLabel, expect.any(Function));
});
it('should call `updateState` when component is mounted', () => {
const spy = jest.spyOn(AboutApp.prototype, 'setState');
shallow(React.createElement(AboutApp));
ipcRenderer.send('about-app-data', aboutDataMock);
expect(spy).toBeCalledWith(aboutDataMock);
});
it('should call `updateState` when component is mounted', () => {
const spy = jest.spyOn(AboutApp.prototype, 'setState');
shallow(React.createElement(AboutApp));
ipcRenderer.send('about-app-data', aboutDataMock);
expect(spy).toBeCalledWith(aboutDataMock);
});
it('should copy the correct data on to clipboard', () => {
const spyMount = jest.spyOn(remote.clipboard, 'write');
const wrapper = shallow(React.createElement(AboutApp));
ipcRenderer.send('about-app-data', aboutDataMock);
const copyButtonSelector = `button.AboutApp-copy-button[title="Copy all the version information in this page"]`;
wrapper.find(copyButtonSelector).simulate('click');
const expectedData = { text: JSON.stringify(aboutDataMock, null, 4) };
expect(spyMount).toBeCalledWith(expectedData, 'clipboard');
});
it('should copy the correct data on to clipboard', () => {
const spyMount = jest.spyOn(remote.clipboard, 'write');
const wrapper = shallow(React.createElement(AboutApp));
ipcRenderer.send('about-app-data', aboutDataMock);
const copyButtonSelector = `button.AboutApp-copy-button[title="Copy all the version information in this page"]`;
wrapper.find(copyButtonSelector).simulate('click');
const expectedData = { text: JSON.stringify(aboutDataMock, null, 4) };
expect(spyMount).toBeCalledWith(expectedData, 'clipboard');
});
});

View File

@ -1,73 +1,83 @@
jest.mock('electron-log');
jest.mock('../src/app/window-handler', () => {
return {
windowHandler: {
setIsAutoReload: jest.fn(() => true),
},
};
return {
windowHandler: {
setIsAutoReload: jest.fn(() => true),
},
};
});
describe('activity detection', () => {
const originalTimeout: number = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
let activityDetectionInstance;
const originalTimeout: number = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
let activityDetectionInstance;
beforeEach(() => {
jest.resetModules();
jest.useFakeTimers();
// I did it for reset module imported between tests
const { activityDetection } = require('../src/app/activity-detection');
activityDetectionInstance = activityDetection;
});
beforeEach(() => {
jest.resetModules();
jest.useFakeTimers();
// I did it for reset module imported between tests
const { activityDetection } = require('../src/app/activity-detection');
activityDetectionInstance = activityDetection;
});
afterAll((done) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
done();
});
afterAll((done) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
done();
});
it('should call `setWindowAndThreshold` correctly', () => {
// mocking startActivityMonitor
const spy: jest.SpyInstance = jest.spyOn(activityDetectionInstance, 'setWindowAndThreshold');
const idleThresholdMock: number = 1000;
it('should call `setWindowAndThreshold` correctly', () => {
// mocking startActivityMonitor
const spy: jest.SpyInstance = jest.spyOn(
activityDetectionInstance,
'setWindowAndThreshold',
);
const idleThresholdMock: number = 1000;
jest.spyOn(activityDetectionInstance, 'startActivityMonitor')
.mockImplementation(() => jest.fn());
jest
.spyOn(activityDetectionInstance, 'startActivityMonitor')
.mockImplementation(() => jest.fn());
activityDetectionInstance.setWindowAndThreshold({}, idleThresholdMock);
activityDetectionInstance.setWindowAndThreshold({}, idleThresholdMock);
expect(spy).toBeCalledWith({}, 1000);
});
expect(spy).toBeCalledWith({}, 1000);
});
it('should start activity monitor when `setWindowAndThreshold` is called', () => {
const idleThresholdMock: number = 1000;
const spy: jest.SpyInstance = jest.spyOn(activityDetectionInstance, 'startActivityMonitor')
.mockImplementation(() => jest.fn());
it('should start activity monitor when `setWindowAndThreshold` is called', () => {
const idleThresholdMock: number = 1000;
const spy: jest.SpyInstance = jest
.spyOn(activityDetectionInstance, 'startActivityMonitor')
.mockImplementation(() => jest.fn());
activityDetectionInstance.setWindowAndThreshold({}, idleThresholdMock);
activityDetectionInstance.setWindowAndThreshold({}, idleThresholdMock);
expect(spy).toBeCalled();
});
expect(spy).toBeCalled();
});
it('should call `activity` when `startActivityMonitor` is called', () => {
const spy: jest.SpyInstance = jest.spyOn(activityDetectionInstance, 'activity');
it('should call `activity` when `startActivityMonitor` is called', () => {
const spy: jest.SpyInstance = jest.spyOn(
activityDetectionInstance,
'activity',
);
activityDetectionInstance.startActivityMonitor();
activityDetectionInstance.startActivityMonitor();
jest.runOnlyPendingTimers();
jest.runOnlyPendingTimers();
expect(spy).toBeCalled();
});
expect(spy).toBeCalled();
});
it('should call `sendActivity` when period was greater than idleTime', () => {
// period is this.idleThreshold = 60 * 60 * 1000;
const mockIdleTime: number = 50;
const spy: jest.SpyInstance = jest.spyOn(activityDetectionInstance, 'sendActivity');
const mockIdleTimeinMillis: number = mockIdleTime * 1000;
it('should call `sendActivity` when period was greater than idleTime', () => {
// period is this.idleThreshold = 60 * 60 * 1000;
const mockIdleTime: number = 50;
const spy: jest.SpyInstance = jest.spyOn(
activityDetectionInstance,
'sendActivity',
);
const mockIdleTimeinMillis: number = mockIdleTime * 1000;
activityDetectionInstance.activity(mockIdleTime);
expect(spy).toBeCalledWith(mockIdleTimeinMillis);
});
activityDetectionInstance.activity(mockIdleTime);
expect(spy).toBeCalledWith(mockIdleTimeinMillis);
});
});

View File

@ -5,179 +5,200 @@ import { Tool } from '../src/renderer/components/snipping-tool';
import { ipcRenderer } from './__mocks__/electron';
const defaultProps = {
paths: [],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.pen,
screenSnippetPath: 'very-nice-path',
paths: [],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.pen,
screenSnippetPath: 'very-nice-path',
};
afterEach(() => {
jest.clearAllMocks();
jest.clearAllMocks();
});
describe('<AnnotateArea/>', () => {
it('should render correctly', () => {
const wrapper = mount(<AnnotateArea {...defaultProps} />);
expect(wrapper).toMatchSnapshot();
});
it('should render correctly', () => {
const wrapper = mount(<AnnotateArea {...defaultProps} />);
expect(wrapper).toMatchSnapshot();
});
it('should call onChange when drawn on annotate area', () => {
const wrapper = mount(<AnnotateArea {...defaultProps} />);
const area = wrapper.find('[data-testid="annotate-area"]');
area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
});
it('should call onChange when drawn on annotate area', () => {
const wrapper = mount(<AnnotateArea {...defaultProps} />);
const area = wrapper.find('[data-testid="annotate-area"]');
area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
});
it('should call onChange with correct pen props if drawn drawn on annotate area with pen', () => {
const wrapper = mount(<AnnotateArea {...defaultProps} />);
const area = wrapper.find('[data-testid="annotate-area"]');
area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(defaultProps.onChange).toHaveBeenCalledWith([{
color: 'rgba(38, 196, 58, 1)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
}]);
});
it('should call onChange with correct pen props if drawn drawn on annotate area with pen', () => {
const wrapper = mount(<AnnotateArea {...defaultProps} />);
const area = wrapper.find('[data-testid="annotate-area"]');
area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(defaultProps.onChange).toHaveBeenCalledWith([
{
color: 'rgba(38, 196, 58, 1)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
},
]);
});
it('should call onChange with correct highlight props if drawn drawn on annotate area with highlight', () => {
const highlightProps = {
paths: [],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.highlight,
screenSnippetPath: 'very-nice-path',
};
const wrapper = mount(<AnnotateArea {...highlightProps} />);
const area = wrapper.find('[data-testid="annotate-area"]');
area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(highlightProps.onChange).toHaveBeenCalledWith([{
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 28,
}]);
});
it('should call onChange with correct highlight props if drawn drawn on annotate area with highlight', () => {
const highlightProps = {
paths: [],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.highlight,
screenSnippetPath: 'very-nice-path',
};
const wrapper = mount(<AnnotateArea {...highlightProps} />);
const area = wrapper.find('[data-testid="annotate-area"]');
area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(highlightProps.onChange).toHaveBeenCalledWith([
{
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 28,
},
]);
});
it('should render path if path is provided in props', () => {
const pathProps = {
paths: [{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
}],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.highlight,
screenSnippetPath: 'very-nice-path',
};
const wrapper = mount(<AnnotateArea {...pathProps} />);
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(true);
});
it('should render path if path is provided in props', () => {
const pathProps = {
paths: [
{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
},
],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.highlight,
screenSnippetPath: 'very-nice-path',
};
const wrapper = mount(<AnnotateArea {...pathProps} />);
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(true);
});
it('should not render any path if no path is provided in props', () => {
const wrapper = mount(<AnnotateArea {...defaultProps} />);
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(false);
});
it('should not render any path if no path is provided in props', () => {
const wrapper = mount(<AnnotateArea {...defaultProps} />);
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(false);
});
it('should call onChange with hidden path if clicked on path with tool eraser', () => {
const pathProps = {
paths: [{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
}],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.eraser,
screenSnippetPath: 'very-nice-path',
};
const wrapper = mount(<AnnotateArea {...pathProps} />);
const path = wrapper.find('[data-testid="path0"]');
path.simulate('click');
expect(pathProps.onChange).toHaveBeenCalledWith([{
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: false,
strokeWidth: 5,
}]);
});
it('should call onChange with hidden path if clicked on path with tool eraser', () => {
const pathProps = {
paths: [
{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
},
],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.eraser,
screenSnippetPath: 'very-nice-path',
};
const wrapper = mount(<AnnotateArea {...pathProps} />);
const path = wrapper.find('[data-testid="path0"]');
path.simulate('click');
expect(pathProps.onChange).toHaveBeenCalledWith([
{
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: false,
strokeWidth: 5,
},
]);
});
it('should send annotate_erased if clicked on path with tool eraser', () => {
const spy = jest.spyOn(ipcRenderer, 'send');
const pathProps = {
paths: [{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
}],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.eraser,
screenSnippetPath: 'very-nice-path',
};
const wrapper = mount(<AnnotateArea {...pathProps} />);
const path = wrapper.find('[data-testid="path0"]');
const expectedValue = { type: 'annotate_erased', element: 'screen_capture_annotate' };
path.simulate('click');
expect(spy).toBeCalledWith('snippet-analytics-data', expectedValue);
});
it('should send annotate_erased if clicked on path with tool eraser', () => {
const spy = jest.spyOn(ipcRenderer, 'send');
const pathProps = {
paths: [
{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
},
],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.eraser,
screenSnippetPath: 'very-nice-path',
};
const wrapper = mount(<AnnotateArea {...pathProps} />);
const path = wrapper.find('[data-testid="path0"]');
const expectedValue = {
type: 'annotate_erased',
element: 'screen_capture_annotate',
};
path.simulate('click');
expect(spy).toBeCalledWith('snippet-analytics-data', expectedValue);
});
it('should send annotate_added_pen event when drawn with pen', () => {
const spy = jest.spyOn(ipcRenderer, 'send');
const expectedValue = { type: 'annotate_added_pen', element: 'screen_capture_annotate' };
const wrapper = mount(<AnnotateArea {...defaultProps} />);
const area = wrapper.find('[data-testid="annotate-area"]');
area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(spy).toBeCalledWith('snippet-analytics-data', expectedValue);
});
it('should send annotate_added_pen event when drawn with pen', () => {
const spy = jest.spyOn(ipcRenderer, 'send');
const expectedValue = {
type: 'annotate_added_pen',
element: 'screen_capture_annotate',
};
const wrapper = mount(<AnnotateArea {...defaultProps} />);
const area = wrapper.find('[data-testid="annotate-area"]');
area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(spy).toBeCalledWith('snippet-analytics-data', expectedValue);
});
it('should send annotate_added_highlight event when drawn with highlight', () => {
const highlightProps = {
paths: [],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.highlight,
screenSnippetPath: 'very-nice-path',
};
const spy = jest.spyOn(ipcRenderer, 'send');
const expectedValue = { type: 'annotate_added_highlight', element: 'screen_capture_annotate' };
const wrapper = mount(<AnnotateArea {...highlightProps} />);
const area = wrapper.find('[data-testid="annotate-area"]');
area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(spy).toBeCalledWith('snippet-analytics-data', expectedValue);
});
it('should send annotate_added_highlight event when drawn with highlight', () => {
const highlightProps = {
paths: [],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
annotateAreaDimensions: { width: 800, height: 800 },
chosenTool: Tool.highlight,
screenSnippetPath: 'very-nice-path',
};
const spy = jest.spyOn(ipcRenderer, 'send');
const expectedValue = {
type: 'annotate_added_highlight',
element: 'screen_capture_annotate',
};
const wrapper = mount(<AnnotateArea {...highlightProps} />);
const area = wrapper.find('[data-testid="annotate-area"]');
area.simulate('mousedown', { pageX: 2, pageY: 49 });
area.simulate('mouseup');
expect(spy).toBeCalledWith('snippet-analytics-data', expectedValue);
});
});

View File

@ -1,86 +1,93 @@
import * as fs from 'fs';
import * as path from 'path';
import * as rimraf from 'rimraf';
import { cleanAppCacheOnInstall, cleanUpAppCache, createAppCacheFile } from '../src/app/app-cache-handler';
import {
cleanAppCacheOnInstall,
cleanUpAppCache,
createAppCacheFile,
} from '../src/app/app-cache-handler';
import { app, session } from './__mocks__/electron';
jest.mock('fs', () => ({
writeFileSync: jest.fn(),
existsSync: jest.fn(() => true),
unlinkSync: jest.fn(),
readdirSync: jest.fn(() => ['Cache', 'GPUCache', 'Symphony.config', 'cloudConfig.config']),
lstatSync: jest.fn(() => {
return {
isDirectory: jest.fn(() => true),
};
}),
writeFileSync: jest.fn(),
existsSync: jest.fn(() => true),
unlinkSync: jest.fn(),
readdirSync: jest.fn(() => [
'Cache',
'GPUCache',
'Symphony.config',
'cloudConfig.config',
]),
lstatSync: jest.fn(() => {
return {
isDirectory: jest.fn(() => true),
};
}),
}));
jest.mock('path', () => ({
join: jest.fn(),
join: jest.fn(),
}));
jest.mock('rimraf', () => ({
sync: jest.fn(),
sync: jest.fn(),
}));
jest.mock('../src/common/logger', () => {
return {
logger: {
error: jest.fn(),
info: jest.fn(),
},
};
return {
logger: {
error: jest.fn(),
info: jest.fn(),
},
};
});
describe('app cache handler', () => {
describe('check app cache file', () => {
const cachePathExpected = path.join(app.getPath('userData'), 'CacheCheck');
describe('check app cache file', () => {
const cachePathExpected = path.join(app.getPath('userData'), 'CacheCheck');
it('should call `cleanUpAppCache` correctly', () => {
const spyFn = 'unlinkSync';
const spy = jest.spyOn(fs, spyFn);
cleanUpAppCache();
expect(spy).toBeCalledWith(cachePathExpected);
});
it('should call `clearCache` when `session.defaultSession` is not null', () => {
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
const spyFn = 'clearCache';
const spy = jest.spyOn(session.defaultSession, spyFn);
cleanUpAppCache();
expect(spy).toBeCalled();
});
it('should call `createAppCacheFile` correctly', () => {
const spyFn = 'writeFileSync';
const spy = jest.spyOn(fs, spyFn);
createAppCacheFile();
expect(spy).lastCalledWith(cachePathExpected, '');
});
it('should call `cleanUpAppCache` correctly', () => {
const spyFn = 'unlinkSync';
const spy = jest.spyOn(fs, spyFn);
cleanUpAppCache();
expect(spy).toBeCalledWith(cachePathExpected);
});
describe('clean app cache on install', () => {
it('should clean app cache and cookies on install', () => {
const pathSpy = jest.spyOn(path, 'join');
const fsReadDirSpy = jest.spyOn(fs, 'readdirSync');
const fsStatSpy = jest.spyOn(fs, 'lstatSync');
const fsUnlinkSpy = jest.spyOn(fs, 'unlinkSync');
const rimrafSpy = jest.spyOn(rimraf, 'sync');
cleanAppCacheOnInstall();
expect(pathSpy).toBeCalled();
expect(fsReadDirSpy).toBeCalled();
expect(fsStatSpy).toBeCalled();
expect(fsUnlinkSpy).toBeCalled();
expect(rimrafSpy).toBeCalled();
});
it('should call `clearCache` when `session.defaultSession` is not null', () => {
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
const spyFn = 'clearCache';
const spy = jest.spyOn(session.defaultSession, spyFn);
cleanUpAppCache();
expect(spy).toBeCalled();
});
it('should call `createAppCacheFile` correctly', () => {
const spyFn = 'writeFileSync';
const spy = jest.spyOn(fs, spyFn);
createAppCacheFile();
expect(spy).lastCalledWith(cachePathExpected, '');
});
});
describe('clean app cache on install', () => {
it('should clean app cache and cookies on install', () => {
const pathSpy = jest.spyOn(path, 'join');
const fsReadDirSpy = jest.spyOn(fs, 'readdirSync');
const fsStatSpy = jest.spyOn(fs, 'lstatSync');
const fsUnlinkSpy = jest.spyOn(fs, 'unlinkSync');
const rimrafSpy = jest.spyOn(rimraf, 'sync');
cleanAppCacheOnInstall();
expect(pathSpy).toBeCalled();
expect(fsReadDirSpy).toBeCalled();
expect(fsStatSpy).toBeCalled();
expect(fsUnlinkSpy).toBeCalled();
expect(rimrafSpy).toBeCalled();
});
});
});

View File

@ -8,347 +8,361 @@ import { logger } from '../src/common/logger';
import { dialog, session, shell } from './__mocks__/electron';
jest.mock('../src/app/reports-handler', () => {
return {
exportLogs: jest.fn(),
exportCrashDumps: jest.fn(),
};
return {
exportLogs: jest.fn(),
exportCrashDumps: jest.fn(),
};
});
jest.mock('../src/common/env', () => {
return {
isWindowsOS: false,
isLinux: false,
isMac: true,
};
return {
isWindowsOS: false,
isLinux: false,
isMac: true,
};
});
jest.mock('../src/app/window-actions', () => {
return {
updateAlwaysOnTop: jest.fn(),
};
return {
updateAlwaysOnTop: jest.fn(),
};
});
jest.mock('../src/app/auto-launch-controller', () => {
return {
autoLaunchInstance: {
disableAutoLaunch: jest.fn(),
enableAutoLaunch: jest.fn(),
},
};
return {
autoLaunchInstance: {
disableAutoLaunch: jest.fn(),
enableAutoLaunch: jest.fn(),
},
};
});
jest.mock('../src/app/config-handler', () => {
return {
CloudConfigDataTypes: {
NOT_SET: 'NOT_SET',
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
},
config: {
getConfigFields: jest.fn(() => {
return {
minimizeOnClose: 'ENABLED',
launchOnStartup: 'ENABLED',
alwaysOnTop: 'ENABLED',
isAlwaysOnTop: 'ENABLED',
bringToFront: 'ENABLED',
devToolsEnabled: true,
};
}),
getGlobalConfigFields: jest.fn(() => {
return {
devToolsEnabled: true,
};
}),
getFilteredCloudConfigFields: jest.fn(() => {
return {
devToolsEnabled: true,
};
}),
getCloudConfigFields: jest.fn(() => {
return {
devToolsEnabled: true,
};
}),
updateUserConfig: jest.fn(),
},
};
return {
CloudConfigDataTypes: {
NOT_SET: 'NOT_SET',
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
},
config: {
getConfigFields: jest.fn(() => {
return {
minimizeOnClose: 'ENABLED',
launchOnStartup: 'ENABLED',
alwaysOnTop: 'ENABLED',
isAlwaysOnTop: 'ENABLED',
bringToFront: 'ENABLED',
devToolsEnabled: true,
};
}),
getGlobalConfigFields: jest.fn(() => {
return {
devToolsEnabled: true,
};
}),
getFilteredCloudConfigFields: jest.fn(() => {
return {
devToolsEnabled: true,
};
}),
getCloudConfigFields: jest.fn(() => {
return {
devToolsEnabled: true,
};
}),
updateUserConfig: jest.fn(),
},
};
});
jest.mock('../src/app/window-handler', () => {
return {
windowHandler: {
createMoreInfoWindow: jest.fn(),
getMainWindow: jest.fn(),
},
};
return {
windowHandler: {
createMoreInfoWindow: jest.fn(),
getMainWindow: jest.fn(),
},
};
});
jest.mock('../src/common/logger', () => {
return {
logger: {
error: jest.fn(),
info: jest.fn(),
},
};
return {
logger: {
error: jest.fn(),
info: jest.fn(),
},
};
});
describe('app menu', () => {
let appMenu;
const updateUserFnLabel = 'updateUserConfig';
const item = {
checked: true,
};
let appMenu;
const updateUserFnLabel = 'updateUserConfig';
const item = {
checked: true,
};
const findMenuItemBuildWindowMenu = (menuItemLabel: string) => {
return appMenu.buildWindowMenu().submenu.find((menuItem) => {
return menuItem.label === menuItemLabel;
});
};
const findMenuItemBuildWindowMenu = (menuItemLabel: string) => {
return appMenu.buildWindowMenu().submenu.find((menuItem) => {
return menuItem.label === menuItemLabel;
});
};
const findMenuItemBuildHelpMenu = (menuItemLabel: string) => {
return appMenu.buildHelpMenu().submenu.find((menuItem) => {
return menuItem.label === menuItemLabel;
});
};
const findMenuItemBuildHelpMenu = (menuItemLabel: string) => {
return appMenu.buildHelpMenu().submenu.find((menuItem) => {
return menuItem.label === menuItemLabel;
});
};
const findHelpTroubleshootingMenuItem = (value: string) => {
const helpMenuItem = findMenuItemBuildHelpMenu('Troubleshooting');
return helpMenuItem.submenu.find((menuItem) => {
return menuItem.label === value;
});
};
const findHelpTroubleshootingMenuItem = (value: string) => {
const helpMenuItem = findMenuItemBuildHelpMenu('Troubleshooting');
return helpMenuItem.submenu.find((menuItem) => {
return menuItem.label === value;
});
};
const env = envMock as any;
const env = envMock as any;
beforeEach(() => {
appMenu = new AppMenu();
env.isWindowsOS = false;
env.isLinux = false;
env.isMac = true;
beforeEach(() => {
appMenu = new AppMenu();
env.isWindowsOS = false;
env.isLinux = false;
env.isMac = true;
});
it('should call `update` correctly', () => {
const spyFn = 'buildMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.locale = 'en-US';
appMenu.update('ja-JP');
expect(spy).toBeCalled();
});
describe('`popupMenu`', () => {
it('should fail when `appMenu.menu` is null', () => {
const spy = jest.spyOn(logger, 'error');
const expectedValue =
'app-menu: tried popup menu, but failed, menu not defined';
appMenu.menu = null;
appMenu.popupMenu();
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `update` correctly', () => {
const spyFn = 'buildMenu';
it('should call `menu.popup` correctly', () => {
const menuMock = {
popup: jest.fn(),
};
const expectedValue: Electron.PopupOptions = {
x: 1,
y: 1,
positioningItem: 1,
callback: () => null,
};
const spy = jest.spyOn(menuMock, 'popup');
appMenu.menu = menuMock;
appMenu.popupMenu(expectedValue);
expect(spy).toBeCalledWith(expectedValue);
});
});
describe('buildMenuKey', () => {
it('should call `buildAboutMenu` correctly', () => {
const spyFn = 'buildAboutMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.buildMenuKey(menuSections.about);
expect(spy).toBeCalled();
});
it('should call `buildEditMenu` correctly', () => {
const spyFn = 'buildEditMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.buildMenuKey(menuSections.edit);
expect(spy).toBeCalled();
});
it('should call `buildEditMenu` correctly on WindowsOS', () => {
env.isWindowsOS = true;
env.isLinux = false;
env.isMac = false;
const spyFn = 'buildEditMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.buildMenuKey(menuSections.edit);
expect(spy).toBeCalled();
});
it('should call `buildViewMenu` correctly', () => {
const spyFn = 'buildViewMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.buildMenuKey(menuSections.view);
expect(spy).toBeCalled();
});
it('should fail when key is incorrect', () => {
const invalidKey = 'error';
const expectedWarning = `app-menu: Invalid ${invalidKey}`;
expect(() => appMenu.buildMenuKey(invalidKey)).toThrow(expectedWarning);
});
describe('buildWindowMenu', () => {
it('should call `buildWindowMenu` correctly', () => {
const spyFn = 'buildWindowMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.locale = 'en-US';
appMenu.update('ja-JP');
appMenu.buildMenuKey(menuSections.window);
expect(spy).toBeCalled();
});
describe('`Auto Launch On Startup`', () => {
let autoLaunchMenuItem;
beforeAll(() => {
autoLaunchMenuItem = findMenuItemBuildWindowMenu(
'Auto Launch On Startup',
);
});
it('should disable `AutoLaunch` when click is triggered', async () => {
const spyFn = 'disableAutoLaunch';
const spyConfig = jest.spyOn(config, updateUserFnLabel);
const expectedValue = { launchOnStartup: 'NOT_SET' };
const spy = jest.spyOn(autoLaunchInstance, spyFn);
const customItem = {
checked: false,
};
await autoLaunchMenuItem.click(customItem);
expect(spy).toBeCalled();
expect(spyConfig).lastCalledWith(expectedValue);
});
it('should enable `AutoLaunch` when click is triggered', async () => {
const spyFn = 'enableAutoLaunch';
const spyConfig = jest.spyOn(config, updateUserFnLabel);
const expectedValue = { launchOnStartup: 'ENABLED' };
const spy = jest.spyOn(autoLaunchInstance, spyFn);
await autoLaunchMenuItem.click(item);
expect(spy).toBeCalled();
expect(spyConfig).lastCalledWith(expectedValue);
});
});
it('should update `alwaysOnTop` value when click is triggered', async () => {
const menuItem = findMenuItemBuildWindowMenu('Always on Top');
await menuItem.click(item);
expect(updateAlwaysOnTop).toBeCalledWith(true, true);
});
it('should update `minimizeOnClose` value when click is triggered', async () => {
const spyConfig = jest.spyOn(config, updateUserFnLabel);
const expectedValue = { minimizeOnClose: 'ENABLED' };
const menuItem = findMenuItemBuildWindowMenu('Minimize on Close');
await menuItem.click(item);
expect(spyConfig).lastCalledWith(expectedValue);
});
describe('`bringToFront`', () => {
it('should update `bringToFront` value when click is triggered', async () => {
const spyConfig = jest.spyOn(config, updateUserFnLabel);
const expectedValue = { bringToFront: 'ENABLED' };
const menuItem = findMenuItemBuildWindowMenu(
'Bring to Front on Notifications',
);
await menuItem.click(item);
expect(spyConfig).lastCalledWith(expectedValue);
});
it('should find `Flash Notification in Taskbar` when is WindowsOS', () => {
const expectedValue = 'Flash Notification in Taskbar';
env.isWindowsOS = true;
env.isLinux = false;
env.isMac = false;
const menuItem = findMenuItemBuildWindowMenu(
'Flash Notification in Taskbar',
);
expect(menuItem.label).toEqual(expectedValue);
});
});
it('should call clear cache and reload correctly', () => {
const focusedWindow = {
isDestroyed: jest.fn(() => false),
reload: jest.fn(),
};
const spySession = jest.spyOn(session.defaultSession, 'clearCache');
const menuItem = findMenuItemBuildWindowMenu('Clear cache and Reload');
menuItem.click(item, focusedWindow);
expect(spySession).toBeCalled();
});
});
describe('`popupMenu`', () => {
it('should fail when `appMenu.menu` is null', () => {
const spy = jest.spyOn(logger, 'error');
const expectedValue = 'app-menu: tried popup menu, but failed, menu not defined';
appMenu.menu = null;
appMenu.popupMenu();
expect(spy).toBeCalledWith(expectedValue);
describe('`buildHelpMenu`', () => {
it('should call `buildHelpMenu` correctly', () => {
const spyFn = 'buildHelpMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.buildMenuKey(menuSections.help);
expect(spy).toBeCalled();
});
it('should call `Symphony Help` correctly', () => {
const spy = jest.spyOn(shell, 'openExternal');
const expectedValue = 'https://support.symphony.com';
const menuItem = findMenuItemBuildHelpMenu('Symphony Help');
menuItem.click();
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `Learn More` correctly', () => {
const spy = jest.spyOn(shell, 'openExternal');
const expectedValue = 'https://support.symphony.com';
const menuItem = findMenuItemBuildHelpMenu('Learn More');
menuItem.click();
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `Show Logs in Finder` correctly', async () => {
const menuItem = findHelpTroubleshootingMenuItem('Show Logs in Finder');
await menuItem.click();
expect(exportLogs).toBeCalled();
});
it('should call `Show crash dump in Finder` correctly', () => {
const menuItem = findHelpTroubleshootingMenuItem(
'Show crash dump in Finder',
);
menuItem.click();
expect(exportCrashDumps).toBeCalled();
});
describe(`Toggle Developer Tools`, () => {
it('should call `toggleDevTools` when focusedWindow is not null', () => {
const focusedWindow = {
isDestroyed: jest.fn(() => false),
reload: jest.fn(),
webContents: {
toggleDevTools: jest.fn(),
},
};
const spy = jest.spyOn(focusedWindow.webContents, 'toggleDevTools');
const menuItem = findHelpTroubleshootingMenuItem(
'Toggle Developer Tools',
);
menuItem.click({}, focusedWindow);
expect(spy).toBeCalled();
});
it('should call `menu.popup` correctly', () => {
const menuMock = {
popup: jest.fn(),
};
const expectedValue: Electron.PopupOptions = {
x: 1,
y: 1,
positioningItem: 1,
callback: () => null,
};
const spy = jest.spyOn(menuMock, 'popup');
appMenu.menu = menuMock;
appMenu.popupMenu(expectedValue);
expect(spy).toBeCalledWith(expectedValue);
});
});
describe('buildMenuKey', () => {
it('should call `buildAboutMenu` correctly', () => {
const spyFn = 'buildAboutMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.buildMenuKey(menuSections.about);
expect(spy).toBeCalled();
});
it('should call `buildEditMenu` correctly', () => {
const spyFn = 'buildEditMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.buildMenuKey(menuSections.edit);
expect(spy).toBeCalled();
});
it('should call `buildEditMenu` correctly on WindowsOS', () => {
env.isWindowsOS = true;
env.isLinux = false;
env.isMac = false;
const spyFn = 'buildEditMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.buildMenuKey(menuSections.edit);
expect(spy).toBeCalled();
});
it('should call `buildViewMenu` correctly', () => {
const spyFn = 'buildViewMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.buildMenuKey(menuSections.view);
expect(spy).toBeCalled();
});
it('should fail when key is incorrect', () => {
const invalidKey = 'error';
const expectedWarning = `app-menu: Invalid ${invalidKey}`;
expect(() => appMenu.buildMenuKey(invalidKey)).toThrow(expectedWarning);
});
describe('buildWindowMenu', () => {
it('should call `buildWindowMenu` correctly', () => {
const spyFn = 'buildWindowMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.buildMenuKey(menuSections.window);
expect(spy).toBeCalled();
});
describe('`Auto Launch On Startup`', () => {
let autoLaunchMenuItem;
beforeAll(() => {
autoLaunchMenuItem = findMenuItemBuildWindowMenu('Auto Launch On Startup');
});
it('should disable `AutoLaunch` when click is triggered', async () => {
const spyFn = 'disableAutoLaunch';
const spyConfig = jest.spyOn(config, updateUserFnLabel);
const expectedValue = { launchOnStartup: 'NOT_SET' };
const spy = jest.spyOn(autoLaunchInstance, spyFn);
const customItem = {
checked: false,
};
await autoLaunchMenuItem.click(customItem);
expect(spy).toBeCalled();
expect(spyConfig).lastCalledWith(expectedValue);
});
it('should enable `AutoLaunch` when click is triggered', async () => {
const spyFn = 'enableAutoLaunch';
const spyConfig = jest.spyOn(config, updateUserFnLabel);
const expectedValue = { launchOnStartup: 'ENABLED' };
const spy = jest.spyOn(autoLaunchInstance, spyFn);
await autoLaunchMenuItem.click(item);
expect(spy).toBeCalled();
expect(spyConfig).lastCalledWith(expectedValue);
});
});
it('should update `alwaysOnTop` value when click is triggered', async () => {
const menuItem = findMenuItemBuildWindowMenu('Always on Top');
await menuItem.click(item);
expect(updateAlwaysOnTop).toBeCalledWith(true, true);
});
it('should update `minimizeOnClose` value when click is triggered', async () => {
const spyConfig = jest.spyOn(config, updateUserFnLabel);
const expectedValue = { minimizeOnClose: 'ENABLED' };
const menuItem = findMenuItemBuildWindowMenu('Minimize on Close');
await menuItem.click(item);
expect(spyConfig).lastCalledWith(expectedValue);
});
describe('`bringToFront`', () => {
it('should update `bringToFront` value when click is triggered', async () => {
const spyConfig = jest.spyOn(config, updateUserFnLabel);
const expectedValue = { bringToFront: 'ENABLED' };
const menuItem = findMenuItemBuildWindowMenu('Bring to Front on Notifications');
await menuItem.click(item);
expect(spyConfig).lastCalledWith(expectedValue);
});
it('should find `Flash Notification in Taskbar` when is WindowsOS', () => {
const expectedValue = 'Flash Notification in Taskbar';
env.isWindowsOS = true;
env.isLinux = false;
env.isMac = false;
const menuItem = findMenuItemBuildWindowMenu('Flash Notification in Taskbar');
expect(menuItem.label).toEqual(expectedValue);
});
});
it('should call clear cache and reload correctly', () => {
const focusedWindow = {
isDestroyed: jest.fn(() => false),
reload: jest.fn(),
};
const spySession = jest.spyOn(session.defaultSession, 'clearCache');
const menuItem = findMenuItemBuildWindowMenu('Clear cache and Reload');
menuItem.click(item, focusedWindow);
expect(spySession).toBeCalled();
});
});
describe('`buildHelpMenu`', () => {
it('should call `buildHelpMenu` correctly', () => {
const spyFn = 'buildHelpMenu';
const spy = jest.spyOn(appMenu, spyFn);
appMenu.buildMenuKey(menuSections.help);
expect(spy).toBeCalled();
});
it('should call `Symphony Help` correctly', () => {
const spy = jest.spyOn(shell, 'openExternal');
const expectedValue = 'https://support.symphony.com';
const menuItem = findMenuItemBuildHelpMenu('Symphony Help');
menuItem.click();
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `Learn More` correctly', () => {
const spy = jest.spyOn(shell, 'openExternal');
const expectedValue = 'https://support.symphony.com';
const menuItem = findMenuItemBuildHelpMenu('Learn More');
menuItem.click();
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `Show Logs in Finder` correctly', async () => {
const menuItem = findHelpTroubleshootingMenuItem('Show Logs in Finder');
await menuItem.click();
expect(exportLogs).toBeCalled();
});
it('should call `Show crash dump in Finder` correctly', () => {
const menuItem = findHelpTroubleshootingMenuItem('Show crash dump in Finder');
menuItem.click();
expect(exportCrashDumps).toBeCalled();
});
describe(`Toggle Developer Tools`, () => {
it('should call `toggleDevTools` when focusedWindow is not null', () => {
const focusedWindow = {
isDestroyed: jest.fn(() => false),
reload: jest.fn(),
webContents: {
toggleDevTools: jest.fn(),
},
};
const spy = jest.spyOn(focusedWindow.webContents, 'toggleDevTools');
const menuItem = findHelpTroubleshootingMenuItem('Toggle Developer Tools');
menuItem.click({}, focusedWindow);
expect(spy).toBeCalled();
});
it('should not call `electron.dialog` when focusedWindow is null', () => {
const spy = jest.spyOn(dialog, 'showMessageBox');
const focusedWindow = null;
const expectedValue = {
type: 'warning',
buttons: ['Ok'],
title: 'Dev Tools disabled',
message: 'Dev Tools has been disabled. Please contact your system administrator',
};
const menuItem = findHelpTroubleshootingMenuItem('Toggle Developer Tools');
menuItem.click({}, focusedWindow);
expect(spy).not.toBeCalledWith(null, expectedValue);
});
});
it('should not call `electron.dialog` when focusedWindow is null', () => {
const spy = jest.spyOn(dialog, 'showMessageBox');
const focusedWindow = null;
const expectedValue = {
type: 'warning',
buttons: ['Ok'],
title: 'Dev Tools disabled',
message:
'Dev Tools has been disabled. Please contact your system administrator',
};
const menuItem = findHelpTroubleshootingMenuItem(
'Toggle Developer Tools',
);
menuItem.click({}, focusedWindow);
expect(spy).not.toBeCalledWith(null, expectedValue);
});
});
});
});
});

View File

@ -5,74 +5,77 @@ import { app } from './__mocks__/electron';
jest.mock('electron-log');
jest.mock('../src/app/config-handler', () => {
return {
CloudConfigDataTypes: {
NOT_SET: 'NOT_SET',
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
},
config: {
getGlobalConfigFields: jest.fn(() => ''),
getConfigFields: jest.fn(() => {
return {
launchOnStartup: 'ENABLED',
};
}),
updateUserConfig: jest.fn(),
},
};
return {
CloudConfigDataTypes: {
NOT_SET: 'NOT_SET',
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
},
config: {
getGlobalConfigFields: jest.fn(() => ''),
getConfigFields: jest.fn(() => {
return {
launchOnStartup: 'ENABLED',
};
}),
updateUserConfig: jest.fn(),
},
};
});
describe('auto launch controller', async () => {
beforeEach(() => {
jest.spyOn(config, 'getConfigFields').mockImplementation(() => {
return {
launchOnStartup: 'ENABLED',
};
});
jest.clearAllMocks();
beforeEach(() => {
jest.spyOn(config, 'getConfigFields').mockImplementation(() => {
return {
launchOnStartup: 'ENABLED',
};
});
jest.clearAllMocks();
});
it('should call `enableAutoLaunch` correctly', async () => {
const spyFn = 'setLoginItemSettings';
const spy = jest.spyOn(app, spyFn);
await autoLaunchInstance.enableAutoLaunch();
expect(spy).toBeCalled();
});
it('should call `enableAutoLaunch` correctly', async () => {
const spyFn = 'setLoginItemSettings';
const spy = jest.spyOn(app, spyFn);
await autoLaunchInstance.enableAutoLaunch();
expect(spy).toBeCalled();
});
it('should call `disableAutoLaunch` correctly', async () => {
const spyFn = 'setLoginItemSettings';
const spy = jest.spyOn(app, spyFn);
await autoLaunchInstance.disableAutoLaunch();
expect(spy).toBeCalled();
});
it('should call `disableAutoLaunch` correctly', async () => {
const spyFn = 'setLoginItemSettings';
const spy = jest.spyOn(app, spyFn);
await autoLaunchInstance.disableAutoLaunch();
expect(spy).toBeCalled();
});
it('should call `isAutoLaunchEnabled` correctly', async () => {
const spyFn = 'getLoginItemSettings';
const spy = jest.spyOn(app, spyFn);
await autoLaunchInstance.isAutoLaunchEnabled();
expect(spy).toBeCalled();
});
it('should call `isAutoLaunchEnabled` correctly', async () => {
const spyFn = 'getLoginItemSettings';
const spy = jest.spyOn(app, spyFn);
await autoLaunchInstance.isAutoLaunchEnabled();
expect(spy).toBeCalled();
});
it('should enable AutoLaunch when `handleAutoLaunch` is called', async () => {
const spyFn = 'enableAutoLaunch';
const spy = jest.spyOn(autoLaunchInstance, spyFn);
jest.spyOn(autoLaunchInstance,'isAutoLaunchEnabled').mockImplementation(() => false);
await autoLaunchInstance.handleAutoLaunch();
expect(spy).toBeCalled();
});
it('should enable AutoLaunch when `handleAutoLaunch` is called', async () => {
const spyFn = 'enableAutoLaunch';
const spy = jest.spyOn(autoLaunchInstance, spyFn);
jest
.spyOn(autoLaunchInstance, 'isAutoLaunchEnabled')
.mockImplementation(() => false);
await autoLaunchInstance.handleAutoLaunch();
expect(spy).toBeCalled();
});
it('should disable AutoLaunch when `handleAutoLaunch` is called', async () => {
jest.spyOn(config, 'getConfigFields').mockImplementation(() => {
return {
launchOnStartup: 'DISABLED',
};
});
const spyFn = 'disableAutoLaunch';
const spy = jest.spyOn(autoLaunchInstance, spyFn);
jest.spyOn(autoLaunchInstance,'isAutoLaunchEnabled').mockImplementation(() => ({openAtLogin: true}));
await autoLaunchInstance.handleAutoLaunch();
expect(spy).toBeCalled();
it('should disable AutoLaunch when `handleAutoLaunch` is called', async () => {
jest.spyOn(config, 'getConfigFields').mockImplementation(() => {
return {
launchOnStartup: 'DISABLED',
};
});
const spyFn = 'disableAutoLaunch';
const spy = jest.spyOn(autoLaunchInstance, spyFn);
jest
.spyOn(autoLaunchInstance, 'isAutoLaunchEnabled')
.mockImplementation(() => ({ openAtLogin: true }));
await autoLaunchInstance.handleAutoLaunch();
expect(spy).toBeCalled();
});
});

View File

@ -3,73 +3,83 @@ import * as React from 'react';
import BasicAuth from '../src/renderer/components/basic-auth';
describe('basic auth', () => {
const usernameTargetMock: object = { target: { id: 'username', value: 'foo' }};
const passwordTargetMock: object = { target: { id: 'password', value: '123456' }};
const basicAuthMock: object = {
hostname: 'example',
isValidCredentials: true,
password: '123456',
username: 'foo',
};
const usernameMock = { username: 'foo' };
const passwordMock = { password: '123456' };
const usernameTargetMock: object = {
target: { id: 'username', value: 'foo' },
};
const passwordTargetMock: object = {
target: { id: 'password', value: '123456' },
};
const basicAuthMock: object = {
hostname: 'example',
isValidCredentials: true,
password: '123456',
username: 'foo',
};
const usernameMock = { username: 'foo' };
const passwordMock = { password: '123456' };
it('should render correctly', () => {
const wrapper: ShallowWrapper = shallow(React.createElement(BasicAuth));
expect(wrapper).toMatchSnapshot();
it('should render correctly', () => {
const wrapper: ShallowWrapper = shallow(React.createElement(BasicAuth));
expect(wrapper).toMatchSnapshot();
});
it('should call `setState` when `change` is called', () => {
const spy: jest.SpyInstance = jest.spyOn(BasicAuth.prototype, 'setState');
const wrapper: ShallowWrapper = shallow(React.createElement(BasicAuth));
wrapper.find('#username').simulate('change', usernameTargetMock);
expect(spy).lastCalledWith(usernameMock);
wrapper.find('#password').simulate('change', passwordTargetMock);
expect(spy).lastCalledWith(passwordMock);
});
it('should call submit login', () => {
const { ipcRenderer } = require('./__mocks__/electron');
const spy: jest.SpyInstance = jest.spyOn(ipcRenderer, 'send');
const wrapper: ShallowWrapper = shallow(React.createElement(BasicAuth));
wrapper.find('#username').simulate('change', usernameTargetMock);
wrapper.find('#password').simulate('change', passwordTargetMock);
wrapper.find('#basicAuth').simulate('submit');
expect(spy).lastCalledWith('basic-auth-login', {
...usernameMock,
...passwordMock,
});
});
it('should call `basic-auth-closed` event when cancel button is clicked', () => {
const { ipcRenderer } = require('./__mocks__/electron');
const spy: jest.SpyInstance = jest.spyOn(ipcRenderer, 'send');
const wrapper: ShallowWrapper = shallow(React.createElement(BasicAuth));
wrapper.find('#cancel').simulate('click');
expect(spy).lastCalledWith('basic-auth-closed', false);
});
describe('basic auth mount and unmount event', () => {
const { ipcRenderer } = require('./__mocks__/electron');
const basicAuthDataLabel: string = 'basic-auth-data';
const onLabelEvent: string = 'on';
const removeListenerLabelEvent: string = 'removeListener';
it('should call `basic-auth-data` event when component is mounted', () => {
const spy: jest.SpyInstance = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(BasicAuth));
expect(spy).toBeCalledWith(basicAuthDataLabel, expect.any(Function));
});
it('should call `setState` when `change` is called', () => {
const spy: jest.SpyInstance = jest.spyOn(BasicAuth.prototype, 'setState');
const wrapper: ShallowWrapper = shallow(React.createElement(BasicAuth));
wrapper.find('#username').simulate('change', usernameTargetMock);
expect(spy).lastCalledWith(usernameMock);
wrapper.find('#password').simulate('change', passwordTargetMock);
expect(spy).lastCalledWith(passwordMock);
it('should remove listen `basic-auth-data` when component is unmounted', () => {
const spy: jest.SpyInstance = jest.spyOn(
ipcRenderer,
removeListenerLabelEvent,
);
const wrapper: ShallowWrapper = shallow(React.createElement(BasicAuth));
wrapper.unmount();
expect(spy).toBeCalledWith(basicAuthDataLabel, expect.any(Function));
});
it('should call submit login', () => {
const { ipcRenderer } = require('./__mocks__/electron');
const spy: jest.SpyInstance = jest.spyOn(ipcRenderer, 'send');
const wrapper: ShallowWrapper = shallow(React.createElement(BasicAuth));
wrapper.find('#username').simulate('change', usernameTargetMock);
wrapper.find('#password').simulate('change', passwordTargetMock);
wrapper.find('#basicAuth').simulate('submit');
expect(spy).lastCalledWith('basic-auth-login', { ...usernameMock, ...passwordMock });
});
it('should call `basic-auth-closed` event when cancel button is clicked', () => {
const { ipcRenderer } = require('./__mocks__/electron');
const spy: jest.SpyInstance = jest.spyOn(ipcRenderer, 'send');
const wrapper: ShallowWrapper = shallow(React.createElement(BasicAuth));
wrapper.find('#cancel').simulate('click');
expect(spy).lastCalledWith('basic-auth-closed', false);
});
describe('basic auth mount and unmount event', () => {
const { ipcRenderer } = require('./__mocks__/electron');
const basicAuthDataLabel: string = 'basic-auth-data';
const onLabelEvent: string = 'on';
const removeListenerLabelEvent: string = 'removeListener';
it('should call `basic-auth-data` event when component is mounted', () => {
const spy: jest.SpyInstance = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(BasicAuth));
expect(spy).toBeCalledWith(basicAuthDataLabel, expect.any(Function));
});
it('should remove listen `basic-auth-data` when component is unmounted', () => {
const spy: jest.SpyInstance = jest.spyOn(ipcRenderer, removeListenerLabelEvent);
const wrapper: ShallowWrapper = shallow(React.createElement(BasicAuth));
wrapper.unmount();
expect(spy).toBeCalledWith(basicAuthDataLabel, expect.any(Function));
});
it('should call `updateState` when component is mounted', () => {
const spy: jest.SpyInstance = jest.spyOn(BasicAuth.prototype, 'setState');
shallow(React.createElement(BasicAuth));
ipcRenderer.send('basic-auth-data', basicAuthMock);
expect(spy).toBeCalledWith(basicAuthMock);
});
it('should call `updateState` when component is mounted', () => {
const spy: jest.SpyInstance = jest.spyOn(BasicAuth.prototype, 'setState');
shallow(React.createElement(BasicAuth));
ipcRenderer.send('basic-auth-data', basicAuthMock);
expect(spy).toBeCalledWith(basicAuthMock);
});
});
});

View File

@ -5,116 +5,116 @@ import { injectStyles } from '../src/app/window-utils';
import { ipcRenderer } from './__mocks__/electron';
const getMainWindow = {
isDestroyed: jest.fn(() => false),
getBounds: jest.fn(() => {
return {
x: 11,
y: 22,
};
}),
isAlwaysOnTop: jest.fn(() => true),
setMenuBarVisibility: jest.fn(),
setAlwaysOnTop: jest.fn(),
setFullScreenable: jest.fn(),
isDestroyed: jest.fn(() => false),
getBounds: jest.fn(() => {
return {
x: 11,
y: 22,
};
}),
isAlwaysOnTop: jest.fn(() => true),
setMenuBarVisibility: jest.fn(),
setAlwaysOnTop: jest.fn(),
setFullScreenable: jest.fn(),
};
jest.mock('electron-log');
jest.mock('../src/common/env', () => {
return {
isWindowsOS: true,
isLinux: false,
isMac: false,
};
return {
isWindowsOS: true,
isLinux: false,
isMac: false,
};
});
jest.mock('../src/app/window-utils', () => {
return {
injectStyles: jest.fn(),
preventWindowNavigation: jest.fn(),
};
return {
injectStyles: jest.fn(),
preventWindowNavigation: jest.fn(),
};
});
jest.mock('../src/app/window-handler', () => {
return {
windowHandler: {
url: 'https://test.symphony.com',
getMainWindow: jest.fn(() => {
return getMainWindow;
}),
openUrlInDefaultBrowser: jest.fn(),
addWindow: jest.fn(),
},
};
return {
windowHandler: {
url: 'https://test.symphony.com',
getMainWindow: jest.fn(() => {
return getMainWindow;
}),
openUrlInDefaultBrowser: jest.fn(),
addWindow: jest.fn(),
},
};
});
jest.mock('../src/app/window-actions', () => {
return {
monitorWindowActions: jest.fn(),
};
return {
monitorWindowActions: jest.fn(),
};
});
jest.mock('../src/common/logger', () => {
return {
logger: {
setLoggerWindow: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
verbose: jest.fn(),
debug: jest.fn(),
silly: jest.fn(),
},
};
return {
logger: {
setLoggerWindow: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
verbose: jest.fn(),
debug: jest.fn(),
silly: jest.fn(),
},
};
});
describe('child window handle', () => {
const frameName = {};
const disposition = 'new-window';
const newWinOptions = {
webPreferences: jest.fn(),
webContents: { ...ipcRenderer, ...getMainWindow, webContents: ipcRenderer },
};
const frameName = {};
const disposition = 'new-window';
const newWinOptions = {
webPreferences: jest.fn(),
webContents: { ...ipcRenderer, ...getMainWindow, webContents: ipcRenderer },
};
it('should call `did-start-loading` correctly on WindowOS', () => {
const newWinUrl = 'about:blank';
const args = [newWinUrl, frameName, disposition, newWinOptions];
const spy = jest.spyOn(getMainWindow, 'setMenuBarVisibility');
handleChildWindow(ipcRenderer as any);
ipcRenderer.send('new-window', ...args);
ipcRenderer.send('did-start-loading');
expect(spy).toBeCalledWith(false);
});
it('should call `did-start-loading` correctly on WindowOS', () => {
const newWinUrl = 'about:blank';
const args = [newWinUrl, frameName, disposition, newWinOptions];
const spy = jest.spyOn(getMainWindow, 'setMenuBarVisibility');
handleChildWindow(ipcRenderer as any);
ipcRenderer.send('new-window', ...args);
ipcRenderer.send('did-start-loading');
expect(spy).toBeCalledWith(false);
});
it('should call `did-finish-load` correctly on WindowOS', () => {
config.getGlobalConfigFields = jest.fn(() => {
return {
url: 'https://foundation-dev.symphony.com',
};
});
const newWinUrl = 'about:blank';
const args = [newWinUrl, frameName, disposition, newWinOptions];
const spy = jest.spyOn(newWinOptions.webContents.webContents, 'send');
handleChildWindow(ipcRenderer as any);
ipcRenderer.send('new-window', ...args);
ipcRenderer.send('did-finish-load');
expect(spy).lastCalledWith('page-load', {
enableCustomTitleBar: false,
isMainWindow: false,
isWindowsOS: true,
locale: 'en-US',
origin: 'https://foundation-dev.symphony.com',
resources: {},
});
expect(injectStyles).toBeCalled();
it('should call `did-finish-load` correctly on WindowOS', () => {
config.getGlobalConfigFields = jest.fn(() => {
return {
url: 'https://foundation-dev.symphony.com',
};
});
const newWinUrl = 'about:blank';
const args = [newWinUrl, frameName, disposition, newWinOptions];
const spy = jest.spyOn(newWinOptions.webContents.webContents, 'send');
handleChildWindow(ipcRenderer as any);
ipcRenderer.send('new-window', ...args);
ipcRenderer.send('did-finish-load');
expect(spy).lastCalledWith('page-load', {
enableCustomTitleBar: false,
isMainWindow: false,
isWindowsOS: true,
locale: 'en-US',
origin: 'https://foundation-dev.symphony.com',
resources: {},
});
expect(injectStyles).toBeCalled();
});
it('should call `windowHandler.openUrlInDefaultBrowser` when url in invalid', () => {
const newWinUrl = 'invalid';
const args = [newWinUrl, frameName, disposition, newWinOptions];
const spy = jest.spyOn(windowHandler, 'openUrlInDefaultBrowser');
handleChildWindow(ipcRenderer as any);
ipcRenderer.send('new-window', ...args);
expect(spy).not.toBeCalledWith('invalid');
});
it('should call `windowHandler.openUrlInDefaultBrowser` when url in invalid', () => {
const newWinUrl = 'invalid';
const args = [newWinUrl, frameName, disposition, newWinOptions];
const spy = jest.spyOn(windowHandler, 'openUrlInDefaultBrowser');
handleChildWindow(ipcRenderer as any);
ipcRenderer.send('new-window', ...args);
expect(spy).not.toBeCalledWith('invalid');
});
});

View File

@ -4,174 +4,178 @@ import { isDevEnv, isLinux, isMac, isWindowsOS } from '../src/common/env';
import { app } from './__mocks__/electron';
jest.mock('../src/app/config-handler', () => {
return {
CloudConfigDataTypes: {
NOT_SET: 'NOT_SET',
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
},
config: {
getConfigFields: jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
disableThrottling: 'DISABLED',
},
disableGpu: true,
};
}),
getCloudConfigFields: jest.fn(() => {
return {
disableThrottling: 'DISABLED',
};
}),
},
};
return {
CloudConfigDataTypes: {
NOT_SET: 'NOT_SET',
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
},
config: {
getConfigFields: jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
disableThrottling: 'DISABLED',
},
disableGpu: true,
};
}),
getCloudConfigFields: jest.fn(() => {
return {
disableThrottling: 'DISABLED',
};
}),
},
};
});
jest.mock('../src/common/utils', () => {
return {
getCommandLineArgs: jest.fn(),
compareVersions: jest.fn(),
};
return {
getCommandLineArgs: jest.fn(),
compareVersions: jest.fn(),
};
});
jest.mock('electron-log');
describe('chrome flags', () => {
beforeEach(() => {
(isDevEnv as any) = false;
(isMac as any) = true;
(isWindowsOS as any) = false;
(isLinux as any) = false;
config.getConfigFields = jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
},
disableGpu: true,
};
});
jest.clearAllMocks();
});
it('should call `setChromeFlags` correctly', () => {
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(1, 'auth-negotiate-delegate-whitelist', 'url');
expect(spy).nthCalledWith(2, 'auth-server-whitelist', 'whitelist');
expect(spy).nthCalledWith(3, 'disable-background-timer-throttling', 'true');
expect(spy).nthCalledWith(4, 'disable-d3d11', true);
expect(spy).nthCalledWith(5, 'disable-gpu', true);
expect(spy).nthCalledWith(6, 'disable-gpu-compositing', true);
});
it('should call `setChromeFlags` correctly when `disableGpu` is false', () => {
config.getConfigFields = jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
},
};
});
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(1, 'auth-negotiate-delegate-whitelist', 'url');
expect(spy).nthCalledWith(2, 'auth-server-whitelist', 'whitelist');
expect(spy).nthCalledWith(3, 'disable-background-timer-throttling', 'true');
expect(spy).not.nthCalledWith(4);
});
it('should set `disable-renderer-backgrounding` chrome flag correctly when cloud config is ENABLED', () => {
config.getConfigFields = jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
disableGpu: false,
disableThrottling: 'ENABLED',
},
};
});
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(5, 'disable-renderer-backgrounding', 'true');
expect(spy).not.nthCalledWith(6);
});
it('should set `disable-renderer-backgrounding` chrome flag correctly when cloud config PMP setting is ENABLED', () => {
config.getCloudConfigFields = jest.fn(() => {
return {
disableThrottling: 'ENABLED',
};
});
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(8, 'disable-renderer-backgrounding', 'true');
expect(spy).not.nthCalledWith(9);
});
it('should set `disable-renderer-backgrounding` chrome flag when any one is ENABLED ', () => {
config.getConfigFields = jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
disableGpu: false,
disableThrottling: 'DISABLED',
},
};
});
config.getCloudConfigFields = jest.fn(() => {
return {
disableThrottling: 'ENABLED',
};
});
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(5, 'disable-renderer-backgrounding', 'true');
expect(spy).not.nthCalledWith(6);
});
it('should set `disable-renderer-backgrounding` chrome flag when PMP is ENABLED', () => {
config.getConfigFields = jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
disableGpu: false,
disableThrottling: 'ENABLED',
},
};
});
config.getCloudConfigFields = jest.fn(() => {
return {
disableThrottling: 'DISABLED',
};
});
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(5, 'disable-renderer-backgrounding', 'true');
expect(spy).not.nthCalledWith(6);
});
describe('`isDevEnv`', () => {
beforeEach(() => {
(isDevEnv as any) = false;
(isMac as any) = true;
(isWindowsOS as any) = false;
(isLinux as any) = false;
config.getConfigFields = jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
},
disableGpu: true,
};
});
jest.clearAllMocks();
(isDevEnv as any) = true;
});
it('should call `setChromeFlags` correctly', () => {
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(1, 'auth-negotiate-delegate-whitelist', 'url');
expect(spy).nthCalledWith(2, 'auth-server-whitelist', 'whitelist');
expect(spy).nthCalledWith(3, 'disable-background-timer-throttling', 'true');
expect(spy).nthCalledWith(4, 'disable-d3d11', true);
expect(spy).nthCalledWith(5, 'disable-gpu', true);
expect(spy).nthCalledWith(6, 'disable-gpu-compositing', true);
});
it('should call `setChromeFlags` correctly when `disableGpu` is false', () => {
config.getConfigFields = jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
},
};
});
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(1, 'auth-negotiate-delegate-whitelist', 'url');
expect(spy).nthCalledWith(2, 'auth-server-whitelist', 'whitelist');
expect(spy).nthCalledWith(3, 'disable-background-timer-throttling', 'true');
expect(spy).not.nthCalledWith(4);
});
it('should set `disable-renderer-backgrounding` chrome flag correctly when cloud config is ENABLED', () => {
config.getConfigFields = jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
disableGpu: false,
disableThrottling: 'ENABLED',
},
};
});
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(5, 'disable-renderer-backgrounding', 'true');
expect(spy).not.nthCalledWith(6);
});
it('should set `disable-renderer-backgrounding` chrome flag correctly when cloud config PMP setting is ENABLED', () => {
config.getCloudConfigFields = jest.fn(() => {
return {
disableThrottling: 'ENABLED',
};
});
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(8, 'disable-renderer-backgrounding', 'true');
expect(spy).not.nthCalledWith(9);
});
it('should set `disable-renderer-backgrounding` chrome flag when any one is ENABLED ', () => {
config.getConfigFields = jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
disableGpu: false,
disableThrottling: 'DISABLED',
},
};
});
config.getCloudConfigFields = jest.fn(() => {
return {
disableThrottling: 'ENABLED',
};
});
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(5, 'disable-renderer-backgrounding', 'true');
expect(spy).not.nthCalledWith(6);
});
it('should set `disable-renderer-backgrounding` chrome flag when PMP is ENABLED', () => {
config.getConfigFields = jest.fn(() => {
return {
customFlags: {
authServerWhitelist: 'url',
authNegotiateDelegateWhitelist: 'whitelist',
disableGpu: false,
disableThrottling: 'ENABLED',
},
};
});
config.getCloudConfigFields = jest.fn(() => {
return {
disableThrottling: 'DISABLED',
};
});
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(5, 'disable-renderer-backgrounding', 'true');
expect(spy).not.nthCalledWith(6);
});
describe('`isDevEnv`', () => {
beforeEach(() => {
(isDevEnv as any) = true;
});
it('should call `setChromeFlags` correctly', () => {
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(1, 'auth-negotiate-delegate-whitelist', 'url');
expect(spy).nthCalledWith(2, 'auth-server-whitelist', 'whitelist');
expect(spy).nthCalledWith(3, 'disable-background-timer-throttling', 'true');
expect(spy).nthCalledWith(4, 'disable-d3d11', true);
expect(spy).nthCalledWith(5, 'disable-gpu', true);
expect(spy).nthCalledWith(6, 'disable-gpu-compositing', true);
});
const spy = jest.spyOn(app.commandLine, 'appendSwitch');
setChromeFlags();
expect(spy).nthCalledWith(1, 'auth-negotiate-delegate-whitelist', 'url');
expect(spy).nthCalledWith(2, 'auth-server-whitelist', 'whitelist');
expect(spy).nthCalledWith(
3,
'disable-background-timer-throttling',
'true',
);
expect(spy).nthCalledWith(4, 'disable-d3d11', true);
expect(spy).nthCalledWith(5, 'disable-gpu', true);
expect(spy).nthCalledWith(6, 'disable-gpu-compositing', true);
});
});
});

View File

@ -3,47 +3,50 @@ import * as React from 'react';
import ColorPickerPill from '../src/renderer/components/color-picker-pill';
const defaultProps = {
availableColors: [
{ rgbaColor: 'rgba(0, 142, 255, 0.64)' },
{ rgbaColor: 'rgba(38, 196, 58, 0.64)' },
{ rgbaColor: 'rgba(246, 178, 2, 0.64)' },
{ rgbaColor: 'rgba(233, 0, 0, 0.64)' },
],
onChange: jest.fn(),
availableColors: [
{ rgbaColor: 'rgba(0, 142, 255, 0.64)' },
{ rgbaColor: 'rgba(38, 196, 58, 0.64)' },
{ rgbaColor: 'rgba(246, 178, 2, 0.64)' },
{ rgbaColor: 'rgba(233, 0, 0, 0.64)' },
],
onChange: jest.fn(),
};
describe('<ColorPickerPill/>', () => {
it('should render correctly', () => {
const wrapper = shallow(<ColorPickerPill {...defaultProps} />);
expect(wrapper).toMatchSnapshot();
});
it('should render correctly', () => {
const wrapper = shallow(<ColorPickerPill {...defaultProps} />);
expect(wrapper).toMatchSnapshot();
});
it('should call onChange when clicked on a color dot', () => {
const wrapper = shallow(<ColorPickerPill {...defaultProps} />);
wrapper
.find('[data-testid="colorDot rgba(233, 0, 0, 0.64)"]')
.simulate('click');
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
});
it('should call onChange when clicked on a color dot', () => {
const wrapper = shallow(<ColorPickerPill {...defaultProps} />);
wrapper
.find('[data-testid="colorDot rgba(233, 0, 0, 0.64)"]')
.simulate('click');
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
});
it('should render chosen dots as larger', () => {
const chosenColorProps = {
availableColors: [{ rgbaColor: 'rgba(0, 0, 0, 0.64)', chosen: true }, { rgbaColor: 'rgba(233, 0, 0, 0.64)' },
],
onChange: jest.fn(),
};
const wrapper = shallow(<ColorPickerPill {...chosenColorProps} />);
expect(wrapper).toMatchSnapshot();
});
it('should render chosen dots as larger', () => {
const chosenColorProps = {
availableColors: [
{ rgbaColor: 'rgba(0, 0, 0, 0.64)', chosen: true },
{ rgbaColor: 'rgba(233, 0, 0, 0.64)' },
],
onChange: jest.fn(),
};
const wrapper = shallow(<ColorPickerPill {...chosenColorProps} />);
expect(wrapper).toMatchSnapshot();
});
it('should render outlined dot if provided in props', () => {
const outlinedColorProps = {
availableColors: [{ rgbaColor: 'rgba(246, 178, 2, 1)' },
{ rgbaColor: 'rgba(255, 255, 255, 1)', outline: 'rgba(0, 0, 0, 1)' },
],
onChange: jest.fn(),
};
const wrapper = shallow(<ColorPickerPill {...outlinedColorProps} />);
expect(wrapper).toMatchSnapshot();
});
it('should render outlined dot if provided in props', () => {
const outlinedColorProps = {
availableColors: [
{ rgbaColor: 'rgba(246, 178, 2, 1)' },
{ rgbaColor: 'rgba(255, 255, 255, 1)', outline: 'rgba(0, 0, 0, 1)' },
],
onChange: jest.fn(),
};
const wrapper = shallow(<ColorPickerPill {...outlinedColorProps} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -6,125 +6,130 @@ import { IConfig, IGlobalConfig } from '../src/app/config-handler';
jest.mock('electron-log');
describe('config', () => {
const configFileName: string = 'Symphony.config';
let userConfigDir: string;
let globalConfigDir: string;
let configInstance: any;
const configFileName: string = 'Symphony.config';
let userConfigDir: string;
let globalConfigDir: string;
let configInstance: any;
const createTempGlobalConfig = (fileName: string): string => {
const tmpDir = os.tmpdir();
return path.join(fs.mkdtempSync(path.join(tmpDir, 'config-')), fileName);
};
const createTempGlobalConfig = (fileName: string): string => {
const tmpDir = os.tmpdir();
return path.join(fs.mkdtempSync(path.join(tmpDir, 'config-')), fileName);
};
const createTempUserConfig = (fileName: string): string => {
const tmpDir = os.tmpdir();
return path.join(fs.mkdtempSync(path.join(tmpDir, 'user-')), fileName);
};
const createTempUserConfig = (fileName: string): string => {
const tmpDir = os.tmpdir();
return path.join(fs.mkdtempSync(path.join(tmpDir, 'user-')), fileName);
};
// write data in global config created
const writeConfigFile = (data: object): void => {
fs.writeFileSync(globalConfigDir, JSON.stringify(data));
};
// write data in global config created
const writeConfigFile = (data: object): void => {
fs.writeFileSync(globalConfigDir, JSON.stringify(data));
};
// write data in user config created
const writeUserFile = (data: object): void => {
fs.writeFileSync(userConfigDir, JSON.stringify(data));
};
// write data in user config created
const writeUserFile = (data: object): void => {
fs.writeFileSync(userConfigDir, JSON.stringify(data));
};
beforeEach(() => {
const { config } = require('../src/app/config-handler');
beforeEach(() => {
const { config } = require('../src/app/config-handler');
globalConfigDir = createTempGlobalConfig(configFileName);
userConfigDir = createTempUserConfig(configFileName);
globalConfigDir = createTempGlobalConfig(configFileName);
userConfigDir = createTempUserConfig(configFileName);
configInstance = config;
jest.resetModules();
configInstance = config;
jest.resetModules();
});
describe('getConfigFields', () => {
it('should fail when field not present in either user or global config', () => {
const fieldMock: string[] = ['no-url'];
const globalConfig: object = { url: 'test' };
const userConfig: object = { configVersion: '4.0.1' };
// creating temp file
writeConfigFile(globalConfig);
writeUserFile(userConfig);
// changing path from /Users/.../SymphonyElectron/config/Symphony.config to temp path
configInstance.globalConfigPath = globalConfigDir;
configInstance.userConfigPath = userConfigDir;
configInstance.readGlobalConfig();
configInstance.readUserConfig();
const configField: IConfig = configInstance.getConfigFields(fieldMock);
expect(configField).toEqual({});
});
describe('getConfigFields', () => {
it('should fail when field not present in either user or global config', () => {
const fieldMock: string[] = ['no-url'];
const globalConfig: object = { url: 'test' };
const userConfig: object = { configVersion: '4.0.1' };
it('should succeed when only present in user config', () => {
const fieldMock: string[] = ['url'];
const globalConfig: object = { url: 'something' };
const userConfig: object = { configVersion: '4.0.1' };
// creating temp file
writeConfigFile(globalConfig);
writeUserFile(userConfig);
writeConfigFile(globalConfig);
writeUserFile(userConfig);
// changing path from /Users/.../SymphonyElectron/config/Symphony.config to temp path
configInstance.globalConfigPath = globalConfigDir;
configInstance.userConfigPath = userConfigDir;
configInstance.readGlobalConfig();
configInstance.readUserConfig();
configInstance.globalConfigPath = globalConfigDir;
configInstance.userConfigPath = userConfigDir;
const configField: IConfig = configInstance.getConfigFields(fieldMock);
expect(configField).toEqual({});
});
configInstance.readUserConfig();
configInstance.readGlobalConfig();
it('should succeed when only present in user config', () => {
const fieldMock: string[] = ['url'];
const globalConfig: object = { url: 'something' };
const userConfig: object = { configVersion: '4.0.1' };
const configField: IGlobalConfig = configInstance.getGlobalConfigFields(
fieldMock,
);
writeConfigFile(globalConfig);
writeUserFile(userConfig);
configInstance.globalConfigPath = globalConfigDir;
configInstance.userConfigPath = userConfigDir;
configInstance.readUserConfig();
configInstance.readGlobalConfig();
const configField: IGlobalConfig = configInstance.getGlobalConfigFields(fieldMock);
expect(configField.url).toBe('something');
});
it('should fail when config path is invalid', () => {
const userConfig: object = { url: 'test' };
let isInvalidPath: boolean = false;
writeConfigFile(userConfig);
configInstance.globalConfigPath = '//';
try {
configInstance.readGlobalConfig();
} catch (e) {
isInvalidPath = true;
}
expect(isInvalidPath).toBeTruthy();
});
expect(configField.url).toBe('something');
});
describe('updateConfig', () => {
it('should overwrite existing field', () => {
const userConfig: object = { configVersion: '4.0.0' };
const overwriteUserConfig: object = { configVersion: '4.0.1' };
writeUserFile(userConfig);
it('should fail when config path is invalid', () => {
const userConfig: object = { url: 'test' };
let isInvalidPath: boolean = false;
writeConfigFile(userConfig);
configInstance.userConfigPath = userConfigDir;
configInstance.readUserConfig();
configInstance.updateUserConfig(overwriteUserConfig);
expect(configInstance.userConfig.configVersion).toBe('4.0.1');
});
it('should add new field', () => {
const userConfig: object = { configVersion: '4.0.0' };
const newValue: object = { test: 'test' };
writeUserFile(userConfig);
configInstance.userConfigPath = userConfigDir;
configInstance.readUserConfig();
configInstance.updateUserConfig(newValue);
expect(configInstance.userConfig).toEqual({ configVersion: '4.0.0', test: 'test' });
});
it('should fail when invalid path is used', () => {
configInstance.userConfigPath = '//';
return expect(configInstance.readUserConfig()).rejects.toBeTruthy();
});
configInstance.globalConfigPath = '//';
try {
configInstance.readGlobalConfig();
} catch (e) {
isInvalidPath = true;
}
expect(isInvalidPath).toBeTruthy();
});
});
describe('updateConfig', () => {
it('should overwrite existing field', () => {
const userConfig: object = { configVersion: '4.0.0' };
const overwriteUserConfig: object = { configVersion: '4.0.1' };
writeUserFile(userConfig);
configInstance.userConfigPath = userConfigDir;
configInstance.readUserConfig();
configInstance.updateUserConfig(overwriteUserConfig);
expect(configInstance.userConfig.configVersion).toBe('4.0.1');
});
it('should add new field', () => {
const userConfig: object = { configVersion: '4.0.0' };
const newValue: object = { test: 'test' };
writeUserFile(userConfig);
configInstance.userConfigPath = userConfigDir;
configInstance.readUserConfig();
configInstance.updateUserConfig(newValue);
expect(configInstance.userConfig).toEqual({
configVersion: '4.0.0',
test: 'test',
});
});
it('should fail when invalid path is used', () => {
configInstance.userConfigPath = '//';
return expect(configInstance.readUserConfig()).rejects.toBeTruthy();
});
});
});

View File

@ -1,110 +1,158 @@
import { showLoadFailure, showNetworkConnectivityError } from '../src/app/dialog-handler';
import {
showLoadFailure,
showNetworkConnectivityError,
} from '../src/app/dialog-handler';
import { windowHandler } from '../src/app/window-handler';
import { BrowserWindow, dialog, ipcRenderer } from './__mocks__/electron';
jest.mock('../src/app/window-handler', () => {
return {
windowHandler: {
createBasicAuthWindow: jest.fn(),
},
};
return {
windowHandler: {
createBasicAuthWindow: jest.fn(),
},
};
});
jest.mock('../src/renderer/notification', () => {
return {
setupNotificationPosition: jest.fn(),
};
return {
setupNotificationPosition: jest.fn(),
};
});
jest.mock('electron-log');
describe('dialog handler', () => {
const callbackMocked = jest.fn();
const webContentsMocked = jest.fn();
const callbackMocked = jest.fn();
const webContentsMocked = jest.fn();
beforeEach(() => {
jest.clearAllMocks().resetModules();
beforeEach(() => {
jest.clearAllMocks().resetModules();
});
describe('events', () => {
it('should call login correctly', () => {
const spy = jest.spyOn(windowHandler, 'createBasicAuthWindow');
const requestMocked = {
url: 'https://symphony.corporate.com/',
};
const authInfoMocked = {
host: 'symphony.com',
};
ipcRenderer.send(
'login',
webContentsMocked,
requestMocked,
authInfoMocked,
callbackMocked,
);
expect(spy).toBeCalledWith(
webContentsMocked,
'symphony.com',
true,
expect.any(Function),
callbackMocked,
);
});
describe('events', () => {
it('should call login correctly', () => {
const spy = jest.spyOn(windowHandler, 'createBasicAuthWindow');
const requestMocked = {
url: 'https://symphony.corporate.com/',
};
const authInfoMocked = {
host: 'symphony.com',
};
ipcRenderer.send('login', webContentsMocked, requestMocked, authInfoMocked, callbackMocked);
expect(spy).toBeCalledWith(webContentsMocked, 'symphony.com', true, expect.any(Function), callbackMocked);
describe('certificate-error', () => {
const urlMocked = 'https://symphony.corporate.com/';
const errorMocked = 'check for server certificate revocation';
const certificate = null;
it('should return false when buttonId is 1', async (done) => {
BrowserWindow.fromWebContents = jest.fn(() => {
return { isDestroyed: jest.fn(() => false) };
});
describe('certificate-error', () => {
const urlMocked = 'https://symphony.corporate.com/';
const errorMocked = 'check for server certificate revocation';
const certificate = null;
it('should return false when buttonId is 1', async (done) => {
BrowserWindow.fromWebContents = jest.fn(() => {
return { isDestroyed: jest.fn(() => false) };
});
dialog.showMessageBox = jest.fn(() => {
return { response: 1 };
});
await ipcRenderer.send('certificate-error', webContentsMocked, urlMocked, errorMocked, certificate, callbackMocked);
done(expect(callbackMocked).toBeCalledWith(false));
});
it('should return true when buttonId is not 1', async (done) => {
BrowserWindow.fromWebContents = jest.fn(() => {
return { isDestroyed: jest.fn(() => false) };
});
dialog.showMessageBox = jest.fn(() => 2);
await ipcRenderer.send('certificate-error', webContentsMocked, urlMocked, errorMocked, certificate, callbackMocked);
expect(callbackMocked).toBeCalledWith(true);
await ipcRenderer.send('certificate-error', webContentsMocked, urlMocked, errorMocked, certificate, callbackMocked);
done(expect(callbackMocked).toBeCalledWith(true));
});
dialog.showMessageBox = jest.fn(() => {
return { response: 1 };
});
});
await ipcRenderer.send(
'certificate-error',
webContentsMocked,
urlMocked,
errorMocked,
certificate,
callbackMocked,
);
done(expect(callbackMocked).toBeCalledWith(false));
});
it('should call `showLoadFailure` correctly', () => {
const spyFn = 'showMessageBox';
const spy = jest.spyOn(dialog, spyFn);
const browserWindowMocked: any = { id: 123 };
const urlMocked = 'test';
const errorDescMocked = 'error';
const errorCodeMocked = 404;
const showDialogMocked = true;
const expectedValue = {
type: 'error',
buttons: ['Reload', 'Ignore'],
defaultId: 0,
cancelId: 1,
noLink: true,
title: 'Loading Error',
message: `Error loading URL:\n${urlMocked}\n\n${errorDescMocked}\n\nError Code: ${errorCodeMocked}`,
};
showLoadFailure(browserWindowMocked, urlMocked, errorDescMocked, errorCodeMocked, callbackMocked, showDialogMocked);
expect(spy).toBeCalledWith({ id: 123 }, expectedValue);
it('should return true when buttonId is not 1', async (done) => {
BrowserWindow.fromWebContents = jest.fn(() => {
return { isDestroyed: jest.fn(() => false) };
});
dialog.showMessageBox = jest.fn(() => 2);
await ipcRenderer.send(
'certificate-error',
webContentsMocked,
urlMocked,
errorMocked,
certificate,
callbackMocked,
);
expect(callbackMocked).toBeCalledWith(true);
await ipcRenderer.send(
'certificate-error',
webContentsMocked,
urlMocked,
errorMocked,
certificate,
callbackMocked,
);
done(expect(callbackMocked).toBeCalledWith(true));
});
});
});
it('should call `showNetworkConnectivityError` correctly', () => {
const spyFn = 'showMessageBox';
const spy = jest.spyOn(dialog, spyFn);
const browserWindowMocked: any = { id: 123 };
const urlMocked = 'test';
const errorDescMocked = 'Network connectivity has been lost. Check your internet connection.';
const expectedValue = {
type: 'error',
buttons: ['Reload', 'Ignore'],
defaultId: 0,
cancelId: 1,
noLink: true,
title: 'Loading Error',
message: `Error loading URL:\n${urlMocked}\n\n${errorDescMocked}`,
};
showNetworkConnectivityError(browserWindowMocked, urlMocked, callbackMocked);
expect(spy).toBeCalledWith({ id: 123 }, expectedValue);
});
it('should call `showLoadFailure` correctly', () => {
const spyFn = 'showMessageBox';
const spy = jest.spyOn(dialog, spyFn);
const browserWindowMocked: any = { id: 123 };
const urlMocked = 'test';
const errorDescMocked = 'error';
const errorCodeMocked = 404;
const showDialogMocked = true;
const expectedValue = {
type: 'error',
buttons: ['Reload', 'Ignore'],
defaultId: 0,
cancelId: 1,
noLink: true,
title: 'Loading Error',
message: `Error loading URL:\n${urlMocked}\n\n${errorDescMocked}\n\nError Code: ${errorCodeMocked}`,
};
showLoadFailure(
browserWindowMocked,
urlMocked,
errorDescMocked,
errorCodeMocked,
callbackMocked,
showDialogMocked,
);
expect(spy).toBeCalledWith({ id: 123 }, expectedValue);
});
it('should call `showNetworkConnectivityError` correctly', () => {
const spyFn = 'showMessageBox';
const spy = jest.spyOn(dialog, spyFn);
const browserWindowMocked: any = { id: 123 };
const urlMocked = 'test';
const errorDescMocked =
'Network connectivity has been lost. Check your internet connection.';
const expectedValue = {
type: 'error',
buttons: ['Reload', 'Ignore'],
defaultId: 0,
cancelId: 1,
noLink: true,
title: 'Loading Error',
message: `Error loading URL:\n${urlMocked}\n\n${errorDescMocked}`,
};
showNetworkConnectivityError(
browserWindowMocked,
urlMocked,
callbackMocked,
);
expect(spy).toBeCalledWith({ id: 123 }, expectedValue);
});
});

View File

@ -1,54 +1,55 @@
jest.mock('electron-log');
jest.mock('../src/app/window-handler', () => {
return {
windowHandler: {
setIsAutoReload: jest.fn(() => true),
},
};
return {
windowHandler: {
setIsAutoReload: jest.fn(() => true),
},
};
});
jest.mock('../src/app/window-utils', () => {
return {
windowExists: jest.fn(() => true),
};
return {
windowExists: jest.fn(() => true),
};
});
describe('download handler', () => {
let downloadHandlerInstance;
let downloadHandlerInstance;
beforeEach(() => {
jest.resetModules();
// I did it for reset module imported between tests
const { downloadHandler } = require('../src/app/download-handler');
downloadHandlerInstance = downloadHandler;
});
beforeEach(() => {
jest.resetModules();
// I did it for reset module imported between tests
const { downloadHandler } = require('../src/app/download-handler');
downloadHandlerInstance = downloadHandler;
});
afterAll((done) => {
done();
});
afterAll((done) => {
done();
});
it('should call `sendDownloadCompleted` when download succeeds', () => {
const spy: jest.SpyInstance = jest.spyOn(downloadHandlerInstance, 'sendDownloadCompleted')
.mockImplementation(() => jest.fn());
it('should call `sendDownloadCompleted` when download succeeds', () => {
const spy: jest.SpyInstance = jest
.spyOn(downloadHandlerInstance, 'sendDownloadCompleted')
.mockImplementation(() => jest.fn());
const data: any = {
_id: '121312-123912321-1231231',
savedPath: '/abc/def/123.txt',
total: '1234556',
fileName: 'Test.txt',
};
const data: any = {
_id: '121312-123912321-1231231',
savedPath: '/abc/def/123.txt',
total: '1234556',
fileName: 'Test.txt',
};
downloadHandlerInstance.onDownloadSuccess(data);
expect(spy).toBeCalled();
});
downloadHandlerInstance.onDownloadSuccess(data);
expect(spy).toBeCalled();
});
it('should call `sendDownloadFailed` when download fails', () => {
const spy: jest.SpyInstance = jest.spyOn(downloadHandlerInstance, 'sendDownloadFailed')
.mockImplementation(() => jest.fn());
downloadHandlerInstance.onDownloadFailed();
expect(spy).toBeCalled();
});
it('should call `sendDownloadFailed` when download fails', () => {
const spy: jest.SpyInstance = jest
.spyOn(downloadHandlerInstance, 'sendDownloadFailed')
.mockImplementation(() => jest.fn());
downloadHandlerInstance.onDownloadFailed();
expect(spy).toBeCalled();
});
});

View File

@ -1,97 +1,104 @@
import * as os from 'os';
jest.mock('../src/common/utils.ts', () => {
return {
getCommandLineArgs: jest.fn(() => os.tmpdir()),
};
return {
getCommandLineArgs: jest.fn(() => os.tmpdir()),
};
});
jest.mock('../src/common/env', () => {
return {
isElectronQA: false,
};
return {
isElectronQA: false,
};
});
jest.mock('electron-log');
describe('logger', () => {
let instance;
beforeEach(() => {
// I did it for reset module imported between tests
const { logger } = require('../src/common/logger');
instance = logger;
jest.resetModules();
});
let instance;
beforeEach(() => {
// I did it for reset module imported between tests
const { logger } = require('../src/common/logger');
instance = logger;
jest.resetModules();
});
it('when no logger registered then queue items', () => {
instance.debug('test');
instance.debug('test2');
const queue = instance.getLogQueue();
expect(queue.length).toBe(2);
});
it('when no logger registered then queue items', () => {
instance.debug('test');
instance.debug('test2');
const queue = instance.getLogQueue();
expect(queue.length).toBe(2);
});
it('should call send when logger get registered', () => {
instance.debug('test');
instance.debug('test2');
it('should call send when logger get registered', () => {
instance.debug('test');
instance.debug('test2');
const mock = jest.fn<Electron.WebContents>(() => ({
send: jest.fn(),
}));
const mockWin = new mock();
instance.setLoggerWindow(mockWin);
expect(mockWin.send).toHaveBeenCalled();
});
const mock = jest.fn<Electron.WebContents>(() => ({
send: jest.fn(),
}));
const mockWin = new mock();
instance.setLoggerWindow(mockWin);
expect(mockWin.send).toHaveBeenCalled();
});
it('should call `logger.error` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.error('test error', { error: 'test error' });
expect(spy).toBeCalledWith('error', 'test error', [{ error: 'test error' }]);
});
it('should call `logger.error` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.error('test error', { error: 'test error' });
expect(spy).toBeCalledWith('error', 'test error', [
{ error: 'test error' },
]);
});
it('should call `logger.warn` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.warn('test warn', { warn: 'test warn' });
expect(spy).toBeCalledWith('warn', 'test warn', [{ warn: 'test warn' }]);
});
it('should call `logger.warn` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.warn('test warn', { warn: 'test warn' });
expect(spy).toBeCalledWith('warn', 'test warn', [{ warn: 'test warn' }]);
});
it('should call `logger.debug` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.debug('test debug', { debug: 'test debug' });
expect(spy).toBeCalledWith('debug', 'test debug', [{ debug: 'test debug' }]);
});
it('should call `logger.debug` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.debug('test debug', { debug: 'test debug' });
expect(spy).toBeCalledWith('debug', 'test debug', [
{ debug: 'test debug' },
]);
});
it('should call `logger.info` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.info('test info', { info: 'test info' });
expect(spy).toBeCalledWith('info', 'test info', [{ info: 'test info' }]);
});
it('should call `logger.info` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.info('test info', { info: 'test info' });
expect(spy).toBeCalledWith('info', 'test info', [{ info: 'test info' }]);
});
it('should call `logger.verbose` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.verbose('test verbose', { verbose: 'test verbose' });
expect(spy).toBeCalledWith('verbose', 'test verbose', [{ verbose: 'test verbose' }]);
});
it('should call `logger.verbose` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.verbose('test verbose', { verbose: 'test verbose' });
expect(spy).toBeCalledWith('verbose', 'test verbose', [
{ verbose: 'test verbose' },
]);
});
it('should call `logger.silly` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.silly('test silly', { silly: 'test silly' });
expect(spy).toBeCalledWith('silly', 'test silly', [{ silly: 'test silly' }]);
});
it('should call `logger.silly` correctly', () => {
const spy = jest.spyOn(instance, 'log');
instance.silly('test silly', { silly: 'test silly' });
expect(spy).toBeCalledWith('silly', 'test silly', [
{ silly: 'test silly' },
]);
});
it('should call `logger.sendToCloud` when `logger.debug` is called', () => {
const spyLog = jest.spyOn(instance, 'log');
const spySendToCloud = jest.spyOn(instance, 'sendToCloud');
instance.debug('test');
expect(spyLog).toBeCalled();
expect(spySendToCloud).toBeCalled();
});
it('should cap at 100 queued log messages', () => {
for (let i = 0; i < 110; i++) {
instance.debug('test' + i);
}
const queue = instance.getLogQueue();
expect(queue.length).toBe(100);
});
it('should call `logger.sendToCloud` when `logger.debug` is called', () => {
const spyLog = jest.spyOn(instance, 'log');
const spySendToCloud = jest.spyOn(instance, 'sendToCloud');
instance.debug('test');
expect(spyLog).toBeCalled();
expect(spySendToCloud).toBeCalled();
});
it('should cap at 100 queued log messages', () => {
for (let i = 0; i < 110; i++) {
instance.debug('test' + i);
}
const queue = instance.getLogQueue();
expect(queue.length).toBe(100);
});
});

View File

@ -13,417 +13,421 @@ import { BrowserWindow, ipcMain } from './__mocks__/electron';
jest.mock('electron-log');
jest.mock('../src/app/protocol-handler', () => {
return {
protocolHandler: {
setPreloadWebContents: jest.fn(),
},
};
return {
protocolHandler: {
setPreloadWebContents: jest.fn(),
},
};
});
jest.mock('../src/app/screen-snippet-handler', () => {
return {
screenSnippet: {
capture: jest.fn(),
},
};
return {
screenSnippet: {
capture: jest.fn(),
},
};
});
jest.mock('../src/app/window-actions', () => {
return {
activate: jest.fn(),
handleKeyPress: jest.fn(),
};
return {
activate: jest.fn(),
handleKeyPress: jest.fn(),
};
});
jest.mock('../src/app/window-handler', () => {
return {
windowHandler: {
closeWindow: jest.fn(),
createNotificationSettingsWindow: jest.fn(),
createScreenPickerWindow: jest.fn(),
createScreenSharingIndicatorWindow: jest.fn(),
isOnline: false,
updateVersionInfo: jest.fn(),
isMana: false,
},
};
return {
windowHandler: {
closeWindow: jest.fn(),
createNotificationSettingsWindow: jest.fn(),
createScreenPickerWindow: jest.fn(),
createScreenSharingIndicatorWindow: jest.fn(),
isOnline: false,
updateVersionInfo: jest.fn(),
isMana: false,
},
};
});
jest.mock('../src/app/window-utils', () => {
return {
downloadManagerAction: jest.fn(),
isValidWindow: jest.fn(() => true),
sanitize: jest.fn(),
setDataUrl: jest.fn(),
showBadgeCount: jest.fn(),
showPopupMenu: jest.fn(),
updateLocale: jest.fn(),
windowExists: jest.fn( () => true),
};
return {
downloadManagerAction: jest.fn(),
isValidWindow: jest.fn(() => true),
sanitize: jest.fn(),
setDataUrl: jest.fn(),
showBadgeCount: jest.fn(),
showPopupMenu: jest.fn(),
updateLocale: jest.fn(),
windowExists: jest.fn(() => true),
};
});
jest.mock('../src/common/logger', () => {
return {
logger: {
setLoggerWindow: jest.fn(),
error: jest.fn(),
},
};
return {
logger: {
setLoggerWindow: jest.fn(),
error: jest.fn(),
},
};
});
jest.mock('../src/app/config-handler', () => {
return {
CloudConfigDataTypes: {
NOT_SET: 'NOT_SET',
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
},
config: {
getConfigFields: jest.fn(() => {
return {
bringToFront: 'ENABLED',
};
}),
},
};
return {
CloudConfigDataTypes: {
NOT_SET: 'NOT_SET',
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
},
config: {
getConfigFields: jest.fn(() => {
return {
bringToFront: 'ENABLED',
};
}),
},
};
});
jest.mock('../src/app/activity-detection', () => {
return {
activityDetection: {
setWindowAndThreshold: jest.fn(),
},
};
return {
activityDetection: {
setWindowAndThreshold: jest.fn(),
},
};
});
jest.mock('../src/app/download-handler', () => {
return {
downloadHandler: {
setWindow: jest.fn(),
openFile: jest.fn(),
showInFinder: jest.fn(),
clearDownloadedItems: jest.fn(),
},
};
return {
downloadHandler: {
setWindow: jest.fn(),
openFile: jest.fn(),
showInFinder: jest.fn(),
clearDownloadedItems: jest.fn(),
},
};
});
jest.mock('../src/app/notifications/notification-helper', () => {
return {
notificationHelper: {
showNotification: jest.fn(),
closeNotification: jest.fn(),
},
};
return {
notificationHelper: {
showNotification: jest.fn(),
closeNotification: jest.fn(),
},
};
});
jest.mock('../src/common/i18n');
describe('main api handler', () => {
beforeEach(() => {
jest.clearAllMocks();
(utils.isValidWindow as any) = jest.fn(() => true);
});
beforeEach(() => {
jest.clearAllMocks();
(utils.isValidWindow as any) = jest.fn(() => true);
describe('symphony-api events', () => {
it('should call `isOnline` correctly', () => {
const value = {
cmd: apiCmds.isOnline,
isOnline: true,
};
ipcMain.send(apiName.symphonyApi, value);
expect(windowHandler.isOnline).toBe(true);
});
describe('symphony-api events', () => {
it('should call `isOnline` correctly', () => {
const value = {
cmd: apiCmds.isOnline,
isOnline: true,
};
ipcMain.send(apiName.symphonyApi, value);
expect(windowHandler.isOnline).toBe(true);
});
it('should call `setBadgeCount` correctly', () => {
const spy = jest.spyOn(utils, 'showBadgeCount');
const value = {
cmd: apiCmds.setBadgeCount,
count: 3,
};
const expectedValue = 3;
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should fail when `isValidWindow` is false', () => {
(utils.isValidWindow as any) = jest.fn(() => false);
const spy = jest.spyOn(utils, 'showBadgeCount');
const value = {
cmd: apiCmds.setBadgeCount,
count: 3,
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).not.toBeCalled();
});
it('should fail when `arg` is false', () => {
const value = null;
const spy = jest.spyOn(utils, 'showBadgeCount');
ipcMain.send(apiName.symphonyApi, value);
expect(spy).not.toBeCalled();
});
it('should call `registerProtocolHandler` correctly', () => {
const spy = jest.spyOn(protocolHandler, 'setPreloadWebContents');
const value = {
cmd: apiCmds.registerProtocolHandler,
};
const expectedValue = {
send: expect.any(Function),
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `badgeDataUrl` correctly', () => {
const spy = jest.spyOn(utils, 'setDataUrl');
const value = {
cmd: apiCmds.badgeDataUrl,
dataUrl: 'https://symphony.com',
count: 3,
};
const expectedValue = [ 'https://symphony.com', 3 ];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `activate` correctly', () => {
const spy = jest.spyOn(windowActions, 'activate');
const value = {
cmd: apiCmds.activate,
windowName: 'notification',
};
const expectedValue = 'notification';
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `registerLogger` correctly', () => {
const spy = jest.spyOn(logger, 'setLoggerWindow');
const value = {
cmd: apiCmds.registerLogger,
};
const expectedValue = {
send: expect.any(Function),
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `registerActivityDetection` correctly', () => {
const spy = jest.spyOn(activityDetection, 'setWindowAndThreshold');
const value = {
cmd: apiCmds.registerActivityDetection,
period: 3,
};
const expectedValue = [ { send: expect.any(Function) }, 3 ];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `registerDownloadHandler` correctly', () => {
const spy = jest.spyOn(downloadHandler, 'setWindow');
const value = {
cmd: apiCmds.registerDownloadHandler,
};
const expectedValue = [ { send: expect.any(Function) } ];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `openFile` correctly', () => {
const spy = jest.spyOn(downloadHandler, 'openFile');
const value = {
cmd: apiCmds.openDownloadedItem,
id: '12345678',
};
const expectedValue = '12345678';
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should not call `openFile` if id is not a string', () => {
const spy = jest.spyOn(downloadHandler, 'openFile');
const value = {
cmd: apiCmds.openDownloadedItem,
id: 10,
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).not.toBeCalled();
});
it('should call `showFile` correctly', () => {
const spy = jest.spyOn(downloadHandler, 'showInFinder');
const value = {
cmd: apiCmds.showDownloadedItem,
id: `12345678`,
};
const expectedValue = '12345678';
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should not call `showFile` if id is not a string', () => {
const spy = jest.spyOn(downloadHandler, 'showInFinder');
const value = {
cmd: apiCmds.showDownloadedItem,
id: 10,
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).not.toBeCalled();
});
it('should call `clearItems` correctly', () => {
const spy = jest.spyOn(downloadHandler, 'clearDownloadedItems');
const value = {
cmd: apiCmds.clearDownloadedItems,
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalled();
});
it('should call `showNotificationSettings` correctly', () => {
const spy = jest.spyOn(windowHandler, 'createNotificationSettingsWindow');
const value = {
cmd: apiCmds.showNotificationSettings,
windowName: 'notification-settings',
theme: 'light',
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith('notification-settings', 'light');
});
it('should call `sanitize` correctly', () => {
const spy = jest.spyOn(utils, 'sanitize');
const value = {
cmd: apiCmds.sanitize,
windowName: 'main',
};
const expectedValue = 'main';
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `bringToFront` correctly', () => {
const spy = jest.spyOn(windowActions, 'activate');
const value = {
cmd: apiCmds.bringToFront,
reason: 'notification',
windowName: 'notification',
};
const expectedValue = [ 'notification', false ];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `openScreenPickerWindow` correctly', () => {
const spy = jest.spyOn(windowHandler, 'createScreenPickerWindow');
const value = {
cmd: apiCmds.openScreenPickerWindow,
sources: [],
id: 3,
};
const expectedValue = [ {send: expect.any(Function)}, [], 3 ];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `popupMenu` correctly', () => {
const fromWebContentsMocked = {
isDestroyed: jest.fn(),
winName: apiName.mainWindowName,
};
const spy = jest.spyOn(utils, 'showPopupMenu');
const value = {
cmd: apiCmds.popupMenu,
};
const expectedValue = { window: fromWebContentsMocked };
jest.spyOn(BrowserWindow, 'fromWebContents').mockImplementation(() => {
return fromWebContentsMocked;
});
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `setLocale` correctly', () => {
const spy = jest.spyOn(utils, 'updateLocale');
const value = {
cmd: apiCmds.setLocale,
locale: 'en-US',
};
const expectedValue = 'en-US';
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `keyPress` correctly', () => {
const spy = jest.spyOn(windowActions, 'handleKeyPress');
const value = {
cmd: apiCmds.keyPress,
keyCode: 3,
};
const expectedValue = 3;
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `openScreenSnippet` correctly', () => {
const spy = jest.spyOn(screenSnippet, 'capture');
const value = {
cmd: apiCmds.openScreenSnippet,
};
const expectedValue = { send: expect.any(Function) };
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `closeWindow` correctly', () => {
const spy = jest.spyOn(windowHandler, 'closeWindow');
const value = {
cmd: apiCmds.closeWindow,
windowType: 2,
winKey: 'main',
};
const expectedValue = [ 2, 'main' ];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `openScreenSharingIndicator` correctly', () => {
const spy = jest.spyOn(windowHandler, 'createScreenSharingIndicatorWindow');
const value = {
cmd: apiCmds.openScreenSharingIndicator,
displayId: 'main',
id: 3,
streamId: '3',
};
const expectedValue = [ { send: expect.any(Function) }, 'main', 3, '3' ];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `downloadManagerAction` correctly', () => {
const spy = jest.spyOn(utils, 'downloadManagerAction');
const value = {
cmd: apiCmds.downloadManagerAction,
type: 2,
path: '/Users/symphony/SymphonyElectron/src/app/main-api-handler.ts',
};
const expectedValue = [ 2, '/Users/symphony/SymphonyElectron/src/app/main-api-handler.ts' ];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `setIsMana` correctly', () => {
const value = {
cmd: apiCmds.setIsMana,
isMana: true,
};
expect(windowHandler.isMana).toBe(false);
ipcMain.send(apiName.symphonyApi, value);
expect(windowHandler.isMana).toBe(true);
});
it('should call `setBadgeCount` correctly', () => {
const spy = jest.spyOn(utils, 'showBadgeCount');
const value = {
cmd: apiCmds.setBadgeCount,
count: 3,
};
const expectedValue = 3;
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should fail when `isValidWindow` is false', () => {
(utils.isValidWindow as any) = jest.fn(() => false);
const spy = jest.spyOn(utils, 'showBadgeCount');
const value = {
cmd: apiCmds.setBadgeCount,
count: 3,
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).not.toBeCalled();
});
it('should fail when `arg` is false', () => {
const value = null;
const spy = jest.spyOn(utils, 'showBadgeCount');
ipcMain.send(apiName.symphonyApi, value);
expect(spy).not.toBeCalled();
});
it('should call `registerProtocolHandler` correctly', () => {
const spy = jest.spyOn(protocolHandler, 'setPreloadWebContents');
const value = {
cmd: apiCmds.registerProtocolHandler,
};
const expectedValue = {
send: expect.any(Function),
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `badgeDataUrl` correctly', () => {
const spy = jest.spyOn(utils, 'setDataUrl');
const value = {
cmd: apiCmds.badgeDataUrl,
dataUrl: 'https://symphony.com',
count: 3,
};
const expectedValue = ['https://symphony.com', 3];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `activate` correctly', () => {
const spy = jest.spyOn(windowActions, 'activate');
const value = {
cmd: apiCmds.activate,
windowName: 'notification',
};
const expectedValue = 'notification';
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `registerLogger` correctly', () => {
const spy = jest.spyOn(logger, 'setLoggerWindow');
const value = {
cmd: apiCmds.registerLogger,
};
const expectedValue = {
send: expect.any(Function),
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `registerActivityDetection` correctly', () => {
const spy = jest.spyOn(activityDetection, 'setWindowAndThreshold');
const value = {
cmd: apiCmds.registerActivityDetection,
period: 3,
};
const expectedValue = [{ send: expect.any(Function) }, 3];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `registerDownloadHandler` correctly', () => {
const spy = jest.spyOn(downloadHandler, 'setWindow');
const value = {
cmd: apiCmds.registerDownloadHandler,
};
const expectedValue = [{ send: expect.any(Function) }];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `openFile` correctly', () => {
const spy = jest.spyOn(downloadHandler, 'openFile');
const value = {
cmd: apiCmds.openDownloadedItem,
id: '12345678',
};
const expectedValue = '12345678';
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should not call `openFile` if id is not a string', () => {
const spy = jest.spyOn(downloadHandler, 'openFile');
const value = {
cmd: apiCmds.openDownloadedItem,
id: 10,
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).not.toBeCalled();
});
it('should call `showFile` correctly', () => {
const spy = jest.spyOn(downloadHandler, 'showInFinder');
const value = {
cmd: apiCmds.showDownloadedItem,
id: `12345678`,
};
const expectedValue = '12345678';
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should not call `showFile` if id is not a string', () => {
const spy = jest.spyOn(downloadHandler, 'showInFinder');
const value = {
cmd: apiCmds.showDownloadedItem,
id: 10,
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).not.toBeCalled();
});
it('should call `clearItems` correctly', () => {
const spy = jest.spyOn(downloadHandler, 'clearDownloadedItems');
const value = {
cmd: apiCmds.clearDownloadedItems,
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalled();
});
it('should call `showNotificationSettings` correctly', () => {
const spy = jest.spyOn(windowHandler, 'createNotificationSettingsWindow');
const value = {
cmd: apiCmds.showNotificationSettings,
windowName: 'notification-settings',
theme: 'light',
};
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith('notification-settings', 'light');
});
it('should call `sanitize` correctly', () => {
const spy = jest.spyOn(utils, 'sanitize');
const value = {
cmd: apiCmds.sanitize,
windowName: 'main',
};
const expectedValue = 'main';
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `bringToFront` correctly', () => {
const spy = jest.spyOn(windowActions, 'activate');
const value = {
cmd: apiCmds.bringToFront,
reason: 'notification',
windowName: 'notification',
};
const expectedValue = ['notification', false];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `openScreenPickerWindow` correctly', () => {
const spy = jest.spyOn(windowHandler, 'createScreenPickerWindow');
const value = {
cmd: apiCmds.openScreenPickerWindow,
sources: [],
id: 3,
};
const expectedValue = [{ send: expect.any(Function) }, [], 3];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `popupMenu` correctly', () => {
const fromWebContentsMocked = {
isDestroyed: jest.fn(),
winName: apiName.mainWindowName,
};
const spy = jest.spyOn(utils, 'showPopupMenu');
const value = {
cmd: apiCmds.popupMenu,
};
const expectedValue = { window: fromWebContentsMocked };
jest.spyOn(BrowserWindow, 'fromWebContents').mockImplementation(() => {
return fromWebContentsMocked;
});
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `setLocale` correctly', () => {
const spy = jest.spyOn(utils, 'updateLocale');
const value = {
cmd: apiCmds.setLocale,
locale: 'en-US',
};
const expectedValue = 'en-US';
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `keyPress` correctly', () => {
const spy = jest.spyOn(windowActions, 'handleKeyPress');
const value = {
cmd: apiCmds.keyPress,
keyCode: 3,
};
const expectedValue = 3;
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `openScreenSnippet` correctly', () => {
const spy = jest.spyOn(screenSnippet, 'capture');
const value = {
cmd: apiCmds.openScreenSnippet,
};
const expectedValue = { send: expect.any(Function) };
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(expectedValue);
});
it('should call `closeWindow` correctly', () => {
const spy = jest.spyOn(windowHandler, 'closeWindow');
const value = {
cmd: apiCmds.closeWindow,
windowType: 2,
winKey: 'main',
};
const expectedValue = [2, 'main'];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `openScreenSharingIndicator` correctly', () => {
const spy = jest.spyOn(
windowHandler,
'createScreenSharingIndicatorWindow',
);
const value = {
cmd: apiCmds.openScreenSharingIndicator,
displayId: 'main',
id: 3,
streamId: '3',
};
const expectedValue = [{ send: expect.any(Function) }, 'main', 3, '3'];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `downloadManagerAction` correctly', () => {
const spy = jest.spyOn(utils, 'downloadManagerAction');
const value = {
cmd: apiCmds.downloadManagerAction,
type: 2,
path: '/Users/symphony/SymphonyElectron/src/app/main-api-handler.ts',
};
const expectedValue = [
2,
'/Users/symphony/SymphonyElectron/src/app/main-api-handler.ts',
];
ipcMain.send(apiName.symphonyApi, value);
expect(spy).toBeCalledWith(...expectedValue);
});
it('should call `setIsMana` correctly', () => {
const value = {
cmd: apiCmds.setIsMana,
isMana: true,
};
expect(windowHandler.isMana).toBe(false);
ipcMain.send(apiName.symphonyApi, value);
expect(windowHandler.isMana).toBe(true);
});
});
});

View File

@ -4,219 +4,247 @@ import NotificationSettings from '../src/renderer/components/notification-settin
import { ipcRenderer } from './__mocks__/electron';
describe('Notification Settings', () => {
const notificationSettingsLabel = 'notification-settings-data';
const notificationSettingsMock = {
const notificationSettingsLabel = 'notification-settings-data';
const notificationSettingsMock = {
position: 'upper-right',
screens: [
{
id: '6713899',
},
{
id: '3512909',
},
],
display: '6713899',
theme: 'light',
};
const onLabelEvent = 'on';
const sendEvent = 'send';
const removeListenerLabelEvent = 'removeListener';
describe('should mount, unmount and render component', () => {
it('should render the component', () => {
const wrapper = shallow(React.createElement(NotificationSettings));
expect(wrapper).toMatchSnapshot();
});
it('should call `notification-settings-data` event when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(NotificationSettings));
expect(spy).toBeCalledWith(
notificationSettingsLabel,
expect.any(Function),
);
});
it('should call `updateState` when component is mounted', () => {
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
expect(spy).toBeCalledWith(notificationSettingsMock);
});
it('should remove listener `notification-settings-data` when component is unmounted', () => {
const spyMount = jest.spyOn(ipcRenderer, onLabelEvent);
const spyUnmount = jest.spyOn(ipcRenderer, removeListenerLabelEvent);
const wrapper = shallow(React.createElement(NotificationSettings));
expect(spyMount).toBeCalledWith(
notificationSettingsLabel,
expect.any(Function),
);
wrapper.unmount();
expect(spyUnmount).toBeCalledWith(
notificationSettingsLabel,
expect.any(Function),
);
});
});
describe('should select display', () => {
it('should select display from drop down', () => {
const notificationSettingsMock = {
position: 'upper-right',
screens: [
{
id: '6713899',
},
{
id: '3512909',
},
],
screens: [],
display: '6713899',
theme: 'light'
};
const onLabelEvent = 'on';
const sendEvent = 'send';
const removeListenerLabelEvent = 'removeListener';
};
describe('should mount, unmount and render component', () => {
it('should render the component', () => {
const wrapper = shallow(React.createElement(NotificationSettings));
expect(wrapper).toMatchSnapshot();
});
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const selectDisplaySpy = jest.spyOn(
NotificationSettings.prototype,
'selectDisplay',
);
it('should call `notification-settings-data` event when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(NotificationSettings));
expect(spy).toBeCalledWith(notificationSettingsLabel, expect.any(Function));
});
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
it('should call `updateState` when component is mounted', () => {
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
shallow(React.createElement(NotificationSettings));
const positionButton = `select.display-selector`;
const input = wrapper.find(positionButton);
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
input.simulate('change', { target: { value: '6713899' } });
expect(spy).toBeCalledWith(notificationSettingsMock);
});
expect(selectDisplaySpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
});
it('should remove listener `notification-settings-data` when component is unmounted', () => {
const spyMount = jest.spyOn(ipcRenderer, onLabelEvent);
const spyUnmount = jest.spyOn(ipcRenderer, removeListenerLabelEvent);
describe('should set display position', () => {
it('should select top right position', () => {
const notificationSettingsMock = {
position: 'upper-right',
screens: [],
display: '6713899',
theme: 'light',
};
const wrapper = shallow(React.createElement(NotificationSettings));
expect(spyMount).toBeCalledWith(notificationSettingsLabel, expect.any(Function));
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(
NotificationSettings.prototype,
'togglePosition',
);
wrapper.unmount();
expect(spyUnmount).toBeCalledWith(notificationSettingsLabel, expect.any(Function));
});
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const input = wrapper.find('[data-testid="upper-right"]');
input.simulate('click', { target: { value: 'upper-right' } });
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
describe('should select display', () => {
it('should select display from drop down', () => {
const notificationSettingsMock = {
position: 'upper-right',
screens: [],
display: '6713899',
};
it('should select bottom right position', () => {
const notificationSettingsMock = {
position: 'bottom-right',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const selectDisplaySpy = jest.spyOn(NotificationSettings.prototype, 'selectDisplay');
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(
NotificationSettings.prototype,
'togglePosition',
);
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const positionButton = `select.display-selector`;
const input = wrapper.find(positionButton);
const input = wrapper.find('[data-testid="lower-right"]');
input.simulate('change', { target: { value: '6713899' } });
expect(selectDisplaySpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
input.simulate('click', { target: { value: 'lower-right' } });
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
describe('should set display position', () => {
it('should select top right position', () => {
const notificationSettingsMock = {
position: 'upper-right',
screens: [],
display: '6713899',
theme: 'light',
};
it('should select top left position', () => {
const notificationSettingsMock = {
position: 'upper-left',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(NotificationSettings.prototype, 'togglePosition');
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(
NotificationSettings.prototype,
'togglePosition',
);
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const input = wrapper.find('[data-testid="upper-right"]');
const input = wrapper.find('[data-testid="upper-left"]');
input.simulate('click', { target: { value: 'upper-right' } });
input.simulate('click', { target: { value: 'upper-left' } });
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
it('should select bottom right position', () => {
const notificationSettingsMock = {
position: 'bottom-right',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(NotificationSettings.prototype, 'togglePosition');
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const input = wrapper.find('[data-testid="lower-right"]');
input.simulate('click', { target: { value: 'lower-right' } });
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
it('should select top left position', () => {
const notificationSettingsMock = {
position: 'upper-left',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(NotificationSettings.prototype, 'togglePosition');
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const input = wrapper.find('[data-testid="upper-left"]');
input.simulate('click', { target: { value: 'upper-left' } });
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
it('should select bottom left position', () => {
const notificationSettingsMock = {
position: 'lower-left',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(NotificationSettings.prototype, 'togglePosition');
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const input = wrapper.find('[data-testid="lower-left"]');
input.simulate('click', { target: { value: 'lower-left' } });
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
describe('should submit or cancel new preferences', () => {
it('should close window on pressing cancel button', () => {
const notificationSettingsMock = {
position: 'lower-left',
screens: [],
display: '6713899',
};
it('should select bottom left position', () => {
const notificationSettingsMock = {
position: 'lower-left',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(ipcRenderer, sendEvent);
const closeButtonSpy = jest.spyOn(NotificationSettings.prototype, 'close');
const spy = jest.spyOn(NotificationSettings.prototype, 'setState');
const togglePositionButtonSpy = jest.spyOn(
NotificationSettings.prototype,
'togglePosition',
);
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const closeButton = `button.footer-button-dismiss`;
const input = wrapper.find(closeButton);
const input = wrapper.find('[data-testid="lower-left"]');
input.simulate('click');
input.simulate('click', { target: { value: 'lower-left' } });
expect(closeButtonSpy).toBeCalled();
expect(spy).toBeCalledWith('symphony-api', {
cmd: 'close-window',
windowType: 'notification-settings',
});
});
expect(togglePositionButtonSpy).toBeCalled();
expect(spy).toBeCalledWith(notificationSettingsMock);
});
});
it('should submit new preferences on pressing ok button', () => {
const notificationSettingsMock = {
position: 'lower-left',
screens: [],
display: '6713899',
};
describe('should submit or cancel new preferences', () => {
it('should close window on pressing cancel button', () => {
const notificationSettingsMock = {
position: 'lower-left',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(ipcRenderer, sendEvent);
const closeButtonSpy = jest.spyOn(NotificationSettings.prototype, 'close');
const spy = jest.spyOn(ipcRenderer, sendEvent);
const closeButtonSpy = jest.spyOn(
NotificationSettings.prototype,
'close',
);
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const closeButton = `button.footer-button-ok`;
const input = wrapper.find(closeButton);
const closeButton = `button.footer-button-dismiss`;
const input = wrapper.find(closeButton);
input.simulate('click');
input.simulate('click');
expect(closeButtonSpy).toBeCalled();
expect(spy).toBeCalledWith('notification-settings-update', {
position: 'lower-left',
display: '6713899',
});
});
expect(closeButtonSpy).toBeCalled();
expect(spy).toBeCalledWith('symphony-api', {
cmd: 'close-window',
windowType: 'notification-settings',
});
});
it('should submit new preferences on pressing ok button', () => {
const notificationSettingsMock = {
position: 'lower-left',
screens: [],
display: '6713899',
};
const spy = jest.spyOn(ipcRenderer, sendEvent);
const closeButtonSpy = jest.spyOn(
NotificationSettings.prototype,
'close',
);
const wrapper = shallow(React.createElement(NotificationSettings));
ipcRenderer.send('notification-settings-data', notificationSettingsMock);
const closeButton = `button.footer-button-ok`;
const input = wrapper.find(closeButton);
input.simulate('click');
expect(closeButtonSpy).toBeCalled();
expect(spy).toBeCalledWith('notification-settings-update', {
position: 'lower-left',
display: '6713899',
});
});
});
});

View File

@ -1,138 +1,154 @@
jest.mock('electron-log');
jest.mock('../src/app/window-actions', () => {
return {
activate: jest.fn(),
};
return {
activate: jest.fn(),
};
});
jest.mock('../src/common/utils', () => {
return {
getCommandLineArgs: jest.fn(() => 'symphony://?userId=22222'),
};
return {
getCommandLineArgs: jest.fn(() => 'symphony://?userId=22222'),
};
});
jest.mock('../src/common/env', () => {
return {
isWindowsOS: false,
isLinux: false,
isMac: true,
};
return {
isWindowsOS: false,
isLinux: false,
isMac: true,
};
});
jest.mock('../src/common/logger', () => {
return {
logger: {
setLoggerWindow: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
verbose: jest.fn(),
debug: jest.fn(),
silly: jest.fn(),
},
};
return {
logger: {
setLoggerWindow: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
verbose: jest.fn(),
debug: jest.fn(),
silly: jest.fn(),
},
};
});
describe('protocol handler', () => {
let protocolHandlerInstance;
let protocolHandlerInstance;
beforeEach(() => {
jest.resetModules();
const { protocolHandler } = require('../src/app/protocol-handler');
protocolHandlerInstance = protocolHandler;
});
beforeEach(() => {
jest.resetModules();
const { protocolHandler } = require('../src/app/protocol-handler');
protocolHandlerInstance = protocolHandler;
});
it('protocol uri should be null by default', () => {
expect(protocolHandlerInstance.protocolUri).toBe('symphony://?userId=22222');
});
it('protocol uri should be null by default', () => {
expect(protocolHandlerInstance.protocolUri).toBe(
'symphony://?userId=22222',
);
});
it('protocol action should be called when uri is correct', () => {
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
it('protocol action should be called when uri is correct', () => {
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
const spy: jest.SpyInstance = jest.spyOn(protocolHandlerInstance.preloadWebContents, 'send');
const uri: string = 'symphony://?userId=123456';
const protocolAction: string = 'protocol-action';
const spy: jest.SpyInstance = jest.spyOn(
protocolHandlerInstance.preloadWebContents,
'send',
);
const uri: string = 'symphony://?userId=123456';
const protocolAction: string = 'protocol-action';
protocolHandlerInstance.sendProtocol(uri);
protocolHandlerInstance.sendProtocol(uri);
expect(spy).toBeCalledWith(protocolAction, 'symphony://?userId=123456');
});
expect(spy).toBeCalledWith(protocolAction, 'symphony://?userId=123456');
});
it('protocol activate should be called when uri is correct on macOS', () => {
const { activate } = require('../src/app/window-actions');
it('protocol activate should be called when uri is correct on macOS', () => {
const { activate } = require('../src/app/window-actions');
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
const uri: string = 'symphony://?userId=123456';
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
const uri: string = 'symphony://?userId=123456';
protocolHandlerInstance.sendProtocol(uri);
protocolHandlerInstance.sendProtocol(uri);
expect(activate).toBeCalledWith('main');
});
expect(activate).toBeCalledWith('main');
});
it('protocol activate should not be called when uri is correct on non macOS', () => {
const env = require('../src/common/env');
env.isMac = false;
it('protocol activate should not be called when uri is correct on non macOS', () => {
const env = require('../src/common/env');
env.isMac = false;
const { activate } = require('../src/app/window-actions');
const { activate } = require('../src/app/window-actions');
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
const uri: string = 'symphony://?userId=123456';
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
const uri: string = 'symphony://?userId=123456';
protocolHandlerInstance.sendProtocol(uri);
protocolHandlerInstance.sendProtocol(uri);
expect(activate).not.toBeCalled();
});
expect(activate).not.toBeCalled();
});
it('protocol action not should be called when uri is incorrect', () => {
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
it('protocol action not should be called when uri is incorrect', () => {
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
const spy: jest.SpyInstance = jest.spyOn(protocolHandlerInstance.preloadWebContents, 'send');
const uri: string = 'symphony---://?userId=123456';
const protocolAction: string = 'protocol-action';
const spy: jest.SpyInstance = jest.spyOn(
protocolHandlerInstance.preloadWebContents,
'send',
);
const uri: string = 'symphony---://?userId=123456';
const protocolAction: string = 'protocol-action';
protocolHandlerInstance.sendProtocol(uri);
protocolHandlerInstance.sendProtocol(uri);
expect(spy).not.toBeCalledWith(protocolAction, 'symphony://?userId=123456');
});
expect(spy).not.toBeCalledWith(protocolAction, 'symphony://?userId=123456');
});
it('protocol should get uri from `processArgv` when `getCommandLineArgs` is called', () => {
const { getCommandLineArgs } = require('../src/common/utils');
it('protocol should get uri from `processArgv` when `getCommandLineArgs` is called', () => {
const { getCommandLineArgs } = require('../src/common/utils');
protocolHandlerInstance.processArgv('');
protocolHandlerInstance.processArgv('');
expect(getCommandLineArgs).toBeCalled();
});
expect(getCommandLineArgs).toBeCalled();
});
it('should be called `sendProtocol` when is windowsOS on `processArgs`', () => {
const env = require('../src/common/env');
env.isWindowsOS = true;
it('should be called `sendProtocol` when is windowsOS on `processArgs`', () => {
const env = require('../src/common/env');
env.isWindowsOS = true;
const spy: jest.SpyInstance = jest.spyOn(protocolHandlerInstance, 'sendProtocol');
const spy: jest.SpyInstance = jest.spyOn(
protocolHandlerInstance,
'sendProtocol',
);
protocolHandlerInstance.processArgv('');
protocolHandlerInstance.processArgv('');
expect(spy).toBeCalled();
});
expect(spy).toBeCalled();
});
it('should invoke `sendProtocol` when `setPreloadWebContents` is called and protocolUri is valid', () => {
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
protocolHandlerInstance.protocolUri = 'symphony://?userId=123456';
it('should invoke `sendProtocol` when `setPreloadWebContents` is called and protocolUri is valid', () => {
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
protocolHandlerInstance.protocolUri = 'symphony://?userId=123456';
const spy: jest.SpyInstance = jest.spyOn(protocolHandlerInstance, 'sendProtocol');
const spy: jest.SpyInstance = jest.spyOn(
protocolHandlerInstance,
'sendProtocol',
);
protocolHandlerInstance.setPreloadWebContents({ send: jest.fn() });
expect(spy).toBeCalledWith('symphony://?userId=123456');
});
protocolHandlerInstance.setPreloadWebContents({ send: jest.fn() });
expect(spy).toBeCalledWith('symphony://?userId=123456');
});
it('should not invoke `sendProtocol` when `setPreloadWebContents` is called and protocolUri is invalid', () => {
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
protocolHandlerInstance.protocolUri = null;
it('should not invoke `sendProtocol` when `setPreloadWebContents` is called and protocolUri is invalid', () => {
protocolHandlerInstance.preloadWebContents = { send: jest.fn() };
protocolHandlerInstance.protocolUri = null;
const spy: jest.SpyInstance = jest.spyOn(protocolHandlerInstance, 'sendProtocol');
protocolHandlerInstance.setPreloadWebContents({ send: jest.fn() });
expect(spy).not.toBeCalled();
});
const spy: jest.SpyInstance = jest.spyOn(
protocolHandlerInstance,
'sendProtocol',
);
protocolHandlerInstance.setPreloadWebContents({ send: jest.fn() });
expect(spy).not.toBeCalled();
});
});

View File

@ -4,365 +4,578 @@ import ScreenPicker from '../src/renderer/components/screen-picker';
import { ipcRenderer } from './__mocks__/electron';
jest.mock('../src/common/env', () => {
return {
isWindowsOS: false,
isLinux: false,
isMac: true,
};
return {
isWindowsOS: false,
isLinux: false,
isMac: true,
};
});
describe('screen picker', () => {
const keyCode = {
pageDown: { keyCode: 34 },
rightArrow: { keyCode: 39 },
pageUp: { keyCode: 33 },
leftArrow: { keyCode: 37 },
homeKey: { keyCode: 36 },
upArrow: { keyCode: 38 },
endKey: { keyCode: 35 },
downArrow: { keyCode: 40 },
enterKey: { keyCode: 13 },
escapeKey: { keyCode: 27 },
random: { keyCode: 100 },
};
const sendEventLabel = 'send';
const screenSourceSelectedLabel = 'screen-source-selected';
const symphonyApiLabel = 'symphony-api';
const screenTabCustomSelector = '#screen-tab';
const applicationTabCustomSelector = '#application-tab';
const screenPickerDataLabel = 'screen-picker-data';
const events = {
keyup: jest.fn(),
};
const stateMock = {
sources: [
{ display_id: '0', id: '0', name: 'Application screen 0', thumbnail: undefined },
{ display_id: '1', id: '1', name: 'Application screen 1', thumbnail: undefined },
{ display_id: '2', id: '2', name: 'Application screen 2', thumbnail: undefined },
],
selectedSource: { display_id: '1', id: '1', name: 'Application screen 1', thumbnail: undefined },
};
const keyCode = {
pageDown: { keyCode: 34 },
rightArrow: { keyCode: 39 },
pageUp: { keyCode: 33 },
leftArrow: { keyCode: 37 },
homeKey: { keyCode: 36 },
upArrow: { keyCode: 38 },
endKey: { keyCode: 35 },
downArrow: { keyCode: 40 },
enterKey: { keyCode: 13 },
escapeKey: { keyCode: 27 },
random: { keyCode: 100 },
};
const sendEventLabel = 'send';
const screenSourceSelectedLabel = 'screen-source-selected';
const symphonyApiLabel = 'symphony-api';
const screenTabCustomSelector = '#screen-tab';
const applicationTabCustomSelector = '#application-tab';
const screenPickerDataLabel = 'screen-picker-data';
const events = {
keyup: jest.fn(),
};
const stateMock = {
sources: [
{
display_id: '0',
id: '0',
name: 'Application screen 0',
thumbnail: undefined,
},
{
display_id: '1',
id: '1',
name: 'Application screen 1',
thumbnail: undefined,
},
{
display_id: '2',
id: '2',
name: 'Application screen 2',
thumbnail: undefined,
},
],
selectedSource: {
display_id: '1',
id: '1',
name: 'Application screen 1',
thumbnail: undefined,
},
};
it('should render correctly', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
expect(wrapper).toMatchSnapshot();
it('should render correctly', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
expect(wrapper).toMatchSnapshot();
});
it('should call `close` correctly', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ipcRenderer, sendEventLabel);
const customSelector = 'button.ScreenPicker-cancel-button';
const expectedValue = { cmd: 'close-window', windowType: 'screen-picker' };
wrapper.find(customSelector).simulate('click');
expect(spy).nthCalledWith(1, screenSourceSelectedLabel, null);
expect(spy).nthCalledWith(2, symphonyApiLabel, expectedValue);
});
it('should call `submit` correctly', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ipcRenderer, sendEventLabel);
const selectedSource = {
display_id: '1',
id: '1',
name: 'Entire screen',
thumbnail: undefined,
};
const customSelector = 'button.ScreenPicker-share-button';
wrapper.setState({ selectedSource });
wrapper.find(customSelector).simulate('click');
expect(spy).lastCalledWith(screenSourceSelectedLabel, selectedSource);
});
it('should call `updateState` correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const updateState = {
sources: [
{
display_id: '0',
id: '0',
name: 'Entire screen',
thumbnail: undefined,
},
],
selectedSource: undefined,
selectedTab: 'screens',
};
shallow(React.createElement(ScreenPicker));
ipcRenderer.send(screenPickerDataLabel, updateState);
expect(spy).toBeCalledWith(updateState);
});
describe('`onToggle` event', () => {
const entireScreenStateMock = {
sources: [
{
display_id: '0',
id: '0',
name: 'Entire screen',
thumbnail: undefined,
},
],
selectedSource: {
display_id: '0',
id: '0',
name: 'Entire screen',
thumbnail: undefined,
},
};
const applicationScreenStateMock = {
sources: [
{
display_id: '',
id: '1',
name: 'Application 1',
thumbnail: undefined,
},
],
selectedSource: {
display_id: '',
id: '1',
name: 'Application 1',
thumbnail: undefined,
},
};
it('should call `onToggle` when screen tab is changed', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedTab: 'screens' };
wrapper.setState(entireScreenStateMock);
wrapper.find(screenTabCustomSelector).simulate('change');
expect(spy).lastCalledWith(expectedValue);
});
it('should call `close` correctly', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ipcRenderer, sendEventLabel);
const customSelector = 'button.ScreenPicker-cancel-button';
const expectedValue = { cmd: 'close-window', windowType: 'screen-picker' };
wrapper.find(customSelector).simulate('click');
expect(spy).nthCalledWith(1, screenSourceSelectedLabel, null);
expect(spy).nthCalledWith(2, symphonyApiLabel, expectedValue);
it('should call `onToggle` when application tab is changed', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedTab: 'applications' };
wrapper.setState(applicationScreenStateMock);
wrapper.find(applicationTabCustomSelector).simulate('change');
expect(spy).lastCalledWith(expectedValue);
});
});
describe('onSelect event', () => {
it('should call `onSelect` when `ScreenPicker-item-container` in Entire screen is clicked', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = {
selectedSource: {
display_id: '0',
fileName: 'fullscreen',
id: '0',
name: 'Entire screen',
thumbnail: undefined,
},
};
const customSelector = '.ScreenPicker-item-container';
const applicationScreenStateMock = {
sources: [
{
display_id: '0',
id: '0',
name: 'Entire screen',
thumbnail: undefined,
},
{
display_id: '1',
id: '1',
name: 'Application screen 1',
thumbnail: undefined,
},
{
display_id: '2',
id: '2',
name: 'Application screen 2',
thumbnail: undefined,
},
],
selectedSource: {
display_id: '1',
fileName: 'fullscreen',
id: '1',
name: 'Application screen 1',
thumbnail: undefined,
},
};
wrapper.setState(applicationScreenStateMock);
wrapper.find(customSelector).first().simulate('click');
expect(spy).lastCalledWith(expectedValue);
});
it('should call `submit` correctly', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ipcRenderer, sendEventLabel);
const selectedSource = { display_id: '1', id: '1', name: 'Entire screen', thumbnail: undefined };
const customSelector = 'button.ScreenPicker-share-button';
wrapper.setState({ selectedSource });
wrapper.find(customSelector).simulate('click');
expect(spy).lastCalledWith(screenSourceSelectedLabel, selectedSource);
it('should call `onSelect` when `ScreenPicker-item-container` in Application screen is clicked', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = {
selectedSource: {
display_id: '2',
fileName: 'fullscreen',
id: '2',
name: 'Application screen 2',
thumbnail: undefined,
},
};
const customSelector = '.ScreenPicker-item-container';
const applicationScreenStateMock = {
sources: [
{
display_id: '0',
id: '0',
name: 'Entire screen',
thumbnail: undefined,
},
{
display_id: '1',
id: '1',
name: 'Application screen 1',
thumbnail: undefined,
},
{
display_id: '2',
id: '2',
name: 'Application screen 2',
thumbnail: undefined,
},
],
selectedSource: {
display_id: '1',
fileName: 'fullscreen',
id: '1',
name: 'Application screen 1',
thumbnail: undefined,
},
};
wrapper.setState(applicationScreenStateMock);
wrapper.find(customSelector).at(2).simulate('click');
expect(spy).lastCalledWith(expectedValue);
});
});
describe('handle keyUp', () => {
beforeAll(() => {
document.addEventListener = jest.fn((event, cb) => {
events[event] = cb;
});
});
it('should call `updateState` correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const updateState = {
sources: [ { display_id: '0', id: '0', name: 'Entire screen', thumbnail: undefined } ],
selectedSource: undefined,
selectedTab: 'screens',
};
shallow(React.createElement(ScreenPicker));
ipcRenderer.send(screenPickerDataLabel, updateState);
expect(spy).toBeCalledWith(updateState);
it('should register `keyup` when component is mounted', () => {
const spy = jest.spyOn(document, 'addEventListener');
shallow(React.createElement(ScreenPicker));
events.keyup(keyCode.random);
expect(spy).lastCalledWith('keyup', expect.any(Function), true);
});
describe('`onToggle` event', () => {
const entireScreenStateMock = {
sources: [
{ display_id: '0', id: '0', name: 'Entire screen', thumbnail: undefined },
],
selectedSource: { display_id: '0', id: '0', name: 'Entire screen', thumbnail: undefined },
};
const applicationScreenStateMock = {
sources: [
{ display_id: '', id: '1', name: 'Application 1', thumbnail: undefined },
],
selectedSource: { display_id: '', id: '1', name: 'Application 1', thumbnail: undefined },
};
it('should call `onToggle` when screen tab is changed', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedTab: 'screens' };
wrapper.setState(entireScreenStateMock);
wrapper.find(screenTabCustomSelector).simulate('change');
expect(spy).lastCalledWith(expectedValue);
});
it('should call `onToggle` when application tab is changed', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedTab: 'applications' };
wrapper.setState(applicationScreenStateMock);
wrapper.find(applicationTabCustomSelector).simulate('change');
expect(spy).lastCalledWith(expectedValue);
});
it('should remove event `keyup` when component is unmounted', () => {
const spy = jest.spyOn(document, 'removeEventListener');
shallow(React.createElement(ScreenPicker)).unmount();
expect(spy).lastCalledWith('keyup', expect.any(Function), true);
});
describe('onSelect event', () => {
it('should call `onSelect` when `ScreenPicker-item-container` in Entire screen is clicked', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedSource: { display_id: '0', fileName: 'fullscreen', id: '0', name: 'Entire screen', thumbnail: undefined }};
const customSelector = '.ScreenPicker-item-container';
const applicationScreenStateMock = {
sources: [
{ display_id: '0', id: '0', name: 'Entire screen', thumbnail: undefined },
{ display_id: '1', id: '1', name: 'Application screen 1', thumbnail: undefined },
{ display_id: '2', id: '2', name: 'Application screen 2', thumbnail: undefined },
],
selectedSource: { display_id: '1', fileName: 'fullscreen', id: '1', name: 'Application screen 1', thumbnail: undefined },
};
wrapper.setState(applicationScreenStateMock);
wrapper.find(customSelector).first().simulate('click');
expect(spy).lastCalledWith(expectedValue);
});
it('should call `onSelect` when `ScreenPicker-item-container` in Application screen is clicked', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedSource: { display_id: '2', fileName: 'fullscreen', id: '2', name: 'Application screen 2', thumbnail: undefined }};
const customSelector = '.ScreenPicker-item-container';
const applicationScreenStateMock = {
sources: [
{ display_id: '0', id: '0', name: 'Entire screen', thumbnail: undefined },
{ display_id: '1', id: '1', name: 'Application screen 1', thumbnail: undefined },
{ display_id: '2', id: '2', name: 'Application screen 2', thumbnail: undefined },
],
selectedSource: { display_id: '1', fileName: 'fullscreen', id: '1', name: 'Application screen 1', thumbnail: undefined },
};
wrapper.setState(applicationScreenStateMock);
wrapper.find(customSelector).at(2).simulate('click');
expect(spy).lastCalledWith(expectedValue);
});
it('should call `handleKeyUpPress` pageDown key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = {
selectedSource: {
display_id: '2',
fileName: 'fullscreen',
id: '2',
name: 'Application screen 2',
thumbnail: undefined,
},
};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.pageDown);
expect(spy).lastCalledWith(expectedValue);
});
describe('handle keyUp', () => {
beforeAll(() => {
document.addEventListener = jest.fn((event, cb) => {
events[event] = cb;
});
});
it('should register `keyup` when component is mounted', () => {
const spy = jest.spyOn(document, 'addEventListener');
shallow(React.createElement(ScreenPicker));
events.keyup(keyCode.random);
expect(spy).lastCalledWith('keyup', expect.any(Function), true);
});
it('should remove event `keyup` when component is unmounted', () => {
const spy = jest.spyOn(document, 'removeEventListener');
shallow(React.createElement(ScreenPicker)).unmount();
expect(spy).lastCalledWith('keyup', expect.any(Function), true);
});
it('should call `handleKeyUpPress` pageDown key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedSource: { display_id: '2', fileName: 'fullscreen', id: '2', name: 'Application screen 2', thumbnail: undefined }};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.pageDown);
expect(spy).lastCalledWith(expectedValue);
});
it('should call `handleKeyUpPress` right arrow key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedSource: { display_id: '2', fileName: 'fullscreen', id: '2', name: 'Application screen 2', thumbnail: undefined }};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.rightArrow);
expect(spy).lastCalledWith(expectedValue);
});
it('should call `handleKeyUpPress` pageUp key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedSource: { display_id: '0', fileName: 'fullscreen', id: '0', name: 'Application screen 0', thumbnail: undefined }};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.pageUp);
expect(spy).lastCalledWith(expectedValue);
});
it('should call `handleKeyUpPress` left arrow key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedSource: { display_id: '0', fileName: 'fullscreen', id: '0', name: 'Application screen 0', thumbnail: undefined }};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.leftArrow);
expect(spy).lastCalledWith(expectedValue);
});
it('should call `handleKeyUpPress` down arrow key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedSource: { display_id: '0', fileName: 'fullscreen', id: '0', name: 'Application screen 0', thumbnail: undefined }};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.downArrow);
expect(spy).lastCalledWith(expectedValue);
});
it('should call `handleKeyUpPress` up arrow key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedSource: { display_id: '2', fileName: 'fullscreen', id: '2', name: 'Application screen 2', thumbnail: undefined }};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.upArrow);
expect(spy).lastCalledWith(expectedValue);
});
it('should call `handleKeyUpPress` enter key correctly', () => {
const spy = jest.spyOn(ipcRenderer, sendEventLabel);
const expectedValue = { display_id: '1', id: '1', name: 'Application screen 1', thumbnail: undefined };
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.enterKey);
expect(spy).lastCalledWith(screenSourceSelectedLabel, expectedValue);
});
it('should call `handleKeyUpPress` escape key correctly', () => {
const spy = jest.spyOn(ipcRenderer, sendEventLabel);
const expectedValue = { cmd: 'close-window', windowType: 'screen-picker' };
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.escapeKey);
expect(spy).nthCalledWith(1, screenSourceSelectedLabel, null);
expect(spy).nthCalledWith(2, symphonyApiLabel, expectedValue);
});
it('should call `handleKeyUpPress` end key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = { selectedSource: { display_id: '0', fileName: 'fullscreen', id: '0', name: 'Application screen 0', thumbnail: undefined }};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.endKey);
expect(spy).lastCalledWith(expectedValue);
});
it('should call `handleKeyUpPress` right arrow key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = {
selectedSource: {
display_id: '2',
fileName: 'fullscreen',
id: '2',
name: 'Application screen 2',
thumbnail: undefined,
},
};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.rightArrow);
expect(spy).lastCalledWith(expectedValue);
});
describe('tab titles', () => {
it('should show `application-tab` when display_id is empty', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const applicationScreenStateMock = {
sources: [
{ display_id: '', id: '1', name: 'Application Screen', thumbnail: undefined },
{ display_id: '', id: '2', name: 'Application Screen 2', thumbnail: undefined },
{ display_id: '', id: '3', name: 'Application Screen 3', thumbnail: undefined },
],
};
wrapper.setState(applicationScreenStateMock);
expect(wrapper.find(applicationTabCustomSelector)).toHaveLength(1);
expect(wrapper.find(screenTabCustomSelector)).toHaveLength(0);
});
it('should show `screen-tab` when source name is Entire screen', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const entireScreenStateMock = {
sources: [
{ display_id: '1', id: '1', name: 'Entire screen', thumbnail: undefined },
{ display_id: '2', id: '2', name: 'Screen 2', thumbnail: undefined },
{ display_id: '3', id: '3', name: 'screen 3', thumbnail: undefined },
],
};
wrapper.setState(entireScreenStateMock);
expect(wrapper.find(screenTabCustomSelector)).toHaveLength(1);
expect(wrapper.find(applicationTabCustomSelector)).toHaveLength(0);
});
it('should show `screen-tab` for Windows when source name is Entire screen and display_id is not present', () => {
const env = require('../src/common/env');
const wrapper = shallow(React.createElement(ScreenPicker));
const entireScreenStateMock = {
sources: [
{ display_id: '', id: '1', name: 'Entire screen', thumbnail: undefined },
{ display_id: '', id: '2', name: 'Screen 2', thumbnail: undefined },
{ display_id: '', id: '3', name: 'screen 3', thumbnail: undefined },
],
};
env.isWindowsOS = true;
env.isLinux = false;
env.isMac = false;
wrapper.setState(entireScreenStateMock);
expect(wrapper.find(screenTabCustomSelector)).toHaveLength(1);
expect(wrapper.find(applicationTabCustomSelector)).toHaveLength(0);
});
it('should not show `screen-tab` for Mac when source name is Entire screen and display_id is not present', () => {
const env = require('../src/common/env');
const wrapper = shallow(React.createElement(ScreenPicker));
const entireScreenStateMock = {
sources: [
{ display_id: '', id: '1', name: 'Entire screen', thumbnail: undefined },
{ display_id: '', id: '2', name: 'Screen 2', thumbnail: undefined },
{ display_id: '', id: '3', name: 'screen 3', thumbnail: undefined },
],
};
env.isWindowsOS = false;
env.isLinux = false;
env.isMac = true;
wrapper.setState(entireScreenStateMock);
expect(wrapper.find(screenTabCustomSelector)).toHaveLength(0);
expect(wrapper.find(applicationTabCustomSelector)).toHaveLength(1);
});
it('should show `screen-tab` and `application-tab` when `isScreensAvailable` and `isApplicationsAvailable` is true', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const customState = {
sources: [
{ display_id: '1', id: '1', name: 'Entire screen', thumbnail: undefined },
{ display_id: '', id: '1', name: 'Application screen', thumbnail: undefined },
],
};
wrapper.setState(customState);
expect(wrapper.find(applicationTabCustomSelector)).toHaveLength(1);
expect(wrapper.find(screenTabCustomSelector)).toHaveLength(1);
});
it('should show `error-message` when source is empty', () => {
const errorSelector = '.error-message';
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState({ sources: []});
expect(wrapper.find(errorSelector)).toHaveLength(1);
});
it('should call `handleKeyUpPress` pageUp key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = {
selectedSource: {
display_id: '0',
fileName: 'fullscreen',
id: '0',
name: 'Application screen 0',
thumbnail: undefined,
},
};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.pageUp);
expect(spy).lastCalledWith(expectedValue);
});
describe('`screen-picker-data` event', () => {
it('should call `screen-picker-data` when component is mounted', () => {
const onEventLabel = 'on';
const spy = jest.spyOn(ipcRenderer, onEventLabel);
shallow(React.createElement(ScreenPicker));
expect(spy).lastCalledWith(screenPickerDataLabel, expect.any(Function));
});
it('should remove listen `screen-picker-data` when component is unmounted', () => {
const removeListenerEventLabel = 'removeListener';
const spy = jest.spyOn(ipcRenderer, removeListenerEventLabel);
shallow(React.createElement(ScreenPicker)).unmount();
expect(spy).lastCalledWith(screenPickerDataLabel, expect.any(Function));
});
it('should call `handleKeyUpPress` left arrow key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = {
selectedSource: {
display_id: '0',
fileName: 'fullscreen',
id: '0',
name: 'Application screen 0',
thumbnail: undefined,
},
};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.leftArrow);
expect(spy).lastCalledWith(expectedValue);
});
it('should call `ScreenPicker-window-border` event when component is mounted and is WindowsOS', () => {
const env = require('../src/common/env');
const spy = jest.spyOn(document.body.classList, 'add');
const expectedValue = 'ScreenPicker-window-border';
env.isWindowsOS = true;
shallow(React.createElement(ScreenPicker));
expect(spy).toBeCalledWith(expectedValue);
it('should call `handleKeyUpPress` down arrow key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = {
selectedSource: {
display_id: '0',
fileName: 'fullscreen',
id: '0',
name: 'Application screen 0',
thumbnail: undefined,
},
};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.downArrow);
expect(spy).lastCalledWith(expectedValue);
});
it('should call `handleKeyUpPress` up arrow key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = {
selectedSource: {
display_id: '2',
fileName: 'fullscreen',
id: '2',
name: 'Application screen 2',
thumbnail: undefined,
},
};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.upArrow);
expect(spy).lastCalledWith(expectedValue);
});
it('should call `handleKeyUpPress` enter key correctly', () => {
const spy = jest.spyOn(ipcRenderer, sendEventLabel);
const expectedValue = {
display_id: '1',
id: '1',
name: 'Application screen 1',
thumbnail: undefined,
};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.enterKey);
expect(spy).lastCalledWith(screenSourceSelectedLabel, expectedValue);
});
it('should call `handleKeyUpPress` escape key correctly', () => {
const spy = jest.spyOn(ipcRenderer, sendEventLabel);
const expectedValue = {
cmd: 'close-window',
windowType: 'screen-picker',
};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.escapeKey);
expect(spy).nthCalledWith(1, screenSourceSelectedLabel, null);
expect(spy).nthCalledWith(2, symphonyApiLabel, expectedValue);
});
it('should call `handleKeyUpPress` end key correctly', () => {
const spy = jest.spyOn(ScreenPicker.prototype, 'setState');
const expectedValue = {
selectedSource: {
display_id: '0',
fileName: 'fullscreen',
id: '0',
name: 'Application screen 0',
thumbnail: undefined,
},
};
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState(stateMock);
events.keyup(keyCode.endKey);
expect(spy).lastCalledWith(expectedValue);
});
});
describe('tab titles', () => {
it('should show `application-tab` when display_id is empty', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const applicationScreenStateMock = {
sources: [
{
display_id: '',
id: '1',
name: 'Application Screen',
thumbnail: undefined,
},
{
display_id: '',
id: '2',
name: 'Application Screen 2',
thumbnail: undefined,
},
{
display_id: '',
id: '3',
name: 'Application Screen 3',
thumbnail: undefined,
},
],
};
wrapper.setState(applicationScreenStateMock);
expect(wrapper.find(applicationTabCustomSelector)).toHaveLength(1);
expect(wrapper.find(screenTabCustomSelector)).toHaveLength(0);
});
it('should show `screen-tab` when source name is Entire screen', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const entireScreenStateMock = {
sources: [
{
display_id: '1',
id: '1',
name: 'Entire screen',
thumbnail: undefined,
},
{ display_id: '2', id: '2', name: 'Screen 2', thumbnail: undefined },
{ display_id: '3', id: '3', name: 'screen 3', thumbnail: undefined },
],
};
wrapper.setState(entireScreenStateMock);
expect(wrapper.find(screenTabCustomSelector)).toHaveLength(1);
expect(wrapper.find(applicationTabCustomSelector)).toHaveLength(0);
});
it('should show `screen-tab` for Windows when source name is Entire screen and display_id is not present', () => {
const env = require('../src/common/env');
const wrapper = shallow(React.createElement(ScreenPicker));
const entireScreenStateMock = {
sources: [
{
display_id: '',
id: '1',
name: 'Entire screen',
thumbnail: undefined,
},
{ display_id: '', id: '2', name: 'Screen 2', thumbnail: undefined },
{ display_id: '', id: '3', name: 'screen 3', thumbnail: undefined },
],
};
env.isWindowsOS = true;
env.isLinux = false;
env.isMac = false;
wrapper.setState(entireScreenStateMock);
expect(wrapper.find(screenTabCustomSelector)).toHaveLength(1);
expect(wrapper.find(applicationTabCustomSelector)).toHaveLength(0);
});
it('should not show `screen-tab` for Mac when source name is Entire screen and display_id is not present', () => {
const env = require('../src/common/env');
const wrapper = shallow(React.createElement(ScreenPicker));
const entireScreenStateMock = {
sources: [
{
display_id: '',
id: '1',
name: 'Entire screen',
thumbnail: undefined,
},
{ display_id: '', id: '2', name: 'Screen 2', thumbnail: undefined },
{ display_id: '', id: '3', name: 'screen 3', thumbnail: undefined },
],
};
env.isWindowsOS = false;
env.isLinux = false;
env.isMac = true;
wrapper.setState(entireScreenStateMock);
expect(wrapper.find(screenTabCustomSelector)).toHaveLength(0);
expect(wrapper.find(applicationTabCustomSelector)).toHaveLength(1);
});
it('should show `screen-tab` and `application-tab` when `isScreensAvailable` and `isApplicationsAvailable` is true', () => {
const wrapper = shallow(React.createElement(ScreenPicker));
const customState = {
sources: [
{
display_id: '1',
id: '1',
name: 'Entire screen',
thumbnail: undefined,
},
{
display_id: '',
id: '1',
name: 'Application screen',
thumbnail: undefined,
},
],
};
wrapper.setState(customState);
expect(wrapper.find(applicationTabCustomSelector)).toHaveLength(1);
expect(wrapper.find(screenTabCustomSelector)).toHaveLength(1);
});
it('should show `error-message` when source is empty', () => {
const errorSelector = '.error-message';
const wrapper = shallow(React.createElement(ScreenPicker));
wrapper.setState({ sources: [] });
expect(wrapper.find(errorSelector)).toHaveLength(1);
});
});
describe('`screen-picker-data` event', () => {
it('should call `screen-picker-data` when component is mounted', () => {
const onEventLabel = 'on';
const spy = jest.spyOn(ipcRenderer, onEventLabel);
shallow(React.createElement(ScreenPicker));
expect(spy).lastCalledWith(screenPickerDataLabel, expect.any(Function));
});
it('should remove listen `screen-picker-data` when component is unmounted', () => {
const removeListenerEventLabel = 'removeListener';
const spy = jest.spyOn(ipcRenderer, removeListenerEventLabel);
shallow(React.createElement(ScreenPicker)).unmount();
expect(spy).lastCalledWith(screenPickerDataLabel, expect.any(Function));
});
});
it('should call `ScreenPicker-window-border` event when component is mounted and is WindowsOS', () => {
const env = require('../src/common/env');
const spy = jest.spyOn(document.body.classList, 'add');
const expectedValue = 'ScreenPicker-window-border';
env.isWindowsOS = true;
shallow(React.createElement(ScreenPicker));
expect(spy).toBeCalledWith(expectedValue);
});
});

View File

@ -4,57 +4,68 @@ import ScreenSharingIndicator from '../src/renderer/components/screen-sharing-in
import { ipcRenderer } from './__mocks__/electron';
jest.mock('../src/common/env', () => {
return {
isWindowsOS: false,
isMac: true,
isLinux: false,
};
return {
isWindowsOS: false,
isMac: true,
isLinux: false,
};
});
describe('screen sharing indicator', () => {
// events
const onEventLabel = 'on';
const removeListenerEventLabel = 'removeListener';
const sendEventLabel = 'send';
const screenSharingIndicatorDataEventLabel = 'screen-sharing-indicator-data';
// state moked
const screenSharingIndicatorStateMock = { id: 10 };
// events
const onEventLabel = 'on';
const removeListenerEventLabel = 'removeListener';
const sendEventLabel = 'send';
const screenSharingIndicatorDataEventLabel = 'screen-sharing-indicator-data';
// state moked
const screenSharingIndicatorStateMock = { id: 10 };
it('should render correctly', () => {
const wrapper = shallow(React.createElement(ScreenSharingIndicator));
expect(wrapper).toMatchSnapshot();
it('should render correctly', () => {
const wrapper = shallow(React.createElement(ScreenSharingIndicator));
expect(wrapper).toMatchSnapshot();
});
it('should call `stopScreenShare` correctly', () => {
const customSelector = 'button.stop-sharing-button';
const stopScreenSharingEventLabel = 'stop-screen-sharing';
const spy = jest.spyOn(ipcRenderer, sendEventLabel);
const wrapper = shallow(React.createElement(ScreenSharingIndicator));
wrapper.setState(screenSharingIndicatorStateMock);
wrapper.find(customSelector).simulate('click');
expect(spy).lastCalledWith(stopScreenSharingEventLabel, 10);
});
it('should call `updateState` correctly', () => {
const setStateEventLabel = 'setState';
const spy = jest.spyOn(
ScreenSharingIndicator.prototype,
setStateEventLabel,
);
shallow(React.createElement(ScreenSharingIndicator));
ipcRenderer.send(
screenSharingIndicatorDataEventLabel,
screenSharingIndicatorStateMock,
);
expect(spy).lastCalledWith({ id: 10 });
});
describe('`screen-sharing-indicator-data` event', () => {
it('should call `screen-sharing-indicator-data` when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onEventLabel);
shallow(React.createElement(ScreenSharingIndicator));
expect(spy).lastCalledWith(
screenSharingIndicatorDataEventLabel,
expect.any(Function),
);
});
it('should call `stopScreenShare` correctly', () => {
const customSelector = 'button.stop-sharing-button';
const stopScreenSharingEventLabel = 'stop-screen-sharing';
const spy = jest.spyOn(ipcRenderer, sendEventLabel);
const wrapper = shallow(React.createElement(ScreenSharingIndicator));
wrapper.setState(screenSharingIndicatorStateMock);
wrapper.find(customSelector).simulate('click');
expect(spy).lastCalledWith(stopScreenSharingEventLabel, 10);
});
it('should call `updateState` correctly', () => {
const setStateEventLabel = 'setState';
const spy = jest.spyOn(ScreenSharingIndicator.prototype, setStateEventLabel);
shallow(React.createElement(ScreenSharingIndicator));
ipcRenderer.send(screenSharingIndicatorDataEventLabel, screenSharingIndicatorStateMock);
expect(spy).lastCalledWith({ id: 10 });
});
describe('`screen-sharing-indicator-data` event', () => {
it('should call `screen-sharing-indicator-data` when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onEventLabel);
shallow(React.createElement(ScreenSharingIndicator));
expect(spy).lastCalledWith(screenSharingIndicatorDataEventLabel, expect.any(Function));
});
it('should call `screen-sharing-indicator-data` when component is unmounted', () => {
const spy = jest.spyOn(ipcRenderer, removeListenerEventLabel);
shallow(React.createElement(ScreenSharingIndicator)).unmount();
expect(spy).toBeCalledWith(screenSharingIndicatorDataEventLabel, expect.any(Function));
});
it('should call `screen-sharing-indicator-data` when component is unmounted', () => {
const spy = jest.spyOn(ipcRenderer, removeListenerEventLabel);
shallow(React.createElement(ScreenSharingIndicator)).unmount();
expect(spy).toBeCalledWith(
screenSharingIndicatorDataEventLabel,
expect.any(Function),
);
});
});
});

View File

@ -3,7 +3,8 @@ import * as React from 'react';
import SnippingTool from '../src/renderer/components/snipping-tool';
import { ipcRenderer } from './__mocks__/electron';
const waitForPromisesToResolve = () => new Promise((resolve) => setTimeout(resolve));
const waitForPromisesToResolve = () =>
new Promise((resolve) => setTimeout(resolve));
afterEach(() => {
jest.clearAllMocks();
@ -23,14 +24,20 @@ describe('Snipping Tool', () => {
it('should send screenshot_taken BI event on component mount', () => {
const spy = jest.spyOn(ipcRenderer, 'send');
const expectedValue = { type: 'screenshot_taken', element: 'screen_capture_annotate' };
const expectedValue = {
type: 'screenshot_taken',
element: 'screen_capture_annotate',
};
mount(React.createElement(SnippingTool));
expect(spy).toBeCalledWith('snippet-analytics-data', expectedValue);
});
it('should send capture_sent BI event when clicking done', async () => {
const spy = jest.spyOn(ipcRenderer, 'send');
const expectedValue = { type: 'annotate_done', element: 'screen_capture_annotate' };
const expectedValue = {
type: 'annotate_done',
element: 'screen_capture_annotate',
};
const wrapper = mount(React.createElement(SnippingTool));
wrapper.find('[data-testid="done-button"]').simulate('click');
wrapper.update();
@ -40,7 +47,10 @@ describe('Snipping Tool', () => {
it('should send annotate_cleared BI event when clicking clear', async () => {
const spy = jest.spyOn(ipcRenderer, 'send');
const expectedValue = { type: 'annotate_cleared', element: 'screen_capture_annotate' };
const expectedValue = {
type: 'annotate_cleared',
element: 'screen_capture_annotate',
};
const wrapper = mount(React.createElement(SnippingTool));
wrapper.find('[data-testid="clear-button"]').simulate('click');
wrapper.update();
@ -57,31 +67,38 @@ describe('Snipping Tool', () => {
it('should render highlight color picker when clicked on highlight', () => {
const wrapper = shallow(React.createElement(SnippingTool));
wrapper.find('[data-testid="highlight-button"]').simulate('click');
expect(wrapper.find('[data-testid="highlight-colorpicker"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="highlight-colorpicker"]').exists()).toBe(
true,
);
});
it('should clear all paths when clicked on clear', () => {
const props = {
existingPaths: [{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
}],
existingPaths: [
{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
},
],
};
const wrapper = mount(<SnippingTool {...props} />);
const annotateComponent = wrapper.find('[data-testid="annotate-component"]');
const annotateComponent = wrapper.find(
'[data-testid="annotate-component"]',
);
wrapper.find('[data-testid="clear-button"]').simulate('click');
wrapper.update();
expect(annotateComponent.prop('paths')).toEqual(
[{
expect(annotateComponent.prop('paths')).toEqual([
{
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: false,
strokeWidth: 5,
}]);
},
]);
});
it('should send upload-snippet event with correct data when clicked on done', async () => {

View File

@ -1,31 +1,29 @@
import * as ttlHandler from '../src/app/ttl-handler';
describe('ttl handler', () => {
beforeEach(() => {
jest.resetModules();
});
beforeEach(() => {
jest.resetModules();
});
it('should return -1 for getExpiryTime', () => {
expect(ttlHandler.getExpiryTime()).toBeDefined();
});
it('should return -1 for getExpiryTime', () => {
expect(ttlHandler.getExpiryTime()).toBeDefined();
});
it('should return true if build is expired', () => {
const expiryMock = jest.spyOn(ttlHandler, 'getExpiryTime');
expiryMock.mockImplementation(() => Date.now() - 10 * 24 * 60);
expect(ttlHandler.checkIfBuildExpired()).toBeTruthy();
});
it('should return true if build is expired', () => {
const expiryMock = jest.spyOn(ttlHandler, 'getExpiryTime');
expiryMock.mockImplementation(() => Date.now() - 10 * 24 * 60);
expect(ttlHandler.checkIfBuildExpired()).toBeTruthy();
});
it('should return false if build is valid', () => {
const expiryMock = jest.spyOn(ttlHandler, 'getExpiryTime');
expiryMock.mockImplementation(() => Date.now() + 10 * 24 * 60);
expect(ttlHandler.checkIfBuildExpired()).toBeFalsy();
});
it('should return false if ttl is not applicable', () => {
const expiryMock = jest.spyOn(ttlHandler, 'getExpiryTime');
expiryMock.mockImplementation(() => -1);
expect(ttlHandler.checkIfBuildExpired()).toBeFalsy();
});
it('should return false if build is valid', () => {
const expiryMock = jest.spyOn(ttlHandler, 'getExpiryTime');
expiryMock.mockImplementation(() => Date.now() + 10 * 24 * 60);
expect(ttlHandler.checkIfBuildExpired()).toBeFalsy();
});
it('should return false if ttl is not applicable', () => {
const expiryMock = jest.spyOn(ttlHandler, 'getExpiryTime');
expiryMock.mockImplementation(() => -1);
expect(ttlHandler.checkIfBuildExpired()).toBeFalsy();
});
});

View File

@ -1,162 +1,175 @@
import { compareVersions, formatString, getCommandLineArgs, getGuid, throttle, calculatePercentage } from '../src/common/utils';
import {
compareVersions,
formatString,
getCommandLineArgs,
getGuid,
throttle,
calculatePercentage,
} from '../src/common/utils';
describe('utils', () => {
describe('`compareVersions`', () => {
it('should return -1 when v1 and v2 are invalid values', () => {
expect(compareVersions('1.0-1-1-', '2.1-1-1-')).toBe(-1);
});
it('should return -1 when v1 < v2', () => {
expect(compareVersions('v1.0', 'v2.1')).toBe(-1);
});
it('should return 1 when v1 > v2', () => {
expect(compareVersions('v1.8', 'v1.1')).toBe(1);
});
it('should return 0 when v1 is equal to v2', () => {
expect(compareVersions('v1.0', 'v1.0')).toBe(0);
});
describe('`compareVersions` using dash', () => {
it('should return 0 when v1 is equal to v2', () => {
expect(compareVersions('v1.0.8-beta1', 'v1.0.8-beta1')).toBe(0);
});
it('should return 1 when v1 is `string` and v2 is `number`', () => {
expect(compareVersions('v1.0.8-beta', 'v1.0.8-1')).toBe(1);
});
it('should return -1 when v1 is `number` and v2 is `string`', () => {
expect(compareVersions('v1.0.8-9', 'v1.0.8-beta')).toBe(-1);
});
it('should return -1 when v1 < v2', () => {
expect(compareVersions('v1.0.0-beta1', 'v1.0.0-beta2')).toBe(-1);
});
it('should return 1 when v1 > v2', () => {
expect(compareVersions('v1.0.0-beta5', 'v1.0.0-beta1')).toBe(1);
});
it('should return -1 when v1 is dashed and v2 is not', () => {
expect(compareVersions('v1.0.0-beta5', 'v1.0.0')).toBe(-1);
});
});
});
describe('`getGuid`', () => {
it('should call `getGuid` correctly', () => {
const valueFirst = getGuid();
const valueSecond = getGuid();
expect(valueFirst).not.toEqual(valueSecond);
});
it('should return 4 dashes when `getGuid` is called', () => {
const dashCountGui = getGuid().split('-').length - 1;
expect(dashCountGui).toBe(4);
});
it('should return 36 length when `getGuid` is called', () => {
const lengthGui = getGuid().length;
expect(lengthGui).toBe(36);
});
describe('`compareVersions`', () => {
it('should return -1 when v1 and v2 are invalid values', () => {
expect(compareVersions('1.0-1-1-', '2.1-1-1-')).toBe(-1);
});
describe('`getCommandLineArgs`', () => {
const argName = '--url=';
it('should call `getCommandLineArgs` correctly', () => {
const myCustomArgs = process.argv;
myCustomArgs.push(`--url='https://corporate.symphony.com'`);
const expectedValue = `--url='https://corporate.symphony.com'`;
expect(getCommandLineArgs(myCustomArgs, argName, false)).toBe(expectedValue);
});
it('should fail when the argv is invalid', () => {
const fakeValue: any = 'null';
const expectedValue = `get-command-line-args: TypeError invalid func arg, must be an array: ${fakeValue}`;
expect(() => {
getCommandLineArgs(fakeValue as string[], argName, false);
}).toThrow(expectedValue);
});
it('should return -1 when v1 < v2', () => {
expect(compareVersions('v1.0', 'v2.1')).toBe(-1);
});
describe('`formatString', () => {
const str = 'this will log {time}';
const strReplaced = 'this will log time';
const data = { time: '1234' };
it('should return `str` when data is empty', () => {
expect(formatString(str)).toEqual(str);
});
it('should replace key to dynamics values when `formatString` is used', () => {
const expectedValue = 'this will log 1234';
expect(formatString(str, data)).toBe(expectedValue);
});
it('should replace multiple keys to dynamics values when `formatString` is used', () => {
const dataTest = { multiple: true, placed: 'correct' };
expect(formatString('The string with {multiple} values {placed}', dataTest)).toBe('The string with true values correct');
});
it('should return `str` when `data` not match', () => {
const dataTest = { test: 123 };
expect(formatString(str, dataTest)).toBe(strReplaced);
});
it('should return `str` when `data` is undefined', () => {
const dataTest = { multiple: 'multiple', invalid: undefined };
expect(formatString('The string with {multiple} values {invalid}', dataTest)).toBe('The string with multiple values invalid');
});
it('should return 1 when v1 > v2', () => {
expect(compareVersions('v1.8', 'v1.1')).toBe(1);
});
describe('`throttle`', () => {
let origNow;
let now;
beforeEach(() => {
origNow = Date.now;
// mock date func
Date.now = () => {
return now;
};
now = 10000;
});
afterEach(() => {
// restore original
Date.now = origNow;
});
it('should fail when wait is invalid', () => {
const functionMock = jest.fn();
const invalidTime = -1;
const expectedValue = `throttle: invalid throttleTime arg, must be a number: ${invalidTime}`;
expect(() => {
throttle(functionMock, invalidTime);
}).toThrow(expectedValue);
});
it('should call `throttle` correctly', () => {
jest.useFakeTimers();
const validTime = 1000;
const functionMock = jest.fn();
const tempFn = throttle(functionMock, validTime);
for (let i = 0; i < 3; i++) {
tempFn();
}
now += 1000;
jest.runTimersToTime(1000);
tempFn();
expect(functionMock).toBeCalledTimes(2);
});
it('should return 0 when v1 is equal to v2', () => {
expect(compareVersions('v1.0', 'v1.0')).toBe(0);
});
describe('calculatePercentage', () => {
it('should calculate the percentage correctly', () => {
expect(calculatePercentage(1440, 90)).toBe(1296);
});
describe('`compareVersions` using dash', () => {
it('should return 0 when v1 is equal to v2', () => {
expect(compareVersions('v1.0.8-beta1', 'v1.0.8-beta1')).toBe(0);
});
it('should calculate the percentage correctly for 50', () => {
expect(calculatePercentage(500, 50)).toBe(250);
});
it('should return 1 when v1 is `string` and v2 is `number`', () => {
expect(compareVersions('v1.0.8-beta', 'v1.0.8-1')).toBe(1);
});
it('should return -1 when v1 is `number` and v2 is `string`', () => {
expect(compareVersions('v1.0.8-9', 'v1.0.8-beta')).toBe(-1);
});
it('should return -1 when v1 < v2', () => {
expect(compareVersions('v1.0.0-beta1', 'v1.0.0-beta2')).toBe(-1);
});
it('should return 1 when v1 > v2', () => {
expect(compareVersions('v1.0.0-beta5', 'v1.0.0-beta1')).toBe(1);
});
it('should return -1 when v1 is dashed and v2 is not', () => {
expect(compareVersions('v1.0.0-beta5', 'v1.0.0')).toBe(-1);
});
});
});
describe('`getGuid`', () => {
it('should call `getGuid` correctly', () => {
const valueFirst = getGuid();
const valueSecond = getGuid();
expect(valueFirst).not.toEqual(valueSecond);
});
it('should return 4 dashes when `getGuid` is called', () => {
const dashCountGui = getGuid().split('-').length - 1;
expect(dashCountGui).toBe(4);
});
it('should return 36 length when `getGuid` is called', () => {
const lengthGui = getGuid().length;
expect(lengthGui).toBe(36);
});
});
describe('`getCommandLineArgs`', () => {
const argName = '--url=';
it('should call `getCommandLineArgs` correctly', () => {
const myCustomArgs = process.argv;
myCustomArgs.push(`--url='https://corporate.symphony.com'`);
const expectedValue = `--url='https://corporate.symphony.com'`;
expect(getCommandLineArgs(myCustomArgs, argName, false)).toBe(
expectedValue,
);
});
it('should fail when the argv is invalid', () => {
const fakeValue: any = 'null';
const expectedValue = `get-command-line-args: TypeError invalid func arg, must be an array: ${fakeValue}`;
expect(() => {
getCommandLineArgs(fakeValue as string[], argName, false);
}).toThrow(expectedValue);
});
});
describe('`formatString', () => {
const str = 'this will log {time}';
const strReplaced = 'this will log time';
const data = { time: '1234' };
it('should return `str` when data is empty', () => {
expect(formatString(str)).toEqual(str);
});
it('should replace key to dynamics values when `formatString` is used', () => {
const expectedValue = 'this will log 1234';
expect(formatString(str, data)).toBe(expectedValue);
});
it('should replace multiple keys to dynamics values when `formatString` is used', () => {
const dataTest = { multiple: true, placed: 'correct' };
expect(
formatString('The string with {multiple} values {placed}', dataTest),
).toBe('The string with true values correct');
});
it('should return `str` when `data` not match', () => {
const dataTest = { test: 123 };
expect(formatString(str, dataTest)).toBe(strReplaced);
});
it('should return `str` when `data` is undefined', () => {
const dataTest = { multiple: 'multiple', invalid: undefined };
expect(
formatString('The string with {multiple} values {invalid}', dataTest),
).toBe('The string with multiple values invalid');
});
});
describe('`throttle`', () => {
let origNow;
let now;
beforeEach(() => {
origNow = Date.now;
// mock date func
Date.now = () => {
return now;
};
now = 10000;
});
afterEach(() => {
// restore original
Date.now = origNow;
});
it('should fail when wait is invalid', () => {
const functionMock = jest.fn();
const invalidTime = -1;
const expectedValue = `throttle: invalid throttleTime arg, must be a number: ${invalidTime}`;
expect(() => {
throttle(functionMock, invalidTime);
}).toThrow(expectedValue);
});
it('should call `throttle` correctly', () => {
jest.useFakeTimers();
const validTime = 1000;
const functionMock = jest.fn();
const tempFn = throttle(functionMock, validTime);
for (let i = 0; i < 3; i++) {
tempFn();
}
now += 1000;
jest.runTimersToTime(1000);
tempFn();
expect(functionMock).toBeCalledTimes(2);
});
});
describe('calculatePercentage', () => {
it('should calculate the percentage correctly', () => {
expect(calculatePercentage(1440, 90)).toBe(1296);
});
it('should calculate the percentage correctly for 50', () => {
expect(calculatePercentage(500, 50)).toBe(250);
});
});
});

View File

@ -4,129 +4,131 @@ import Welcome from '../src/renderer/components/welcome';
import { ipcRenderer } from './__mocks__/electron';
describe('welcome', () => {
const welcomeLabel = 'welcome';
const welcomeMock = {
url: 'https://my.symphony.com',
message: '',
urlValid: true,
sso: false,
const welcomeLabel = 'welcome';
const welcomeMock = {
url: 'https://my.symphony.com',
message: '',
urlValid: true,
sso: false,
};
const onLabelEvent = 'on';
const removeListenerLabelEvent = 'removeListener';
it('should render correctly', () => {
const wrapper = shallow(React.createElement(Welcome));
expect(wrapper).toMatchSnapshot();
});
it('should call `welcome` event when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(Welcome));
expect(spy).toBeCalledWith(welcomeLabel, expect.any(Function));
});
it('should remove listener `welcome` when component is unmounted', () => {
const spyMount = jest.spyOn(ipcRenderer, onLabelEvent);
const spyUnmount = jest.spyOn(ipcRenderer, removeListenerLabelEvent);
const wrapper = shallow(React.createElement(Welcome));
expect(spyMount).toBeCalledWith(welcomeLabel, expect.any(Function));
wrapper.unmount();
expect(spyUnmount).toBeCalledWith(welcomeLabel, expect.any(Function));
});
it('should call `updateState` when component is mounted', () => {
const spy = jest.spyOn(Welcome.prototype, 'setState');
shallow(React.createElement(Welcome));
ipcRenderer.send('welcome', welcomeMock);
expect(spy).toBeCalledWith(welcomeMock);
});
it('should change pod url in text box', () => {
const podUrlMock = {
url: 'https://corporate.symphony.com',
message: '',
urlValid: true,
sso: false,
};
const onLabelEvent = 'on';
const removeListenerLabelEvent = 'removeListener';
it('should render correctly', () => {
const wrapper = shallow(React.createElement(Welcome));
expect(wrapper).toMatchSnapshot();
const spy = jest.spyOn(Welcome.prototype, 'setState');
const updatePodUrlSpy = jest.spyOn(Welcome.prototype, 'updatePodUrl');
const wrapper = shallow(React.createElement(Welcome));
ipcRenderer.send('welcome', welcomeMock);
const welcomePodUrlBox = `input.Welcome-main-container-podurl-box`;
const input = wrapper.find(welcomePodUrlBox);
input.simulate('focus');
input.simulate('change', {
target: { value: 'https://corporate.symphony.com' },
});
it('should call `welcome` event when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(Welcome));
expect(spy).toBeCalledWith(welcomeLabel, expect.any(Function));
});
expect(updatePodUrlSpy).toBeCalled();
expect(spy).toBeCalledWith(podUrlMock);
});
it('should remove listener `welcome` when component is unmounted', () => {
const spyMount = jest.spyOn(ipcRenderer, onLabelEvent);
const spyUnmount = jest.spyOn(ipcRenderer, removeListenerLabelEvent);
it('should show message for invalid pod url', () => {
const podUrlMock = {
url: 'abcdef',
message: 'Please enter a valid url',
urlValid: false,
sso: false,
};
const wrapper = shallow(React.createElement(Welcome));
expect(spyMount).toBeCalledWith(welcomeLabel, expect.any(Function));
const spy = jest.spyOn(Welcome.prototype, 'setState');
const updatePodUrlSpy = jest.spyOn(Welcome.prototype, 'updatePodUrl');
wrapper.unmount();
expect(spyUnmount).toBeCalledWith(welcomeLabel, expect.any(Function));
});
const wrapper = shallow(React.createElement(Welcome));
ipcRenderer.send('welcome', welcomeMock);
it('should call `updateState` when component is mounted', () => {
const spy = jest.spyOn(Welcome.prototype, 'setState');
shallow(React.createElement(Welcome));
const welcomePodUrlBox = `input.Welcome-main-container-podurl-box`;
const input = wrapper.find(welcomePodUrlBox);
ipcRenderer.send('welcome', welcomeMock);
input.simulate('focus');
input.simulate('change', { target: { value: 'abcdef' } });
expect(spy).toBeCalledWith(welcomeMock);
});
expect(updatePodUrlSpy).toBeCalled();
expect(spy).toBeCalledWith(podUrlMock);
});
it('should change pod url in text box', () => {
const podUrlMock = {
url: 'https://corporate.symphony.com',
message: '',
urlValid: true,
sso: false,
};
it('should click sso checkbox', () => {
const podUrlMock = {
url: 'https://my.symphony.com',
message: '',
urlValid: true,
sso: true,
};
const spy = jest.spyOn(Welcome.prototype, 'setState');
const updatePodUrlSpy = jest.spyOn(Welcome.prototype, 'updatePodUrl');
const spy = jest.spyOn(Welcome.prototype, 'setState');
const updatePodUrlSpy = jest.spyOn(Welcome.prototype, 'updateSsoCheckbox');
const wrapper = shallow(React.createElement(Welcome));
ipcRenderer.send('welcome', welcomeMock);
const wrapper = shallow(React.createElement(Welcome));
ipcRenderer.send('welcome', welcomeMock);
const welcomePodUrlBox = `input.Welcome-main-container-podurl-box`;
const input = wrapper.find(welcomePodUrlBox);
const welcomePodUrlBox = `input[type="checkbox"]`;
const input = wrapper.find(welcomePodUrlBox);
input.simulate('focus');
input.simulate('change', {target: {value: 'https://corporate.symphony.com'}});
input.simulate('focus');
input.simulate('change', { target: { checked: true } });
expect(updatePodUrlSpy).toBeCalled();
expect(spy).toBeCalledWith(podUrlMock);
});
expect(updatePodUrlSpy).toBeCalled();
expect(spy).toBeCalledWith(podUrlMock);
});
it('should show message for invalid pod url', () => {
const podUrlMock = {
url: 'abcdef',
message: 'Please enter a valid url',
urlValid: false,
sso: false,
};
it('should set pod url', () => {
const spy = jest.spyOn(Welcome.prototype, 'setState');
const setPodUrlSpy = jest.spyOn(Welcome.prototype, 'setPodUrl');
const spy = jest.spyOn(Welcome.prototype, 'setState');
const updatePodUrlSpy = jest.spyOn(Welcome.prototype, 'updatePodUrl');
const wrapper = shallow(React.createElement(Welcome));
ipcRenderer.send('welcome', welcomeMock);
const welcomeContinueButton = `button.Welcome-continue-button`;
wrapper.find(welcomeContinueButton).simulate('click');
const wrapper = shallow(React.createElement(Welcome));
ipcRenderer.send('welcome', welcomeMock);
const welcomePodUrlBox = `input.Welcome-main-container-podurl-box`;
const input = wrapper.find(welcomePodUrlBox);
input.simulate('focus');
input.simulate('change', {target: {value: 'abcdef'}});
expect(updatePodUrlSpy).toBeCalled();
expect(spy).toBeCalledWith(podUrlMock);
});
it('should click sso checkbox', () => {
const podUrlMock = {
url: 'https://my.symphony.com',
message: '',
urlValid: true,
sso: true,
};
const spy = jest.spyOn(Welcome.prototype, 'setState');
const updatePodUrlSpy = jest.spyOn(Welcome.prototype, 'updateSsoCheckbox');
const wrapper = shallow(React.createElement(Welcome));
ipcRenderer.send('welcome', welcomeMock);
const welcomePodUrlBox = `input[type="checkbox"]`;
const input = wrapper.find(welcomePodUrlBox);
input.simulate('focus');
input.simulate('change', {target: {checked: true}});
expect(updatePodUrlSpy).toBeCalled();
expect(spy).toBeCalledWith(podUrlMock);
});
it('should set pod url', () => {
const spy = jest.spyOn(Welcome.prototype, 'setState');
const setPodUrlSpy = jest.spyOn(Welcome.prototype, 'setPodUrl');
const wrapper = shallow(React.createElement(Welcome));
ipcRenderer.send('welcome', welcomeMock);
const welcomeContinueButton = `button.Welcome-continue-button`;
wrapper.find(welcomeContinueButton).simulate('click');
expect(setPodUrlSpy).toBeCalled();
expect(spy).toBeCalledWith(welcomeMock);
});
expect(setPodUrlSpy).toBeCalled();
expect(spy).toBeCalledWith(welcomeMock);
});
});

View File

@ -5,194 +5,210 @@ import { ipcRenderer, remote } from './__mocks__/electron';
// @ts-ignore
global.MutationObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
disconnect: jest.fn(),
observe: jest.fn(),
disconnect: jest.fn(),
}));
describe('windows title bar', () => {
beforeEach(() => {
// state initial
jest.spyOn(remote, 'getCurrentWindow').mockImplementation(() => {
return {
isFullScreen: jest.fn(() => {
return false;
}),
isMaximized: jest.fn(() => {
return false;
}),
on: jest.fn(),
removeListener: jest.fn(),
isDestroyed: jest.fn(() => {
return false;
}),
close: jest.fn(),
maximize: jest.fn(),
minimize: jest.fn(),
unmaximize: jest.fn(),
setFullScreen: jest.fn(),
};
});
});
const getCurrentWindowFnLabel = 'getCurrentWindow';
const onEventLabel = 'on';
const maximizeEventLabel = 'maximize';
const unmaximizeEventLabel = 'unmaximize';
const enterFullScreenEventLabel = 'enter-full-screen';
const leaveFullScreenEventLabel = 'leave-full-screen';
it('should render correctly', () => {
const wrapper = shallow(React.createElement(WindowsTitleBar));
expect(wrapper).toMatchSnapshot();
});
it('should mount correctly', () => {
const wrapper = shallow(React.createElement(WindowsTitleBar));
const instance: any = wrapper.instance();
const window = instance.window;
const spy = jest.spyOn(remote, getCurrentWindowFnLabel);
const spyWindow = jest.spyOn(window, onEventLabel);
expect(spy).toBeCalled();
expect(spyWindow).nthCalledWith(
1,
maximizeEventLabel,
expect.any(Function),
);
expect(spyWindow).nthCalledWith(
2,
unmaximizeEventLabel,
expect.any(Function),
);
expect(spyWindow).nthCalledWith(
3,
enterFullScreenEventLabel,
expect.any(Function),
);
expect(spyWindow).nthCalledWith(
4,
leaveFullScreenEventLabel,
expect.any(Function),
);
});
it('should call `close` correctly', () => {
const fnLabel = 'close';
const titleLabel = 'Close';
const wrapper = shallow(React.createElement(WindowsTitleBar));
const customSelector = `button.title-bar-button[title="${titleLabel}"]`;
const instance: any = wrapper.instance();
const window = instance.window;
const spy = jest.spyOn(window, fnLabel);
wrapper.find(customSelector).simulate('click');
expect(spy).toBeCalled();
});
it('should call `minimize` correctly', () => {
const fnLabel = 'minimize';
const titleLabel = 'Minimize';
const wrapper = shallow(React.createElement(WindowsTitleBar));
const customSelector = `button.title-bar-button[title="${titleLabel}"]`;
const instance: any = wrapper.instance();
const window = instance.window;
const spy = jest.spyOn(window, fnLabel);
wrapper.find(customSelector).simulate('click');
expect(spy).toBeCalled();
});
it('should call `showMenu` correctly', () => {
const titleLabel = 'Menu';
const symphonyApiLabel = 'symphony-api';
const expectedValue = {
cmd: 'popup-menu',
};
const spy = jest.spyOn(ipcRenderer, 'send');
const customSelector = `button.hamburger-menu-button[title="${titleLabel}"]`;
const wrapper = shallow(React.createElement(WindowsTitleBar));
wrapper.find(customSelector).simulate('click');
expect(spy).toBeCalledWith(symphonyApiLabel, expectedValue);
});
it('should call `onMouseDown` correctly', () => {
const titleLabel = 'Menu';
const customSelector = `button.hamburger-menu-button[title="${titleLabel}"]`;
const wrapper = shallow(React.createElement(WindowsTitleBar));
const event = {
preventDefault: jest.fn(),
};
const spy = jest.spyOn(event, 'preventDefault');
wrapper.find(customSelector).simulate('mouseDown', event);
expect(spy).toBeCalled();
});
it('should call `updateState` correctly', () => {
const wrapper = shallow(React.createElement(WindowsTitleBar));
const spy = jest.spyOn(wrapper, 'setState');
const instance: any = wrapper.instance();
instance.updateState({ isMaximized: false });
expect(spy).lastCalledWith(expect.any(Function));
});
describe('componentDidMount event', () => {
beforeEach(() => {
// state initial
jest.spyOn(remote, 'getCurrentWindow').mockImplementation(() => {
return {
isFullScreen: jest.fn(() => {
return false;
}),
isMaximized: jest.fn(() => {
return false;
}),
on: jest.fn(),
removeListener: jest.fn(),
isDestroyed: jest.fn(() => {
return false;
}),
close: jest.fn(),
maximize: jest.fn(),
minimize: jest.fn(),
unmaximize: jest.fn(),
setFullScreen: jest.fn(),
};
});
document.body.innerHTML = `<div id="content-wrapper"></div>`;
});
const getCurrentWindowFnLabel = 'getCurrentWindow';
const onEventLabel = 'on';
const maximizeEventLabel = 'maximize';
const unmaximizeEventLabel = 'unmaximize';
const enterFullScreenEventLabel = 'enter-full-screen';
const leaveFullScreenEventLabel = 'leave-full-screen';
it('should render correctly', () => {
const wrapper = shallow(React.createElement(WindowsTitleBar));
expect(wrapper).toMatchSnapshot();
});
it('should mount correctly', () => {
const wrapper = shallow(React.createElement(WindowsTitleBar));
const instance: any = wrapper.instance();
const window = instance.window;
const spy = jest.spyOn(remote, getCurrentWindowFnLabel);
const spyWindow = jest.spyOn(window, onEventLabel);
expect(spy).toBeCalled();
expect(spyWindow).nthCalledWith(1, maximizeEventLabel, expect.any(Function));
expect(spyWindow).nthCalledWith(2, unmaximizeEventLabel, expect.any(Function));
expect(spyWindow).nthCalledWith(3, enterFullScreenEventLabel, expect.any(Function));
expect(spyWindow).nthCalledWith(4, leaveFullScreenEventLabel, expect.any(Function));
});
it('should call `close` correctly', () => {
const fnLabel = 'close';
const titleLabel = 'Close';
const wrapper = shallow(React.createElement(WindowsTitleBar));
const customSelector = `button.title-bar-button[title="${titleLabel}"]`;
const instance: any = wrapper.instance();
const window = instance.window;
const spy = jest.spyOn(window, fnLabel);
wrapper.find(customSelector).simulate('click');
expect(spy).toBeCalled();
});
it('should call `minimize` correctly', () => {
const fnLabel = 'minimize';
const titleLabel = 'Minimize';
const wrapper = shallow(React.createElement(WindowsTitleBar));
const customSelector = `button.title-bar-button[title="${titleLabel}"]`;
const instance: any = wrapper.instance();
const window = instance.window;
const spy = jest.spyOn(window, fnLabel);
wrapper.find(customSelector).simulate('click');
expect(spy).toBeCalled();
});
it('should call `showMenu` correctly', () => {
const titleLabel = 'Menu';
const symphonyApiLabel = 'symphony-api';
const expectedValue = {
cmd: 'popup-menu',
it('should call `componentDidMount` when isFullScreen', () => {
const spy = jest.spyOn(document.body.style, 'removeProperty');
const expectedValue = 'margin-top';
// changing state before componentDidMount
jest.spyOn(remote, 'getCurrentWindow').mockImplementation(() => {
return {
isFullScreen: jest.fn(() => {
return true;
}),
isMaximized: jest.fn(() => {
return false;
}),
on: jest.fn(),
removeListener: jest.fn(),
isDestroyed: jest.fn(() => {
return false;
}),
close: jest.fn(),
maximize: jest.fn(),
minimize: jest.fn(),
unmaximize: jest.fn(),
setFullScreen: jest.fn(),
};
const spy = jest.spyOn(ipcRenderer, 'send');
const customSelector = `button.hamburger-menu-button[title="${titleLabel}"]`;
const wrapper = shallow(React.createElement(WindowsTitleBar));
wrapper.find(customSelector).simulate('click');
expect(spy).toBeCalledWith(symphonyApiLabel, expectedValue);
});
shallow(React.createElement(WindowsTitleBar));
expect(spy).toBeCalledWith(expectedValue);
});
});
describe('maximize functions', () => {
it('should call `unmaximize` correctly when is not full screen', () => {
const titleLabel = 'Restore';
const unmaximizeFn = 'unmaximize';
const customSelector = `button.title-bar-button[title="${titleLabel}"]`;
const wrapper = shallow(React.createElement(WindowsTitleBar));
const instance: any = wrapper.instance();
const window = instance.window;
const spy = jest.spyOn(window, unmaximizeFn);
wrapper.setState({ isMaximized: true });
wrapper.find(customSelector).simulate('click');
expect(spy).toBeCalled();
});
it('should call `onMouseDown` correctly', () => {
const titleLabel = 'Menu';
const customSelector = `button.hamburger-menu-button[title="${titleLabel}"]`;
const wrapper = shallow(React.createElement(WindowsTitleBar));
const event = {
preventDefault: jest.fn(),
};
const spy = jest.spyOn(event, 'preventDefault');
wrapper.find(customSelector).simulate('mouseDown', event);
expect(spy).toBeCalled();
it('should call `unmaximize` correctly when is full screen', () => {
const windowSpyFn = 'setFullScreen';
const titleLabel = 'Restore';
const customSelector = `button.title-bar-button[title="${titleLabel}"]`;
const wrapper = shallow(React.createElement(WindowsTitleBar));
const instance: any = wrapper.instance();
const window = instance.window;
const spy = jest.spyOn(window, windowSpyFn);
window.isFullScreen = jest.fn(() => {
return true;
});
wrapper.setState({ isMaximized: true });
wrapper.find(customSelector).simulate('click');
expect(spy).toBeCalledWith(false);
});
it('should call `updateState` correctly', () => {
const wrapper = shallow(React.createElement(WindowsTitleBar));
const spy = jest.spyOn(wrapper, 'setState');
const instance: any = wrapper.instance();
instance.updateState({ isMaximized: false });
expect(spy).lastCalledWith(expect.any(Function));
});
describe('componentDidMount event', () => {
beforeEach(() => {
document.body.innerHTML = `<div id="content-wrapper"></div>`;
});
it('should call `componentDidMount` when isFullScreen', () => {
const spy = jest.spyOn(document.body.style, 'removeProperty');
const expectedValue = 'margin-top';
// changing state before componentDidMount
jest.spyOn(remote, 'getCurrentWindow').mockImplementation(() => {
return {
isFullScreen: jest.fn(() => {
return true;
}),
isMaximized: jest.fn(() => {
return false;
}),
on: jest.fn(),
removeListener: jest.fn(),
isDestroyed: jest.fn(() => {
return false;
}),
close: jest.fn(),
maximize: jest.fn(),
minimize: jest.fn(),
unmaximize: jest.fn(),
setFullScreen: jest.fn(),
};
});
shallow(React.createElement(WindowsTitleBar));
expect(spy).toBeCalledWith(expectedValue);
});
});
describe('maximize functions', () => {
it('should call `unmaximize` correctly when is not full screen', () => {
const titleLabel = 'Restore';
const unmaximizeFn = 'unmaximize';
const customSelector = `button.title-bar-button[title="${titleLabel}"]`;
const wrapper = shallow(React.createElement(WindowsTitleBar));
const instance: any = wrapper.instance();
const window = instance.window;
const spy = jest.spyOn(window, unmaximizeFn);
wrapper.setState({ isMaximized: true });
wrapper.find(customSelector).simulate('click');
expect(spy).toBeCalled();
});
it('should call `unmaximize` correctly when is full screen', () => {
const windowSpyFn = 'setFullScreen';
const titleLabel = 'Restore';
const customSelector = `button.title-bar-button[title="${titleLabel}"]`;
const wrapper = shallow(React.createElement(WindowsTitleBar));
const instance: any = wrapper.instance();
const window = instance.window;
const spy = jest.spyOn(window, windowSpyFn);
window.isFullScreen = jest.fn(() => {
return true;
});
wrapper.setState({ isMaximized: true });
wrapper.find(customSelector).simulate('click');
expect(spy).toBeCalledWith(false);
});
it('should call maximize correctly when it is not in full screen', () => {
const titleLabel = 'Maximize';
const maximizeFn = 'maximize';
const expectedState = { isMaximized: true };
const customSelector = `button.title-bar-button[title="${titleLabel}"]`;
const wrapper = shallow(React.createElement(WindowsTitleBar));
const instance: any = wrapper.instance();
const window = instance.window;
const spyWindow = jest.spyOn(window, maximizeFn);
const spyState = jest.spyOn(wrapper, 'setState');
wrapper.find(customSelector).simulate('click');
expect(spyWindow).toBeCalled();
expect(spyState).lastCalledWith(expectedState);
});
it('should call maximize correctly when it is not in full screen', () => {
const titleLabel = 'Maximize';
const maximizeFn = 'maximize';
const expectedState = { isMaximized: true };
const customSelector = `button.title-bar-button[title="${titleLabel}"]`;
const wrapper = shallow(React.createElement(WindowsTitleBar));
const instance: any = wrapper.instance();
const window = instance.window;
const spyWindow = jest.spyOn(window, maximizeFn);
const spyState = jest.spyOn(wrapper, 'setState');
wrapper.find(customSelector).simulate('click');
expect(spyWindow).toBeCalled();
expect(spyState).lastCalledWith(expectedState);
});
});
});

View File

@ -2,47 +2,56 @@ import test from 'ava';
import * as robot from 'robotjs';
import { Application } from 'spectron';
import { robotActions } from './fixtures/robot-actions';
import { loadURL, podUrl, sleep, startApplication, stopApplication, Timeouts } from './fixtures/spectron-setup';
import {
loadURL,
podUrl,
sleep,
startApplication,
stopApplication,
Timeouts,
} from './fixtures/spectron-setup';
let app;
test.before(async (t) => {
app = await startApplication() as Application;
t.true(app.isRunning());
app = (await startApplication()) as Application;
t.true(app.isRunning());
await loadURL(app, podUrl);
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
await loadURL(app, podUrl);
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
await sleep(Timeouts.fiveSec);
await sleep(Timeouts.fiveSec);
});
test.after.always(async () => {
await stopApplication(app);
await stopApplication(app);
});
test('about-app: verify about application feature', async (t) => {
robotActions.clickAppMenu();
robot.keyTap('down');
robot.keyTap('enter');
robotActions.clickAppMenu();
robot.keyTap('down');
robot.keyTap('enter');
// wait for about window to load
await sleep(Timeouts.halfSec);
await app.client.windowByIndex(1);
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
t.truthy(await app.browserWindow.getTitle(), 'About Symphony');
// wait for about window to load
await sleep(Timeouts.halfSec);
await app.client.windowByIndex(1);
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
t.truthy(await app.browserWindow.getTitle(), 'About Symphony');
});
test('about-app: verify copy button with few data validation', async (t) => {
await sleep(Timeouts.oneSec);
await app.client.click('.AboutApp-copy-button');
const clipboard = JSON.parse(await app.client.electron.remote.clipboard.readText());
await sleep(Timeouts.oneSec);
await app.client.click('.AboutApp-copy-button');
const clipboard = JSON.parse(
await app.client.electron.remote.clipboard.readText(),
);
t.log(clipboard);
t.true(clipboard.hasOwnProperty('appName'));
t.true(clipboard.hasOwnProperty('clientVersion'));
t.true(clipboard.hasOwnProperty('sfeVersion'));
t.true(clipboard.hasOwnProperty('sfeClientType'));
t.true(clipboard.hasOwnProperty('sdaVersion'));
t.true(clipboard.hasOwnProperty('sdaBuildNumber'));
robotActions.closeWindow();
t.log(clipboard);
t.true(clipboard.hasOwnProperty('appName'));
t.true(clipboard.hasOwnProperty('clientVersion'));
t.true(clipboard.hasOwnProperty('sfeVersion'));
t.true(clipboard.hasOwnProperty('sfeClientType'));
t.true(clipboard.hasOwnProperty('sdaVersion'));
t.true(clipboard.hasOwnProperty('sdaBuildNumber'));
robotActions.closeWindow();
});

View File

@ -3,75 +3,72 @@ import { isMac } from '../../src/common/env';
import { Timeouts } from './spectron-setup';
class RobotActions {
constructor() {
robot.setKeyboardDelay(Timeouts.oneSec);
robot.setMouseDelay(Timeouts.oneSec);
}
constructor() {
robot.setKeyboardDelay(Timeouts.oneSec);
robot.setMouseDelay(Timeouts.oneSec);
}
/**
* Closes window via keyboard action
*/
public closeWindow(): void {
const modifier = isMac ? ['command'] : ['control'];
robot.keyToggle('w', 'down', modifier);
robot.keyToggle('w', 'up', modifier);
}
/**
* Closes window via keyboard action
*/
public closeWindow(): void {
const modifier = isMac ? [ 'command' ] : [ 'control' ];
robot.keyToggle('w', 'down', modifier);
robot.keyToggle('w', 'up', modifier);
}
/**
* Makes the application fullscreen via keyboard
*/
public toggleFullscreen(): void {
robot.keyToggle('f', 'down', ['command', 'control']);
robot.keyToggle('f', 'up', ['command', 'control']);
}
/**
* Makes the application fullscreen via keyboard
*/
public toggleFullscreen(): void {
robot.keyToggle('f', 'down', [ 'command', 'control' ]);
robot.keyToggle('f', 'up', [ 'command', 'control' ]);
}
/**
* Zoom in via keyboard Command/Ctrl +
*/
public zoomIn(): void {
const modifier = isMac ? ['command'] : ['control'];
robot.keyToggle('+', 'down', modifier);
robot.keyToggle('+', 'up', modifier);
}
/**
* Zoom in via keyboard Command/Ctrl +
*/
public zoomIn(): void {
const modifier = isMac ? [ 'command' ] : [ 'control' ];
robot.keyToggle('+', 'down', modifier);
robot.keyToggle('+', 'up', modifier);
}
/**
* Zoom out via keyboard
*/
public zoomOut(): void {
const modifier = isMac ? ['command'] : ['control'];
robot.keyToggle('-', 'down', modifier);
robot.keyToggle('-', 'up', modifier);
}
/**
* Zoom out via keyboard
*/
public zoomOut(): void {
const modifier = isMac ? [ 'command' ] : [ 'control' ];
robot.keyToggle('-', 'down', modifier);
robot.keyToggle('-', 'up', modifier);
}
/**
* Zoom reset via keyboard
*/
public zoomReset(): void {
const modifier = isMac ? ['command'] : ['control'];
robot.keyToggle('0', 'down', modifier);
robot.keyToggle('0', 'up', modifier);
}
/**
* Zoom reset via keyboard
*/
public zoomReset(): void {
const modifier = isMac ? [ 'command' ] : [ 'control' ];
robot.keyToggle('0', 'down', modifier);
robot.keyToggle('0', 'up', modifier);
}
/**
* Click the App menu
*/
public clickAppMenu(point?: Electron.Point): void {
if (isMac) {
robot.moveMouse(83, 14);
robot.mouseClick();
} else {
if (!point) {
throw new Error('browser window points are required');
}
robot.moveMouse(point.x + 10, point.y + 14);
robot.mouseClick();
}
/**
* Click the App menu
*/
public clickAppMenu(point?: Electron.Point): void {
if (isMac) {
robot.moveMouse(83, 14);
robot.mouseClick();
} else {
if (!point) {
throw new Error('browser window points are required');
}
robot.moveMouse(point.x + 10, point.y + 14);
robot.mouseClick();
}
}
}
const robotActions = new RobotActions();
export {
robotActions,
};
export { robotActions };

View File

@ -4,47 +4,64 @@ import { Application, BasicAppSettings } from 'spectron';
export const podUrl = 'https://corporate.symphony.com';
export enum Timeouts {
halfSec = 500,
oneSec = 1000,
threeSec = 3000,
fiveSec = 5000,
tenSec = 10000,
halfSec = 500,
oneSec = 1000,
threeSec = 3000,
fiveSec = 5000,
tenSec = 10000,
}
/**
* Returns the electron executable path
*/
export const getElectronPath = (): string => {
let electronPath = path.join(__dirname, '..', '..', '..', 'node_modules', '.bin', 'electron');
if (process.platform === 'win32') {
electronPath += '.cmd';
}
return electronPath;
let electronPath = path.join(
__dirname,
'..',
'..',
'..',
'node_modules',
'.bin',
'electron',
);
if (process.platform === 'win32') {
electronPath += '.cmd';
}
return electronPath;
};
/**
* Returns the demo application html path
*/
export const getDemoFilePath = (): string => {
return `file://${path.join(__dirname, '..', '..', '..', '/src/demo/index.html')}`;
return `file://${path.join(
__dirname,
'..',
'..',
'..',
'/src/demo/index.html',
)}`;
};
/**
* Returns app init file
*/
export const getArgs = (): string[] => {
return [ path.join(__dirname, '..', '..', '/src/app/init.js') ];
return [path.join(__dirname, '..', '..', '/src/app/init.js')];
};
/**
* Stops the application
* @param application
*/
export const stopApplication = async (application): Promise<Application | undefined> => {
if (!application || !application.isRunning()) {
return;
}
return await application.stop();
export const stopApplication = async (
application,
): Promise<Application | undefined> => {
if (!application || !application.isRunning()) {
return;
}
await application.stop();
return;
};
/**
@ -53,19 +70,19 @@ export const stopApplication = async (application): Promise<Application | undefi
* @param options {BasicAppSettings}
*/
export const startApplication = async (
shouldLoadDemoApp: boolean = false,
options: BasicAppSettings = {
path: getElectronPath(),
args: getArgs(),
},
shouldLoadDemoApp: boolean = false,
options: BasicAppSettings = {
path: getElectronPath(),
args: getArgs(),
},
): Promise<Application> => {
// loads demo page correctly
if (shouldLoadDemoApp && options.args) {
options.args.push(`. --url=file://${getDemoFilePath()}`);
}
const application = new Application(options);
await application.start();
return application;
// loads demo page correctly
if (shouldLoadDemoApp && options.args) {
options.args.push(`. --url=file://${getDemoFilePath()}`);
}
const application = new Application(options);
await application.start();
return application;
};
/**
@ -73,9 +90,9 @@ export const startApplication = async (
* @param ms
*/
export const sleep = (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
/**
@ -85,13 +102,15 @@ export const sleep = (ms) => {
* @param url
*/
export const loadURL = async (app: Application, url: string): Promise<void> => {
try {
return await app.browserWindow.loadURL(url);
} catch (error) {
const errorIsNavigatedError: boolean = error.message.includes('Inspected target navigated or closed');
try {
return await app.browserWindow.loadURL(url);
} catch (error) {
const errorIsNavigatedError: boolean = error.message.includes(
'Inspected target navigated or closed',
);
if (!errorIsNavigatedError) {
throw error;
}
if (!errorIsNavigatedError) {
throw error;
}
}
};

View File

@ -4,31 +4,32 @@ import { Application } from 'spectron';
import { robotActions } from './fixtures/robot-actions';
import {
getDemoFilePath, loadURL,
sleep,
startApplication,
stopApplication,
Timeouts,
getDemoFilePath,
loadURL,
sleep,
startApplication,
stopApplication,
Timeouts,
} from './fixtures/spectron-setup';
let app;
test.before(async (t) => {
app = await startApplication() as Application;
t.true(app.isRunning());
app = (await startApplication()) as Application;
t.true(app.isRunning());
});
test.after.always(async () => {
await stopApplication(app);
await stopApplication(app);
});
test('fullscreen: verify application full screen feature', async (t) => {
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
robotActions.toggleFullscreen();
t.true(await app.browserWindow.isFullScreen());
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
robotActions.toggleFullscreen();
t.true(await app.browserWindow.isFullScreen());
await sleep(Timeouts.halfSec);
robot.keyTap('escape');
t.false(await app.browserWindow.isFullScreen());
await sleep(Timeouts.halfSec);
robot.keyTap('escape');
t.false(await app.browserWindow.isFullScreen());
});

View File

@ -7,28 +7,28 @@ import { startApplication, stopApplication } from './fixtures/spectron-setup';
let app;
test.before(async (t) => {
app = await startApplication() as Application;
t.true(app.isRunning());
app = (await startApplication()) as Application;
t.true(app.isRunning());
});
test.after.always(async () => {
await stopApplication(app);
await stopApplication(app);
});
test('minimize: verify application minimize / maximize feature', async (t) => {
const win = app.browserWindow;
win.minimize();
t.true(await win.isMinimized());
const win = app.browserWindow;
win.minimize();
t.true(await win.isMinimized());
win.restore();
t.true(await win.isVisible());
win.restore();
t.true(await win.isVisible());
});
test('minimize: verify application to be minimized with keyboard accelerator', async (t) => {
const win = app.browserWindow;
robotActions.closeWindow();
t.false(await win.isVisible());
const win = app.browserWindow;
robotActions.closeWindow();
t.false(await win.isVisible());
win.restore();
t.true(await win.isVisible());
win.restore();
t.true(await win.isVisible());
});

View File

@ -1,47 +1,54 @@
import test from 'ava';
import { Application } from 'spectron';
import { getDemoFilePath, loadURL, sleep, startApplication, stopApplication, Timeouts } from './fixtures/spectron-setup';
import {
getDemoFilePath,
loadURL,
sleep,
startApplication,
stopApplication,
Timeouts,
} from './fixtures/spectron-setup';
let app;
test.before(async (t) => {
app = await startApplication(true) as Application;
t.true(app.isRunning());
app = (await startApplication(true)) as Application;
t.true(app.isRunning());
});
test.after.always(async () => {
await stopApplication(app);
await stopApplication(app);
});
test('Verify is the application is running', async (t) => {
t.true(app.isRunning());
t.true(app.isRunning());
});
test('Verify notification window is created', async (t) => {
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
await app.client.click('#notf');
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
await app.client.click('#notf');
await sleep(Timeouts.oneSec);
t.timeout(10000);
t.is(await app.client.getWindowCount(), 2);
await app.client.windowByIndex(1);
await app.client.click('.close');
await sleep(Timeouts.oneSec);
t.timeout(10000);
t.is(await app.client.getWindowCount(), 2);
await app.client.windowByIndex(1);
await app.client.click('.close');
await sleep(2000);
await app.client.windowByIndex(0);
await sleep(2000);
await app.client.windowByIndex(0);
});
test('Verify notification window is hidden', async (t) => {
await app.client.click('#notf');
await app.client.click('#notf');
await sleep(Timeouts.oneSec);
t.timeout(Timeouts.fiveSec);
await app.client.windowByIndex(1);
await app.client.click('.close');
await sleep(Timeouts.oneSec);
t.timeout(Timeouts.fiveSec);
await app.client.windowByIndex(1);
await app.client.click('.close');
await sleep(2000);
await app.client.windowByIndex(0);
t.is(await app.client.getWindowCount(), 2);
await sleep(2000);
await app.client.windowByIndex(0);
t.is(await app.client.getWindowCount(), 2);
});

View File

@ -3,54 +3,55 @@ import * as robot from 'robotjs';
import { Application } from 'spectron';
import {
getDemoFilePath, loadURL,
sleep,
startApplication,
stopApplication,
Timeouts,
getDemoFilePath,
loadURL,
sleep,
startApplication,
stopApplication,
Timeouts,
} from './fixtures/spectron-setup';
let app;
export const openScreenPicker = async (window) => {
if (!window) {
throw new Error('openScreenPicker: must be called with Application');
}
await window.client.scroll(125, 1000);
await sleep(Timeouts.halfSec);
await window.client.click('#get-sources');
await window.client.waitUntilWindowLoaded(Timeouts.fiveSec);
if (!window) {
throw new Error('openScreenPicker: must be called with Application');
}
await window.client.scroll(125, 1000);
await sleep(Timeouts.halfSec);
await window.client.click('#get-sources');
await window.client.waitUntilWindowLoaded(Timeouts.fiveSec);
};
test.before(async (t) => {
app = await startApplication() as Application;
t.true(app.isRunning());
app = (await startApplication()) as Application;
t.true(app.isRunning());
});
test.after.always(async () => {
await stopApplication(app);
await stopApplication(app);
});
test('screen-picker: verify screen-picker close button', async (t) => {
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
await openScreenPicker(app);
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
await openScreenPicker(app);
await sleep(Timeouts.halfSec);
t.is(await app.client.getWindowCount(), 2);
await app.client.windowByIndex(1);
await app.client.click('.ScreenPicker-x-button');
await sleep(Timeouts.halfSec);
t.is(await app.client.getWindowCount(), 1);
await sleep(Timeouts.halfSec);
t.is(await app.client.getWindowCount(), 2);
await app.client.windowByIndex(1);
await app.client.click('.ScreenPicker-x-button');
await sleep(Timeouts.halfSec);
t.is(await app.client.getWindowCount(), 1);
});
test('screen-picker: verify screen-picker escape keyboard actions', async (t) => {
await app.client.windowByIndex(0);
await openScreenPicker(app);
await app.client.windowByIndex(0);
await openScreenPicker(app);
await sleep(Timeouts.halfSec);
t.is(await app.client.getWindowCount(), 2);
robot.keyTap('escape');
await sleep(Timeouts.halfSec);
t.is(await app.client.getWindowCount(), 1);
await sleep(Timeouts.halfSec);
t.is(await app.client.getWindowCount(), 2);
robot.keyTap('escape');
await sleep(Timeouts.halfSec);
t.is(await app.client.getWindowCount(), 1);
});

View File

@ -3,66 +3,67 @@ import * as robot from 'robotjs';
import { Application } from 'spectron';
import {
getDemoFilePath, loadURL,
sleep,
startApplication,
stopApplication,
Timeouts,
getDemoFilePath,
loadURL,
sleep,
startApplication,
stopApplication,
Timeouts,
} from './fixtures/spectron-setup';
let app;
export const openScreenPicker = async (window) => {
if (!window) {
throw new Error('openScreenPicker: must be called with Application');
}
await window.client.scroll(125, 1000);
await sleep(Timeouts.halfSec);
await window.client.click('#get-sources');
await window.client.waitUntilWindowLoaded(Timeouts.fiveSec);
if (!window) {
throw new Error('openScreenPicker: must be called with Application');
}
await window.client.scroll(125, 1000);
await sleep(Timeouts.halfSec);
await window.client.click('#get-sources');
await window.client.waitUntilWindowLoaded(Timeouts.fiveSec);
};
test.before(async (t) => {
app = await startApplication() as Application;
t.true(app.isRunning());
app = (await startApplication()) as Application;
t.true(app.isRunning());
});
test.after.always(async () => {
await stopApplication(app);
await stopApplication(app);
});
test('screen-sharing-indicator: verify screen sharing indicator with frame is shown', async (t) => {
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
await openScreenPicker(app);
robot.setKeyboardDelay(2000);
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
await openScreenPicker(app);
robot.setKeyboardDelay(2000);
await sleep(Timeouts.halfSec);
t.is(await app.client.getWindowCount(), 2);
await app.client.windowByIndex(1);
// will select the entire screen option in the picker
robot.keyTap('right');
robot.keyTap('left');
robot.keyTap('enter');
await sleep(Timeouts.halfSec);
t.is(await app.client.getWindowCount(), 2);
await app.client.windowByIndex(1);
// will select the entire screen option in the picker
robot.keyTap('right');
robot.keyTap('left');
robot.keyTap('enter');
await sleep(2000);
t.is(await app.client.getWindowCount(), 2);
await sleep(2000);
t.is(await app.client.getWindowCount(), 2);
});
test('screen-sharing-indicator: verify screen sharing indicator title', async (t) => {
// including the screen sharing frame
// including the screen sharing frame
await app.client.windowByIndex(1);
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
const indicatorTitle = await app.browserWindow.getTitle();
if (indicatorTitle !== 'Screen Sharing Indicator - Symphony') {
await app.client.windowByIndex(1);
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
}
const indicatorTitle = await app.browserWindow.getTitle();
if (indicatorTitle !== 'Screen Sharing Indicator - Symphony') {
await app.client.windowByIndex(1);
}
await app.client.click('.stop-sharing-button');
await app.client.windowByIndex(0);
await sleep(Timeouts.halfSec);
// verify both frame and indicator are closed
// when stop button is clicked
t.is(await app.client.getWindowCount(), 1);
await app.client.click('.stop-sharing-button');
await app.client.windowByIndex(0);
await sleep(Timeouts.halfSec);
// verify both frame and indicator are closed
// when stop button is clicked
t.is(await app.client.getWindowCount(), 1);
});

View File

@ -3,35 +3,41 @@ import * as robot from 'robotjs';
import { Application } from 'spectron';
import { getDemoFilePath, loadURL, startApplication, stopApplication, Timeouts } from './fixtures/spectron-setup';
import {
getDemoFilePath,
loadURL,
startApplication,
stopApplication,
Timeouts,
} from './fixtures/spectron-setup';
let app;
test.before(async (t) => {
app = await startApplication() as Application;
t.true(app.isRunning());
app = (await startApplication()) as Application;
t.true(app.isRunning());
});
test.after.always(async () => {
await stopApplication(app);
await stopApplication(app);
});
test('spell-checker: verify application spell checking feature', async (t) => {
robot.setKeyboardDelay(Timeouts.oneSec);
const missSpelledWord = 'teest ';
robot.setKeyboardDelay(Timeouts.oneSec);
const missSpelledWord = 'teest ';
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
await app.client.electron.remote.clipboard.writeText(missSpelledWord);
await app.client.click('#tag');
await app.client.webContents.paste();
await app.client.waitForValue('#tag', Timeouts.fiveSec);
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
await app.client.electron.remote.clipboard.writeText(missSpelledWord);
await app.client.click('#tag');
await app.client.webContents.paste();
await app.client.waitForValue('#tag', Timeouts.fiveSec);
t.is(await app.client.getValue('#tag'), missSpelledWord);
t.is(await app.client.getValue('#tag'), missSpelledWord);
await app.client.rightClick('#tag', 10, 10);
robot.keyTap('down');
robot.keyTap('enter');
await app.client.rightClick('#tag', 10, 10);
robot.keyTap('down');
robot.keyTap('enter');
t.not(await app.client.getValue('#tag'), missSpelledWord);
t.not(await app.client.getValue('#tag'), missSpelledWord);
});

View File

@ -3,40 +3,41 @@ import { Application } from 'spectron';
import { robotActions } from './fixtures/robot-actions';
import {
getDemoFilePath, loadURL,
sleep,
startApplication,
stopApplication,
Timeouts,
getDemoFilePath,
loadURL,
sleep,
startApplication,
stopApplication,
Timeouts,
} from './fixtures/spectron-setup';
let app;
test.before(async (t) => {
app = await startApplication() as Application;
t.true(app.isRunning());
app = (await startApplication()) as Application;
t.true(app.isRunning());
});
test.after.always(async () => {
await stopApplication(app);
await stopApplication(app);
});
test('zoom: verify application zoom feature', async (t) => {
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
await loadURL(app, getDemoFilePath());
await app.client.waitUntilWindowLoaded(Timeouts.fiveSec);
robotActions.zoomIn();
t.is(await app.webContents.getZoomLevel(), 0.5);
await sleep(Timeouts.oneSec);
robotActions.zoomIn();
t.is(await app.webContents.getZoomLevel(), 0.5);
await sleep(Timeouts.oneSec);
robotActions.zoomIn();
t.is(await app.webContents.getZoomLevel(), 1);
await sleep(Timeouts.oneSec);
robotActions.zoomIn();
t.is(await app.webContents.getZoomLevel(), 1);
await sleep(Timeouts.oneSec);
robotActions.zoomOut();
t.is(await app.webContents.getZoomLevel(), 0.5);
await sleep(Timeouts.oneSec);
robotActions.zoomOut();
t.is(await app.webContents.getZoomLevel(), 0.5);
await sleep(Timeouts.oneSec);
robotActions.zoomReset();
t.is(await app.webContents.getZoomLevel(), 0);
robotActions.zoomReset();
t.is(await app.webContents.getZoomLevel(), 0);
});

View File

@ -1,95 +1,103 @@
import * as electron from 'electron';
import { app } from 'electron';
import { app, powerMonitor } from 'electron';
import Timer = NodeJS.Timer;
import { logger } from '../common/logger';
import { windowHandler } from './window-handler';
class ActivityDetection {
private idleThreshold: number;
private window: Electron.WebContents | null;
private timer: Timer | undefined;
private queryInterval: NodeJS.Timer | undefined;
private idleThreshold: number;
private window: Electron.WebContents | null;
private timer: Timer | undefined;
private queryInterval: NodeJS.Timer | undefined;
constructor() {
this.idleThreshold = 60 * 60 * 1000;
this.window = null;
constructor() {
this.idleThreshold = 60 * 60 * 1000;
this.window = null;
}
/**
* Sets the window and the idle threshold from the web app
*
* @param window {Electron.BrowserWindow}
* @param idleThreshold {number}
*/
public setWindowAndThreshold(
window: Electron.WebContents,
idleThreshold: number,
): void {
this.window = window;
this.idleThreshold = idleThreshold;
if (this.queryInterval) {
clearInterval(this.queryInterval);
}
this.startActivityMonitor();
logger.info(
`activity-detection: Initialized activity detection with an idleThreshold of ${idleThreshold}`,
);
}
/**
* Start a timer for monitoring user active
*/
private startActivityMonitor(): void {
if (app.isReady()) {
logger.info(`activity-detection: Starting activity monitor`);
this.queryInterval = setInterval(() => {
const idleTime = powerMonitor.getSystemIdleTime();
this.activity(idleTime);
}, this.idleThreshold);
}
}
/**
* Validates and send the user activity based on
* the idle threshold set be the web app
*
* @param idleTime {number}
*/
private activity(idleTime: number): void {
const idleTimeInMillis = idleTime * 1000;
if (idleTimeInMillis < this.idleThreshold) {
this.sendActivity(idleTimeInMillis);
if (this.timer) {
clearInterval(this.timer);
}
// set auto reload to false so the
// activate func works normally
windowHandler.setIsAutoReload(false);
this.timer = undefined;
logger.info(
`activity-detection: activity occurred, updating the client!`,
);
return;
}
/**
* Sets the window and the idle threshold from the web app
*
* @param window {Electron.BrowserWindow}
* @param idleThreshold {number}
*/
public setWindowAndThreshold(window: Electron.WebContents, idleThreshold: number): void {
this.window = window;
this.idleThreshold = idleThreshold;
if (this.queryInterval) {
clearInterval(this.queryInterval);
}
this.startActivityMonitor();
logger.info(`activity-detection: Initialized activity detection with an idleThreshold of ${idleThreshold}`);
}
/**
* Start a timer for monitoring user active
*/
private startActivityMonitor(): void {
if (!this.timer) {
logger.info(
`activity-detection: user is inactive, started monitoring for every 1 sec`,
);
// starts monitoring for user activity every 1 sec
// when user goes inactive
this.timer = setInterval(() => {
if (app.isReady()) {
logger.info(`activity-detection: Starting activity monitor`);
this.queryInterval = setInterval(() => {
const idleTime = electron.powerMonitor.getSystemIdleTime();
this.activity(idleTime);
}, this.idleThreshold);
const activeTime = powerMonitor.getSystemIdleTime();
this.activity(activeTime);
}
}, 1000);
}
}
/**
* Validates and send the user activity based on
* the idle threshold set be the web app
*
* @param idleTime {number}
*/
private activity(idleTime: number): void {
const idleTimeInMillis = idleTime * 1000;
if (idleTimeInMillis < this.idleThreshold) {
this.sendActivity(idleTimeInMillis);
if (this.timer) {
clearInterval(this.timer);
}
// set auto reload to false so the
// activate func works normally
windowHandler.setIsAutoReload(false);
this.timer = undefined;
logger.info(`activity-detection: activity occurred, updating the client!`);
return;
}
if (!this.timer) {
logger.info(`activity-detection: user is inactive, started monitoring for every 1 sec`);
// starts monitoring for user activity every 1 sec
// when user goes inactive
this.timer = setInterval(() => {
if (app.isReady()) {
const activeTime = electron.powerMonitor.getSystemIdleTime();
this.activity(activeTime);
}
}, 1000);
}
}
/**
* Send user activity to the web app
*
* @param idleTime {number}
*/
private sendActivity(idleTime: number): void {
if (this.window && !this.window.isDestroyed()) {
this.window.send('activity', idleTime || 1);
logger.info(`activity-detection: Sending activity status to the client!`);
}
/**
* Send user activity to the web app
*
* @param idleTime {number}
*/
private sendActivity(idleTime: number): void {
if (this.window && !this.window.isDestroyed()) {
this.window.send('activity', idleTime || 1);
logger.info(`activity-detection: Sending activity status to the client!`);
}
}
}
const activityDetection = new ActivityDetection();

View File

@ -1,88 +1,88 @@
export interface IAnalyticsData {
element: AnalyticsElements;
action_type: MenuActionTypes | ScreenSnippetActionTypes;
action_result?: AnalyticsActions;
element: AnalyticsElements;
action_type: MenuActionTypes | ScreenSnippetActionTypes;
action_result?: AnalyticsActions;
}
export enum MenuActionTypes {
AUTO_LAUNCH_ON_START_UP = 'auto_launch_on_start_up',
ALWAYS_ON_TOP = 'always_on_top',
MINIMIZE_ON_CLOSE = 'minimize_on_close',
FLASH_NOTIFICATION_IN_TASK_BAR = 'flash_notification_in_task_bar',
HAMBURGER_MENU = 'hamburger_menu',
REFRESH_APP_IN_IDLE = 'refresh_app_in_idle',
AUTO_LAUNCH_ON_START_UP = 'auto_launch_on_start_up',
ALWAYS_ON_TOP = 'always_on_top',
MINIMIZE_ON_CLOSE = 'minimize_on_close',
FLASH_NOTIFICATION_IN_TASK_BAR = 'flash_notification_in_task_bar',
HAMBURGER_MENU = 'hamburger_menu',
REFRESH_APP_IN_IDLE = 'refresh_app_in_idle',
}
export enum ScreenSnippetActionTypes {
SCREENSHOT_TAKEN = 'screenshot_taken',
ANNOTATE_ADDED_PEN = 'annotate_added_pen',
ANNOTATE_ADDED_HIGHLIGHT = 'annotate_added_highlight',
ANNOTATE_DONE = 'annotate_done',
ANNOTATE_CLEARED = 'annotate_cleared',
ANNOTATE_ERASED = 'annotate_erased',
SCREENSHOT_TAKEN = 'screenshot_taken',
ANNOTATE_ADDED_PEN = 'annotate_added_pen',
ANNOTATE_ADDED_HIGHLIGHT = 'annotate_added_highlight',
ANNOTATE_DONE = 'annotate_done',
ANNOTATE_CLEARED = 'annotate_cleared',
ANNOTATE_ERASED = 'annotate_erased',
}
export enum AnalyticsActions {
ENABLED = 'ON',
DISABLED = 'OFF',
ENABLED = 'ON',
DISABLED = 'OFF',
}
export enum AnalyticsElements {
MENU = 'Menu',
SCREEN_CAPTURE_ANNOTATE = 'screen_capture_annotate',
MENU = 'Menu',
SCREEN_CAPTURE_ANNOTATE = 'screen_capture_annotate',
}
const MAX_EVENT_QUEUE_LENGTH = 50;
const analyticsCallback = 'analytics-callback';
class Analytics {
private preloadWindow: Electron.webContents | undefined;
private analyticsEventQueue: IAnalyticsData[] = [];
private preloadWindow: Electron.webContents | undefined;
private analyticsEventQueue: IAnalyticsData[] = [];
/**
* Stores the reference to the preload window
*
* @param webContents {Electron.webContents}
*/
public registerPreloadWindow(webContents: Electron.webContents): void {
this.preloadWindow = webContents;
/**
* Stores the reference to the preload window
*
* @param webContents {Electron.webContents}
*/
public registerPreloadWindow(webContents: Electron.webContents): void {
this.preloadWindow = webContents;
if (!(this.preloadWindow && !this.preloadWindow.isDestroyed())) {
return;
}
if (this.analyticsEventQueue && this.analyticsEventQueue.length > 0) {
this.analyticsEventQueue.forEach((events) => {
if (this.preloadWindow && !this.preloadWindow.isDestroyed()) {
this.preloadWindow.send(analyticsCallback, events);
}
});
this.resetAnalytics();
}
if (!(this.preloadWindow && !this.preloadWindow.isDestroyed())) {
return;
}
/**
* Sends the analytics events to the web client
*
* @param eventData {IAnalyticsData}
*/
public track(eventData: IAnalyticsData): void {
if (this.analyticsEventQueue && this.analyticsEventQueue.length > 0) {
this.analyticsEventQueue.forEach((events) => {
if (this.preloadWindow && !this.preloadWindow.isDestroyed()) {
this.preloadWindow.send(analyticsCallback, eventData);
return;
}
this.analyticsEventQueue.push(eventData);
// don't store more than 50 msgs. keep most recent log msgs.
if (this.analyticsEventQueue.length > MAX_EVENT_QUEUE_LENGTH) {
this.analyticsEventQueue.shift();
this.preloadWindow.send(analyticsCallback, events);
}
});
this.resetAnalytics();
}
}
/**
* Clears the analytics queue
*/
public resetAnalytics(): void {
this.analyticsEventQueue = [];
/**
* Sends the analytics events to the web client
*
* @param eventData {IAnalyticsData}
*/
public track(eventData: IAnalyticsData): void {
if (this.preloadWindow && !this.preloadWindow.isDestroyed()) {
this.preloadWindow.send(analyticsCallback, eventData);
return;
}
this.analyticsEventQueue.push(eventData);
// don't store more than 50 msgs. keep most recent log msgs.
if (this.analyticsEventQueue.length > MAX_EVENT_QUEUE_LENGTH) {
this.analyticsEventQueue.shift();
}
}
/**
* Clears the analytics queue
*/
public resetAnalytics(): void {
this.analyticsEventQueue = [];
}
}
const analytics = new Analytics();

View File

@ -14,22 +14,29 @@ const cacheCheckFilePath: string = path.join(userDataPath, 'CacheCheck');
* Cleans old cache
*/
const cleanOldCache = (): void => {
const fileRemovalList = ['blob_storage', 'Cache', 'Cookies', 'temp', 'Cookies-journal', 'GPUCache'];
const fileRemovalList = [
'blob_storage',
'Cache',
'Cookies',
'temp',
'Cookies-journal',
'GPUCache',
];
const files = fs.readdirSync(userDataPath);
const files = fs.readdirSync(userDataPath);
files.forEach((file) => {
const filePath = path.join(userDataPath, file);
if (!fileRemovalList.includes(file)) {
return;
}
files.forEach((file) => {
const filePath = path.join(userDataPath, file);
if (!fileRemovalList.includes(file)) {
return;
}
if (fs.lstatSync(filePath).isDirectory()) {
rimraf.sync(filePath);
return;
}
fs.unlinkSync(filePath);
});
if (fs.lstatSync(filePath).isDirectory()) {
rimraf.sync(filePath);
return;
}
fs.unlinkSync(filePath);
});
};
/**
@ -37,31 +44,39 @@ const cleanOldCache = (): void => {
* the cache for the session
*/
export const cleanUpAppCache = async (): Promise<void> => {
if (fs.existsSync(cacheCheckFilePath)) {
await fs.unlinkSync(cacheCheckFilePath);
logger.info(`app-cache-handler: last exit was clean, deleted the app cache file`);
return;
}
if (session.defaultSession) {
await session.defaultSession.clearCache();
logger.info(`app-cache-handler: we didn't have a clean exit last time, so, cleared the cache that may have been corrupted!`);
}
if (fs.existsSync(cacheCheckFilePath)) {
await fs.unlinkSync(cacheCheckFilePath);
logger.info(
`app-cache-handler: last exit was clean, deleted the app cache file`,
);
return;
}
if (session.defaultSession) {
await session.defaultSession.clearCache();
logger.info(
`app-cache-handler: we didn't have a clean exit last time, so, cleared the cache that may have been corrupted!`,
);
}
};
/**
* Creates a new file cache file on app exit
*/
export const createAppCacheFile = (): void => {
logger.info(`app-cache-handler: this is a clean exit, creating app cache file`);
fs.writeFileSync(cacheCheckFilePath, '');
logger.info(
`app-cache-handler: this is a clean exit, creating app cache file`,
);
fs.writeFileSync(cacheCheckFilePath, '');
};
/**
* Cleans the app cache on new install
*/
export const cleanAppCacheOnInstall = (): void => {
logger.info(`app-cache-handler: cleaning app cache and cookies on new install`);
cleanOldCache();
logger.info(
`app-cache-handler: cleaning app cache and cookies on new install`,
);
cleanOldCache();
};
/**
@ -69,31 +84,41 @@ export const cleanAppCacheOnInstall = (): void => {
* @param window Browser window to listen to for crash events
*/
export const cleanAppCacheOnCrash = (window: BrowserWindow): void => {
logger.info(`app-cache-handler: listening to crash events & cleaning app cache`);
const events = ['unresponsive', 'crashed', 'plugin-crashed'];
logger.info(
`app-cache-handler: listening to crash events & cleaning app cache`,
);
const events = ['unresponsive', 'crashed', 'plugin-crashed'];
events.forEach((windowEvent: any) => {
window.webContents.on(windowEvent, async () => {
logger.info(`app-cache-handler: Window Event '${windowEvent}' occurred. Clearing cache & restarting app`);
const focusedWindow = BrowserWindow.getFocusedWindow();
if (!focusedWindow || (typeof focusedWindow.isDestroyed === 'function' && focusedWindow.isDestroyed())) {
return;
}
const options = {
type: 'question',
title: i18n.t('Relaunch Application')(),
message: i18n.t('Oops! Something went wrong. Would you like to restart the app?')(),
buttons: [i18n.t('Restart')(), i18n.t('Cancel')()],
cancelId: 1,
};
events.forEach((windowEvent: any) => {
window.webContents.on(windowEvent, async () => {
logger.info(
`app-cache-handler: Window Event '${windowEvent}' occurred. Clearing cache & restarting app`,
);
const focusedWindow = BrowserWindow.getFocusedWindow();
if (
!focusedWindow ||
(typeof focusedWindow.isDestroyed === 'function' &&
focusedWindow.isDestroyed())
) {
return;
}
const options = {
type: 'question',
title: i18n.t('Relaunch Application')(),
message: i18n.t(
'Oops! Something went wrong. Would you like to restart the app?',
)(),
buttons: [i18n.t('Restart')(), i18n.t('Cancel')()],
cancelId: 1,
};
const { response } = await dialog.showMessageBox(focusedWindow, options);
const { response } = await dialog.showMessageBox(focusedWindow, options);
if (response === 0) {
cleanOldCache();
app.relaunch();
app.exit();
}
});
if (response === 0) {
cleanOldCache();
app.relaunch();
app.exit();
}
});
});
};

File diff suppressed because it is too large Load Diff

View File

@ -2,17 +2,15 @@ import { app } from 'electron';
import { logger } from '../common/logger';
class AppStateHandler {
/**
* Restarts the app with the command line arguments
* passed from the previous session
*/
public restart() {
logger.info(`Restarting app as per instruction from SFE`);
app.relaunch();
app.exit();
}
/**
* Restarts the app with the command line arguments
* passed from the previous session
*/
public restart() {
logger.info(`Restarting app as per instruction from SFE`);
app.relaunch();
app.exit();
}
}
const appStateHandler = new AppStateHandler();

View File

@ -4,74 +4,76 @@ import { isMac } from '../common/env';
import { logger } from '../common/logger';
import { CloudConfigDataTypes, config, IConfig } from './config-handler';
const { autoLaunchPath }: IConfig = config.getConfigFields([ 'autoLaunchPath' ]);
const { autoLaunchPath }: IConfig = config.getConfigFields(['autoLaunchPath']);
const props = isMac ? {
mac: {
const props = isMac
? {
mac: {
useLaunchAgent: true,
},
name: 'Symphony',
path: process.execPath,
} : {
name: 'Symphony',
path: autoLaunchPath
},
name: 'Symphony',
path: process.execPath,
}
: {
name: 'Symphony',
path: autoLaunchPath
? autoLaunchPath.replace(/\//g, '\\')
: null || process.execPath,
};
};
class AutoLaunchController {
/**
* Enable auto launch and displays error dialog on failure
*
* @return {Promise<void>}
*/
public enableAutoLaunch(): void {
app.setLoginItemSettings({ openAtLogin: true, path: props.path });
logger.info(`auto-launch-controller: Enabled auto launch!`);
}
/**
* Enable auto launch and displays error dialog on failure
*
* @return {Promise<void>}
*/
public enableAutoLaunch(): void {
app.setLoginItemSettings({ openAtLogin: true, path: props.path });
logger.info(`auto-launch-controller: Enabled auto launch!`);
/**
* Disable auto launch and displays error dialog on failure
*
* @return {Promise<void>}
*/
public disableAutoLaunch(): void {
app.setLoginItemSettings({ openAtLogin: false, path: props.path });
logger.info(`auto-launch-controller: Disabled auto launch!`);
}
/**
* Checks if auto launch is enabled
*
* @return {Boolean}
*/
public isAutoLaunchEnabled(): LoginItemSettings {
return app.getLoginItemSettings();
}
/**
* Validates the user config and enables auto launch
*/
public async handleAutoLaunch(): Promise<void> {
const { launchOnStartup }: IConfig = config.getConfigFields([
'launchOnStartup',
]);
const {
openAtLogin: isAutoLaunchEnabled,
}: LoginItemSettings = this.isAutoLaunchEnabled();
if (launchOnStartup === CloudConfigDataTypes.ENABLED) {
if (!isAutoLaunchEnabled) {
this.enableAutoLaunch();
}
return;
}
/**
* Disable auto launch and displays error dialog on failure
*
* @return {Promise<void>}
*/
public disableAutoLaunch(): void {
app.setLoginItemSettings({ openAtLogin: false, path: props.path });
logger.info(`auto-launch-controller: Disabled auto launch!`);
if (isAutoLaunchEnabled) {
this.disableAutoLaunch();
}
/**
* Checks if auto launch is enabled
*
* @return {Boolean}
*/
public isAutoLaunchEnabled(): LoginItemSettings {
return app.getLoginItemSettings();
}
/**
* Validates the user config and enables auto launch
*/
public async handleAutoLaunch(): Promise<void> {
const { launchOnStartup }: IConfig = config.getConfigFields([ 'launchOnStartup' ]);
const { openAtLogin: isAutoLaunchEnabled }: LoginItemSettings = this.isAutoLaunchEnabled();
if (launchOnStartup === CloudConfigDataTypes.ENABLED) {
if (!isAutoLaunchEnabled) {
this.enableAutoLaunch();
}
return;
}
if (isAutoLaunchEnabled) {
this.disableAutoLaunch();
}
}
}
}
const autoLaunchInstance = new AutoLaunchController();
export {
autoLaunchInstance,
};
export { autoLaunchInstance };

View File

@ -9,18 +9,18 @@ import { getGuid } from '../common/utils';
import { whitelistHandler } from '../common/whitelist-handler';
import { config } from './config-handler';
import {
handlePermissionRequests,
monitorWindowActions,
onConsoleMessages,
removeWindowEventListener,
sendInitialBoundChanges,
handlePermissionRequests,
monitorWindowActions,
onConsoleMessages,
removeWindowEventListener,
sendInitialBoundChanges,
} from './window-actions';
import { ICustomBrowserWindow, windowHandler } from './window-handler';
import {
getBounds,
// handleCertificateProxyVerification,
injectStyles,
preventWindowNavigation,
getBounds,
// handleCertificateProxyVerification,
injectStyles,
preventWindowNavigation,
} from './window-utils';
const DEFAULT_POP_OUT_WIDTH = 300;
@ -34,25 +34,31 @@ const MIN_HEIGHT = 300;
* @param url URL to be verified
*/
const verifyProtocolForNewUrl = (url: string): boolean => {
const parsedUrl = parse(url);
if (!parsedUrl) {
logger.info(`child-window-handler: The url ${url} doesn't have a protocol. Returning false for verification!`);
return false;
}
// url parse returns protocol with :
if (parsedUrl.protocol === 'https:') {
logger.info(`child-window-handler: The url ${url} is a https url! Returning true for verification!`);
return true;
}
// url parse returns protocol with :
if (parsedUrl.protocol === 'http:') {
logger.info(`child-window-handler: The url ${url} is a http url! Returning true for verification!`);
return true;
}
const parsedUrl = parse(url);
if (!parsedUrl) {
logger.info(
`child-window-handler: The url ${url} doesn't have a protocol. Returning false for verification!`,
);
return false;
}
// url parse returns protocol with :
if (parsedUrl.protocol === 'https:') {
logger.info(
`child-window-handler: The url ${url} is a https url! Returning true for verification!`,
);
return true;
}
// url parse returns protocol with :
if (parsedUrl.protocol === 'http:') {
logger.info(
`child-window-handler: The url ${url} is a http url! Returning true for verification!`,
);
return true;
}
return false;
};
/**
@ -62,185 +68,235 @@ const verifyProtocolForNewUrl = (url: string): boolean => {
* @param url {string}
*/
const getParsedUrl = (url: string): Url => {
const parsedUrl = parse(url);
const parsedUrl = parse(url);
if (!parsedUrl.protocol || parsedUrl.protocol !== 'https') {
logger.info(`child-window-handler: The url ${url} doesn't have a valid protocol. Adding https as protocol.`);
parsedUrl.protocol = 'https:';
parsedUrl.slashes = true;
}
const finalParsedUrl = parse(format(parsedUrl));
logger.info(`child-window-handler: The original url ${url} is finally parsed as ${JSON.stringify(finalParsedUrl)}`);
return finalParsedUrl;
if (!parsedUrl.protocol || parsedUrl.protocol !== 'https') {
logger.info(
`child-window-handler: The url ${url} doesn't have a valid protocol. Adding https as protocol.`,
);
parsedUrl.protocol = 'https:';
parsedUrl.slashes = true;
}
const finalParsedUrl = parse(format(parsedUrl));
logger.info(
`child-window-handler: The original url ${url} is finally parsed as ${JSON.stringify(
finalParsedUrl,
)}`,
);
return finalParsedUrl;
};
export const handleChildWindow = (webContents: WebContents): void => {
const childWindow = (event, newWinUrl, frameName, disposition, newWinOptions): void => {
logger.info(`child-window-handler: trying to create new child window for url: ${newWinUrl},
const childWindow = (
event,
newWinUrl,
frameName,
disposition,
newWinOptions,
): void => {
logger.info(`child-window-handler: trying to create new child window for url: ${newWinUrl},
frame name: ${frameName || undefined}, disposition: ${disposition}`);
const mainWindow = windowHandler.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) {
logger.info(`child-window-handler: main window is not available / destroyed, not creating child window!`);
return;
}
if (!windowHandler.url) {
logger.info(`child-window-handler: we don't have a valid url, not creating child window!`);
return;
}
const mainWindow = windowHandler.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) {
logger.info(
`child-window-handler: main window is not available / destroyed, not creating child window!`,
);
return;
}
if (!windowHandler.url) {
logger.info(
`child-window-handler: we don't have a valid url, not creating child window!`,
);
return;
}
if (!newWinOptions.webPreferences) {
newWinOptions.webPreferences = {};
}
if (!newWinOptions.webPreferences) {
newWinOptions.webPreferences = {};
}
Object.assign(newWinOptions.webPreferences, webContents);
Object.assign(newWinOptions.webPreferences, webContents);
// need this to extract other parameters
const newWinParsedUrl = getParsedUrl(newWinUrl);
// need this to extract other parameters
const newWinParsedUrl = getParsedUrl(newWinUrl);
const newWinUrlData = whitelistHandler.parseDomain(newWinUrl);
const mainWinUrlData = whitelistHandler.parseDomain(windowHandler.url);
const newWinUrlData = whitelistHandler.parseDomain(newWinUrl);
const mainWinUrlData = whitelistHandler.parseDomain(windowHandler.url);
const newWinDomainName = `${newWinUrlData.domain}${newWinUrlData.tld}`;
const mainWinDomainName = `${mainWinUrlData.domain}${mainWinUrlData.tld}`;
const newWinDomainName = `${newWinUrlData.domain}${newWinUrlData.tld}`;
const mainWinDomainName = `${mainWinUrlData.domain}${mainWinUrlData.tld}`;
logger.info(`child-window-handler: main window url: ${mainWinUrlData.subdomain}.${mainWinUrlData.domain}.${mainWinUrlData.tld}`);
logger.info(
`child-window-handler: main window url: ${mainWinUrlData.subdomain}.${mainWinUrlData.domain}.${mainWinUrlData.tld}`,
);
const emptyUrlString = [ 'about:blank', 'about:blank#blocked' ];
const dispositionWhitelist = ['new-window', 'foreground-tab'];
const emptyUrlString = ['about:blank', 'about:blank#blocked'];
const dispositionWhitelist = ['new-window', 'foreground-tab'];
// only allow window.open to succeed is if coming from same host,
// otherwise open in default browser.
if ((newWinDomainName === mainWinDomainName || emptyUrlString.includes(newWinUrl))
&& frameName !== ''
&& dispositionWhitelist.includes(disposition)) {
// only allow window.open to succeed is if coming from same host,
// otherwise open in default browser.
if (
(newWinDomainName === mainWinDomainName ||
emptyUrlString.includes(newWinUrl)) &&
frameName !== '' &&
dispositionWhitelist.includes(disposition)
) {
logger.info(
`child-window-handler: opening pop-out window for ${newWinUrl}`,
);
logger.info(`child-window-handler: opening pop-out window for ${newWinUrl}`);
const newWinKey = getGuid();
if (!frameName) {
logger.info(
`child-window-handler: frame name missing! not opening the url ${newWinUrl}`,
);
return;
}
const newWinKey = getGuid();
if (!frameName) {
logger.info(`child-window-handler: frame name missing! not opening the url ${newWinUrl}`);
return;
}
const width = newWinOptions.width || DEFAULT_POP_OUT_WIDTH;
const height = newWinOptions.height || DEFAULT_POP_OUT_HEIGHT;
const width = newWinOptions.width || DEFAULT_POP_OUT_WIDTH;
const height = newWinOptions.height || DEFAULT_POP_OUT_HEIGHT;
// try getting x and y position from query parameters
const query = newWinParsedUrl && parseQuerystring(newWinParsedUrl.query as string);
if (query && query.x && query.y) {
const newX = Number.parseInt(query.x as string, 10);
const newY = Number.parseInt(query.y as string, 10);
// only accept if both are successfully parsed.
if (Number.isInteger(newX) && Number.isInteger(newY)) {
const newWinRect = { x: newX, y: newY, width, height };
const { x, y } = getBounds(newWinRect, DEFAULT_POP_OUT_WIDTH, DEFAULT_POP_OUT_HEIGHT);
newWinOptions.x = x;
newWinOptions.y = y;
} else {
newWinOptions.x = 0;
newWinOptions.y = 0;
}
} else {
// create new window at slight offset from main window.
const { x, y } = mainWindow.getBounds();
newWinOptions.x = x + 50;
newWinOptions.y = y + 50;
}
newWinOptions.width = Math.max(width, DEFAULT_POP_OUT_WIDTH);
newWinOptions.height = Math.max(height, DEFAULT_POP_OUT_HEIGHT);
newWinOptions.minWidth = MIN_WIDTH;
newWinOptions.minHeight = MIN_HEIGHT;
newWinOptions.alwaysOnTop = mainWindow.isAlwaysOnTop();
newWinOptions.frame = true;
newWinOptions.winKey = newWinKey;
newWinOptions.fullscreen = false;
const childWebContents: WebContents = newWinOptions.webContents;
// Event needed to hide native menu bar
childWebContents.once('did-start-loading', () => {
const browserWin = BrowserWindow.fromWebContents(childWebContents) as ICustomBrowserWindow;
const { contextOriginUrl } = config.getGlobalConfigFields([ 'contextOriginUrl' ]);
browserWin.setFullScreenable(true);
browserWin.origin = contextOriginUrl || windowHandler.url;
if (isWindowsOS && browserWin && !browserWin.isDestroyed()) {
browserWin.setMenuBarVisibility(false);
}
});
childWebContents.once('did-finish-load', async () => {
logger.info(`child-window-handler: child window content loaded for url ${newWinUrl}!`);
const browserWin: ICustomBrowserWindow = BrowserWindow.fromWebContents(childWebContents) as ICustomBrowserWindow;
if (!browserWin) {
return;
}
windowHandler.addWindow(newWinKey, browserWin);
const { url } = config.getGlobalConfigFields([ 'url' ]);
const { enableRendererLogs } = config.getConfigFields([ 'enableRendererLogs' ]);
browserWin.webContents.send('page-load', {
isWindowsOS,
locale: i18n.getLocale(),
resources: i18n.loadedResources,
origin: url,
enableCustomTitleBar: false,
isMainWindow: false,
});
// Inserts css on to the window
await injectStyles(browserWin, false);
browserWin.winName = frameName;
browserWin.setAlwaysOnTop(mainWindow.isAlwaysOnTop());
logger.info(`child-window-handler: setting always on top for child window? ${mainWindow.isAlwaysOnTop()}!`);
// prevents window from navigating
preventWindowNavigation(browserWin, true);
// Handle media/other permissions
handlePermissionRequests(browserWin.webContents);
// Monitor window actions
monitorWindowActions(browserWin);
// Update initial bound changes
sendInitialBoundChanges(browserWin);
if (enableRendererLogs) {
browserWin.webContents.on('console-message', onConsoleMessages);
}
// Remove all attached event listeners
browserWin.on('close', () => {
logger.info(`child-window-handler: close event occurred for window with url ${newWinUrl}!`);
removeWindowEventListener(browserWin);
});
if (browserWin.webContents) {
// validate link and create a child window or open in browser
handleChildWindow(browserWin.webContents);
// Certificate verification proxy
// if (!isDevEnv) {
// browserWin.webContents.session.setCertificateVerifyProc(handleCertificateProxyVerification);
// }
// Updates media permissions for preload context
const { permissions } = config.getConfigFields([ 'permissions' ]);
browserWin.webContents.send('is-screen-share-enabled', permissions.media);
}
});
// try getting x and y position from query parameters
const query =
newWinParsedUrl && parseQuerystring(newWinParsedUrl.query as string);
if (query && query.x && query.y) {
const newX = Number.parseInt(query.x as string, 10);
const newY = Number.parseInt(query.y as string, 10);
// only accept if both are successfully parsed.
if (Number.isInteger(newX) && Number.isInteger(newY)) {
const newWinRect = { x: newX, y: newY, width, height };
const { x, y } = getBounds(
newWinRect,
DEFAULT_POP_OUT_WIDTH,
DEFAULT_POP_OUT_HEIGHT,
);
newWinOptions.x = x;
newWinOptions.y = y;
} else {
event.preventDefault();
if (newWinUrl && newWinUrl.length > 2083) {
logger.info(`child-window-handler: new window url length is greater than 2083, not performing any action!`);
return;
}
if (!verifyProtocolForNewUrl(newWinUrl)) {
logger.info(`child-window-handler: new window url protocol is not http or https, not performing any action!`);
return;
}
logger.info(`child-window-handler: new window url is ${newWinUrl} which is not of the same host,
so opening it in the default browser!`);
windowHandler.openUrlInDefaultBrowser(newWinUrl);
newWinOptions.x = 0;
newWinOptions.y = 0;
}
};
webContents.on('new-window', childWindow);
} else {
// create new window at slight offset from main window.
const { x, y } = mainWindow.getBounds();
newWinOptions.x = x + 50;
newWinOptions.y = y + 50;
}
newWinOptions.width = Math.max(width, DEFAULT_POP_OUT_WIDTH);
newWinOptions.height = Math.max(height, DEFAULT_POP_OUT_HEIGHT);
newWinOptions.minWidth = MIN_WIDTH;
newWinOptions.minHeight = MIN_HEIGHT;
newWinOptions.alwaysOnTop = mainWindow.isAlwaysOnTop();
newWinOptions.frame = true;
newWinOptions.winKey = newWinKey;
newWinOptions.fullscreen = false;
const childWebContents: WebContents = newWinOptions.webContents;
// Event needed to hide native menu bar
childWebContents.once('did-start-loading', () => {
const browserWin = BrowserWindow.fromWebContents(
childWebContents,
) as ICustomBrowserWindow;
const { contextOriginUrl } = config.getGlobalConfigFields([
'contextOriginUrl',
]);
browserWin.setFullScreenable(true);
browserWin.origin = contextOriginUrl || windowHandler.url;
if (isWindowsOS && browserWin && !browserWin.isDestroyed()) {
browserWin.setMenuBarVisibility(false);
}
});
childWebContents.once('did-finish-load', async () => {
logger.info(
`child-window-handler: child window content loaded for url ${newWinUrl}!`,
);
const browserWin: ICustomBrowserWindow = BrowserWindow.fromWebContents(
childWebContents,
) as ICustomBrowserWindow;
if (!browserWin) {
return;
}
windowHandler.addWindow(newWinKey, browserWin);
const { url } = config.getGlobalConfigFields(['url']);
const { enableRendererLogs } = config.getConfigFields([
'enableRendererLogs',
]);
browserWin.webContents.send('page-load', {
isWindowsOS,
locale: i18n.getLocale(),
resources: i18n.loadedResources,
origin: url,
enableCustomTitleBar: false,
isMainWindow: false,
});
// Inserts css on to the window
await injectStyles(browserWin, false);
browserWin.winName = frameName;
browserWin.setAlwaysOnTop(mainWindow.isAlwaysOnTop());
logger.info(
`child-window-handler: setting always on top for child window? ${mainWindow.isAlwaysOnTop()}!`,
);
// prevents window from navigating
preventWindowNavigation(browserWin, true);
// Handle media/other permissions
handlePermissionRequests(browserWin.webContents);
// Monitor window actions
monitorWindowActions(browserWin);
// Update initial bound changes
sendInitialBoundChanges(browserWin);
if (enableRendererLogs) {
browserWin.webContents.on('console-message', onConsoleMessages);
}
// Remove all attached event listeners
browserWin.on('close', () => {
logger.info(
`child-window-handler: close event occurred for window with url ${newWinUrl}!`,
);
removeWindowEventListener(browserWin);
});
if (browserWin.webContents) {
// validate link and create a child window or open in browser
handleChildWindow(browserWin.webContents);
// Certificate verification proxy
// if (!isDevEnv) {
// browserWin.webContents.session.setCertificateVerifyProc(handleCertificateProxyVerification);
// }
// Updates media permissions for preload context
const { permissions } = config.getConfigFields(['permissions']);
browserWin.webContents.send(
'is-screen-share-enabled',
permissions.media,
);
}
});
} else {
event.preventDefault();
if (newWinUrl && newWinUrl.length > 2083) {
logger.info(
`child-window-handler: new window url length is greater than 2083, not performing any action!`,
);
return;
}
if (!verifyProtocolForNewUrl(newWinUrl)) {
logger.info(
`child-window-handler: new window url protocol is not http or https, not performing any action!`,
);
return;
}
logger.info(`child-window-handler: new window url is ${newWinUrl} which is not of the same host,
so opening it in the default browser!`);
windowHandler.openUrlInDefaultBrowser(newWinUrl);
}
};
webContents.on('new-window', childWindow);
};

View File

@ -4,81 +4,112 @@ import { logger } from '../common/logger';
import { CloudConfigDataTypes, config, IConfig } from './config-handler';
// Set default flags
logger.info(`chrome-flags: Setting mandatory chrome flags`, { flag: { 'ssl-version-fallback-min': 'tls1.2' } });
logger.info(`chrome-flags: Setting mandatory chrome flags`, {
flag: { 'ssl-version-fallback-min': 'tls1.2' },
});
app.commandLine.appendSwitch('ssl-version-fallback-min', 'tls1.2');
// Special args that need to be excluded as part of the chrome command line switch
const specialArgs = [ '--url', '--multiInstance', '--userDataPath=', 'symphony://', '--inspect-brk', '--inspect', '--logPath' ];
const specialArgs = [
'--url',
'--multiInstance',
'--userDataPath=',
'symphony://',
'--inspect-brk',
'--inspect',
'--logPath',
];
/**
* Sets chrome flags
*/
export const setChromeFlags = () => {
logger.info(`chrome-flags: Checking if we need to set chrome flags!`);
logger.info(`chrome-flags: Checking if we need to set chrome flags!`);
const flagsConfig = config.getConfigFields(['customFlags', 'disableGpu']) as IConfig;
const { disableThrottling } = config.getCloudConfigFields([ 'disableThrottling' ]) as any;
const configFlags: object = {
'auth-negotiate-delegate-whitelist': flagsConfig.customFlags.authServerWhitelist,
'auth-server-whitelist': flagsConfig.customFlags.authNegotiateDelegateWhitelist,
'disable-background-timer-throttling': 'true',
'disable-d3d11': flagsConfig.disableGpu || null,
'disable-gpu': flagsConfig.disableGpu || null,
'disable-gpu-compositing': flagsConfig.disableGpu || null,
'enable-blink-features': 'RTCInsertableStreams',
};
if (flagsConfig.customFlags.disableThrottling === CloudConfigDataTypes.ENABLED || disableThrottling === CloudConfigDataTypes.ENABLED) {
configFlags['disable-renderer-backgrounding'] = 'true';
const flagsConfig = config.getConfigFields([
'customFlags',
'disableGpu',
]) as IConfig;
const { disableThrottling } = config.getCloudConfigFields([
'disableThrottling',
]) as any;
const configFlags: object = {
'auth-negotiate-delegate-whitelist':
flagsConfig.customFlags.authServerWhitelist,
'auth-server-whitelist':
flagsConfig.customFlags.authNegotiateDelegateWhitelist,
'disable-background-timer-throttling': 'true',
'disable-d3d11': flagsConfig.disableGpu || null,
'disable-gpu': flagsConfig.disableGpu || null,
'disable-gpu-compositing': flagsConfig.disableGpu || null,
'enable-blink-features': 'RTCInsertableStreams',
};
if (
flagsConfig.customFlags.disableThrottling ===
CloudConfigDataTypes.ENABLED ||
disableThrottling === CloudConfigDataTypes.ENABLED
) {
configFlags['disable-renderer-backgrounding'] = 'true';
}
for (const key in configFlags) {
if (!Object.prototype.hasOwnProperty.call(configFlags, key)) {
continue;
}
const val = configFlags[key];
if (key && val) {
logger.info(
`chrome-flags: Setting chrome flag for ${key} with value ${val}!`,
);
app.commandLine.appendSwitch(key, val);
}
}
const cmdArgs = process.argv;
cmdArgs.forEach((arg) => {
// We need to check if the argument key matches the one
// in the special args array and return if it does match
const argSplit = arg.split('=');
const argKey = argSplit[0];
const argValue = argSplit[1] && arg.substring(arg.indexOf('=') + 1);
if (arg.startsWith('--') && specialArgs.includes(argKey)) {
return;
}
for (const key in configFlags) {
if (!Object.prototype.hasOwnProperty.call(configFlags, key)) {
continue;
}
const val = configFlags[key];
if (key && val) {
logger.info(`chrome-flags: Setting chrome flag for ${key} with value ${val}!`);
app.commandLine.appendSwitch(key, val);
}
// All the chrome flags starts with --
// So, any other arg (like 'electron' or '.')
// need to be skipped
if (arg.startsWith('--')) {
// Since chrome takes values after an equals
// We split the arg and set it either as
// just a key, or as a key-value pair
if (argKey && argValue) {
app.commandLine.appendSwitch(argKey.substr(2), argValue);
} else {
app.commandLine.appendSwitch(argKey);
}
logger.info(
`Appended chrome command line switch ${argKey} with value ${argValue}`,
);
}
const cmdArgs = process.argv;
cmdArgs.forEach((arg) => {
// We need to check if the argument key matches the one
// in the special args array and return if it does match
const argSplit = arg.split('=');
const argKey = argSplit[0];
const argValue = argSplit[1] && arg.substring(arg.indexOf('=') + 1);
if (arg.startsWith('--') && specialArgs.includes(argKey)) {
return;
}
// All the chrome flags starts with --
// So, any other arg (like 'electron' or '.')
// need to be skipped
if (arg.startsWith('--')) {
// Since chrome takes values after an equals
// We split the arg and set it either as
// just a key, or as a key-value pair
if (argKey && argValue) {
app.commandLine.appendSwitch(argKey.substr(2), argValue);
} else {
app.commandLine.appendSwitch(argKey);
}
logger.info( `Appended chrome command line switch ${argKey} with value ${argValue}`);
}
});
});
};
/**
* Sets default session properties
*/
export const setSessionProperties = () => {
logger.info(`chrome-flags: Settings session properties`);
const { customFlags } = config.getConfigFields([ 'customFlags' ]) as IConfig;
logger.info(`chrome-flags: Settings session properties`);
const { customFlags } = config.getConfigFields(['customFlags']) as IConfig;
if (session.defaultSession && customFlags && customFlags.authServerWhitelist && customFlags.authServerWhitelist !== '') {
session.defaultSession.allowNTLMCredentialsForDomains(customFlags.authServerWhitelist);
}
if (
session.defaultSession &&
customFlags &&
customFlags.authServerWhitelist &&
customFlags.authServerWhitelist !== ''
) {
session.defaultSession.allowNTLMCredentialsForDomains(
customFlags.authServerWhitelist,
);
}
};

View File

@ -11,436 +11,591 @@ import { filterOutSelectedValues, pick } from '../common/utils';
const writeFile = util.promisify(fs.writeFile);
export enum CloudConfigDataTypes {
NOT_SET = 'NOT_SET',
ENABLED = 'ENABLED',
DISABLED = 'DISABLED',
NOT_SET = 'NOT_SET',
ENABLED = 'ENABLED',
DISABLED = 'DISABLED',
}
export interface IConfig {
url: string;
minimizeOnClose: CloudConfigDataTypes;
launchOnStartup: CloudConfigDataTypes;
alwaysOnTop: CloudConfigDataTypes;
bringToFront: CloudConfigDataTypes;
whitelistUrl: string;
isCustomTitleBar: CloudConfigDataTypes;
memoryRefresh: CloudConfigDataTypes;
memoryThreshold: string;
disableGpu: boolean;
enableRendererLogs: boolean;
devToolsEnabled: boolean;
ctWhitelist: string[];
podWhitelist: string[];
autoLaunchPath: string;
permissions: IPermission;
customFlags: ICustomFlag;
buildNumber?: string;
configVersion?: string;
notificationSettings: INotificationSetting;
mainWinPos?: ICustomRectangle;
locale?: string;
installVariant?: string;
bootCount?: number;
url: string;
minimizeOnClose: CloudConfigDataTypes;
launchOnStartup: CloudConfigDataTypes;
alwaysOnTop: CloudConfigDataTypes;
bringToFront: CloudConfigDataTypes;
whitelistUrl: string;
isCustomTitleBar: CloudConfigDataTypes;
memoryRefresh: CloudConfigDataTypes;
memoryThreshold: string;
disableGpu: boolean;
enableRendererLogs: boolean;
devToolsEnabled: boolean;
ctWhitelist: string[];
podWhitelist: string[];
autoLaunchPath: string;
permissions: IPermission;
customFlags: ICustomFlag;
buildNumber?: string;
configVersion?: string;
notificationSettings: INotificationSetting;
mainWinPos?: ICustomRectangle;
locale?: string;
installVariant?: string;
bootCount?: number;
}
export interface IGlobalConfig {
contextOriginUrl: string;
url: string;
contextIsolation: boolean;
contextOriginUrl: string;
url: string;
contextIsolation: boolean;
}
export interface ICloudConfig {
configVersion?: string;
podLevelEntitlements: IPodLevelEntitlements;
acpFeatureLevelEntitlements: IACPFeatureLevelEntitlements;
pmpEntitlements: IPMPEntitlements;
configVersion?: string;
podLevelEntitlements: IPodLevelEntitlements;
acpFeatureLevelEntitlements: IACPFeatureLevelEntitlements;
pmpEntitlements: IPMPEntitlements;
}
export interface IPodLevelEntitlements {
minimizeOnClose: CloudConfigDataTypes;
isCustomTitleBar: CloudConfigDataTypes;
alwaysOnTop: CloudConfigDataTypes;
memoryRefresh: CloudConfigDataTypes;
bringToFront: CloudConfigDataTypes;
disableThrottling: CloudConfigDataTypes;
launchOnStartup: CloudConfigDataTypes;
memoryThreshold: string;
ctWhitelist: string;
podWhitelist: string;
authNegotiateDelegateWhitelist: string;
whitelistUrl: string;
authServerWhitelist: string;
autoLaunchPath: string;
minimizeOnClose: CloudConfigDataTypes;
isCustomTitleBar: CloudConfigDataTypes;
alwaysOnTop: CloudConfigDataTypes;
memoryRefresh: CloudConfigDataTypes;
bringToFront: CloudConfigDataTypes;
disableThrottling: CloudConfigDataTypes;
launchOnStartup: CloudConfigDataTypes;
memoryThreshold: string;
ctWhitelist: string;
podWhitelist: string;
authNegotiateDelegateWhitelist: string;
whitelistUrl: string;
authServerWhitelist: string;
autoLaunchPath: string;
}
export interface IACPFeatureLevelEntitlements {
devToolsEnabled: boolean;
permissions: IPermission;
devToolsEnabled: boolean;
permissions: IPermission;
}
export interface IPMPEntitlements {
minimizeOnClose: CloudConfigDataTypes;
bringToFront: CloudConfigDataTypes;
memoryRefresh: CloudConfigDataTypes;
refreshAppThreshold: CloudConfigDataTypes;
disableThrottling: CloudConfigDataTypes;
minimizeOnClose: CloudConfigDataTypes;
bringToFront: CloudConfigDataTypes;
memoryRefresh: CloudConfigDataTypes;
refreshAppThreshold: CloudConfigDataTypes;
disableThrottling: CloudConfigDataTypes;
}
export interface IPermission {
media: boolean;
geolocation: boolean;
notifications: boolean;
midiSysex: boolean;
pointerLock: boolean;
fullscreen: boolean;
openExternal: boolean;
media: boolean;
geolocation: boolean;
notifications: boolean;
midiSysex: boolean;
pointerLock: boolean;
fullscreen: boolean;
openExternal: boolean;
}
export interface ICustomFlag {
authServerWhitelist: string;
authNegotiateDelegateWhitelist: string;
disableThrottling: CloudConfigDataTypes;
authServerWhitelist: string;
authNegotiateDelegateWhitelist: string;
disableThrottling: CloudConfigDataTypes;
}
export interface INotificationSetting {
position: string;
display: string;
position: string;
display: string;
}
export interface ICustomRectangle extends Partial<Electron.Rectangle> {
isMaximized?: boolean;
isFullScreen?: boolean;
isMaximized?: boolean;
isFullScreen?: boolean;
}
class Config {
public userConfig: IConfig | {};
public globalConfig: IConfig | {};
public cloudConfig: ICloudConfig | {};
public filteredCloudConfig: ICloudConfig | {};
private isFirstTime: boolean = true;
private installVariant: string | undefined;
private bootCount: number | undefined;
private readonly configFileName: string;
private readonly installVariantFilename: string;
private readonly installVariantPath: string;
private readonly userConfigPath: string;
private readonly appPath: string;
private readonly globalConfigPath: string;
private readonly cloudConfigPath: string;
public userConfig: IConfig | {};
public globalConfig: IConfig | {};
public cloudConfig: ICloudConfig | {};
public filteredCloudConfig: ICloudConfig | {};
private isFirstTime: boolean = true;
private installVariant: string | undefined;
private bootCount: number | undefined;
private readonly configFileName: string;
private readonly installVariantFilename: string;
private readonly installVariantPath: string;
private readonly userConfigPath: string;
private readonly appPath: string;
private readonly globalConfigPath: string;
private readonly cloudConfigPath: string;
constructor() {
this.configFileName = 'Symphony.config';
this.installVariantFilename = 'InstallVariant.info';
this.userConfigPath = path.join(app.getPath('userData'), this.configFileName);
this.cloudConfigPath = path.join(app.getPath('userData'), 'cloudConfig.config');
this.appPath = isDevEnv ? app.getAppPath() : path.dirname(app.getPath('exe'));
this.globalConfigPath = isDevEnv
? path.join(this.appPath, path.join('config', this.configFileName))
: path.join(this.appPath, (isMac) ? '..' : '', 'config', this.configFileName);
constructor() {
this.configFileName = 'Symphony.config';
this.installVariantFilename = 'InstallVariant.info';
this.userConfigPath = path.join(
app.getPath('userData'),
this.configFileName,
);
this.cloudConfigPath = path.join(
app.getPath('userData'),
'cloudConfig.config',
);
this.appPath = isDevEnv
? app.getAppPath()
: path.dirname(app.getPath('exe'));
this.globalConfigPath = isDevEnv
? path.join(this.appPath, path.join('config', this.configFileName))
: path.join(
this.appPath,
isMac ? '..' : '',
'config',
this.configFileName,
);
this.installVariantPath = isDevEnv
? path.join(this.appPath, path.join('config', this.installVariantFilename))
: path.join(this.appPath, (isMac) ? '..' : '', 'config', this.installVariantFilename);
this.installVariantPath = isDevEnv
? path.join(
this.appPath,
path.join('config', this.installVariantFilename),
)
: path.join(
this.appPath,
isMac ? '..' : '',
'config',
this.installVariantFilename,
);
if (isLinux) {
this.globalConfigPath = path.join(this.appPath, (isElectronQA) ? '..' : '', 'config', this.configFileName);
this.installVariantPath = path.join(this.appPath, (isElectronQA) ? '..' : '', 'config', this.installVariantFilename);
}
this.globalConfig = {};
this.userConfig = {};
this.cloudConfig = {};
this.filteredCloudConfig = {};
this.readUserConfig();
this.readGlobalConfig();
this.readInstallVariant();
this.readCloudConfig();
this.checkFirstTimeLaunch();
if (isLinux) {
this.globalConfigPath = path.join(
this.appPath,
isElectronQA ? '..' : '',
'config',
this.configFileName,
);
this.installVariantPath = path.join(
this.appPath,
isElectronQA ? '..' : '',
'config',
this.installVariantFilename,
);
}
/**
* Returns the specified fields from both user and global config file
* and keep values from user config as priority
*
* @param fields
*/
public getConfigFields(fields: string[]): IConfig {
const configFields = { ...this.getGlobalConfigFields(fields), ...this.getUserConfigFields(fields), ...this.getFilteredCloudConfigFields(fields) } as IConfig;
logger.info(`config-handler: getting combined config values for the fields ${fields}`, configFields);
return configFields;
this.globalConfig = {};
this.userConfig = {};
this.cloudConfig = {};
this.filteredCloudConfig = {};
this.readUserConfig();
this.readGlobalConfig();
this.readInstallVariant();
this.readCloudConfig();
this.checkFirstTimeLaunch();
}
/**
* Returns the specified fields from both user and global config file
* and keep values from user config as priority
*
* @param fields
*/
public getConfigFields(fields: string[]): IConfig {
const configFields: IConfig = {
...this.getGlobalConfigFields(fields),
...this.getUserConfigFields(fields),
...this.getFilteredCloudConfigFields(fields),
};
logger.info(
`config-handler: getting combined config values for the fields ${fields}`,
configFields,
);
return configFields;
}
/**
* Returns the specified fields from user config file
*
* @param fields {Array}
*/
public getUserConfigFields(fields: string[]): IConfig {
const userConfigData = pick(this.userConfig, fields) as IConfig;
logger.info(
`config-handler: getting user config values for the fields ${fields}`,
userConfigData,
);
return userConfigData;
}
/**
* Returns the specified fields from global config file
*
* @param fields {Array}
*/
public getGlobalConfigFields(fields: string[]): IGlobalConfig {
const globalConfigData = pick(this.globalConfig, fields) as IGlobalConfig;
logger.info(
`config-handler: getting global config values for the fields ${fields}`,
globalConfigData,
);
return globalConfigData;
}
/**
* Returns filtered & prioritised fields from cloud config file
*
* @param fields {Array}
*/
public getFilteredCloudConfigFields(fields: string[]): IConfig | {} {
const filteredCloudConfigData = pick(
this.filteredCloudConfig,
fields,
) as IConfig;
logger.info(
`config-handler: getting filtered cloud config values for the ${fields}`,
filteredCloudConfigData,
);
return filteredCloudConfigData;
}
/**
* Returns the actual cloud config with priority
* @param fields
*/
public getCloudConfigFields(fields: string[]): IConfig {
const {
acpFeatureLevelEntitlements,
podLevelEntitlements,
pmpEntitlements,
} = this.cloudConfig as ICloudConfig;
const cloudConfig = {
...acpFeatureLevelEntitlements,
...podLevelEntitlements,
...pmpEntitlements,
};
logger.info(`config-handler: prioritized cloud config data`, cloudConfig);
const cloudConfigData = pick(cloudConfig, fields) as IConfig;
logger.info(
`config-handler: getting prioritized cloud config values for the fields ${fields}`,
cloudConfigData,
);
return cloudConfigData;
}
/**
* updates new data to the user config
*
* @param data {IConfig}
*/
public async updateUserConfig(data: Partial<IConfig>): Promise<void> {
logger.info(
`config-handler: updating user config values with the data`,
JSON.stringify(data),
);
this.userConfig = { ...this.userConfig, ...data };
try {
await writeFile(
this.userConfigPath,
JSON.stringify(this.userConfig, null, 2),
{ encoding: 'utf8' },
);
logger.info(
`config-handler: updated user config values with the data ${JSON.stringify(
data,
)}`,
);
} catch (error) {
logger.error(
`config-handler: failed to update user config file with ${JSON.stringify(
data,
)}`,
error,
);
dialog.showErrorBox(
`Update failed`,
`Failed to update user config due to error: ${error}`,
);
}
}
/**
* updates new data to the cloud config
*
* @param data {IConfig}
*/
public async updateCloudConfig(data: Partial<ICloudConfig>): Promise<void> {
logger.info(
`config-handler: Updating the cloud config data from SFE: `,
data,
);
this.cloudConfig = { ...this.cloudConfig, ...data };
// recalculate cloud config when we have data from SFE
this.filterCloudConfig();
logger.info(
`config-handler: prioritized and filtered cloud config: `,
this.filteredCloudConfig,
);
try {
await writeFile(
this.cloudConfigPath,
JSON.stringify(this.cloudConfig, null, 2),
{ encoding: 'utf8' },
);
logger.info(`config-handler: writing cloud config values to file`);
} catch (error) {
logger.error(
`config-handler: failed to update cloud config file with ${data}`,
error,
);
}
}
/**
* Return true if the app is launched for the first time
* otherwise false
*/
public isFirstTimeLaunch(): boolean {
return this.isFirstTime;
}
/**
* Method that updates user config file
* by modifying the old config file
*/
public async setUpFirstTimeLaunch(): Promise<void> {
const execPath = path.dirname(this.appPath);
const shouldUpdateUserConfig =
execPath.indexOf('AppData\\Local\\Programs') !== -1 || isMac;
if (shouldUpdateUserConfig) {
const {
minimizeOnClose,
launchOnStartup,
alwaysOnTop,
memoryRefresh,
bringToFront,
isCustomTitleBar,
...filteredFields
}: IConfig = this.userConfig as IConfig;
// update to the new build number
filteredFields.buildNumber = buildNumber;
filteredFields.installVariant = this.installVariant;
filteredFields.bootCount = 0;
logger.info(
`config-handler: setting first time launch for build`,
buildNumber,
);
await this.updateUserConfig(filteredFields);
return;
}
await this.updateUserConfig({
buildNumber,
installVariant: this.installVariant,
bootCount: this.bootCount,
});
}
/**
* Gets the boot count for an SDA installation
*/
public getBootCount(): number | undefined {
logger.info(`config-handler: Current boot count is ${this.bootCount}`);
return this.bootCount;
}
/**
* Updates user config on start by fetching new settings from the global config
* @private
*/
public async updateUserConfigOnStart() {
logger.info(
`config-handler: updating user config with the latest global config values`,
);
const latestGlobalConfig = this.globalConfig as IConfig;
// The properties set below are typically controlled by IT admins, so, we copy
// all the values from global config to the user config on SDA relaunch
await this.updateUserConfig({
whitelistUrl: latestGlobalConfig.whitelistUrl,
memoryThreshold: latestGlobalConfig.memoryThreshold,
devToolsEnabled: latestGlobalConfig.devToolsEnabled,
ctWhitelist: latestGlobalConfig.ctWhitelist,
podWhitelist: latestGlobalConfig.podWhitelist,
permissions: latestGlobalConfig.permissions,
autoLaunchPath: latestGlobalConfig.autoLaunchPath,
customFlags: latestGlobalConfig.customFlags,
});
}
/**
* filters out the cloud config
*/
private filterCloudConfig(): void {
const {
acpFeatureLevelEntitlements,
podLevelEntitlements,
pmpEntitlements,
} = this.cloudConfig as ICloudConfig;
// Filter out some values
const filteredACP = filterOutSelectedValues(acpFeatureLevelEntitlements, [
true,
'NOT_SET',
'',
[],
]);
const filteredPod = filterOutSelectedValues(podLevelEntitlements, [
true,
'NOT_SET',
'',
[],
]);
const filteredPMP = filterOutSelectedValues(pmpEntitlements, [
true,
'NOT_SET',
'',
[],
]);
// priority is PMP > ACP > SDA
this.filteredCloudConfig = {
...filteredACP,
...filteredPod,
...filteredPMP,
};
}
/**
* Parses the config data string
*
* @param data
*/
private parseConfigData(data: string): object {
let parsedData;
if (!data) {
logger.error(`config-handler: unable to read config file`);
throw new Error('unable to read user config file');
}
try {
parsedData = JSON.parse(data);
logger.info(`config-handler: parsed JSON file with data`, parsedData);
} catch (e) {
logger.error(
`config-handler: parsing JSON file failed due to error ${e}`,
);
throw new Error(e);
}
return parsedData;
}
/**
* Reads a stores the user config file
*
* If user config doesn't exits?
* this creates a new one with { configVersion: current_app_version, buildNumber: current_app_build_number }
*/
private async readUserConfig() {
if (!fs.existsSync(this.userConfigPath)) {
// Need to wait until app ready event to access user data
await app.whenReady();
await this.readGlobalConfig();
logger.info(
`config-handler: user config doesn't exist! will create new one and update config`,
);
const { url, ...rest } = this.globalConfig as IConfig;
await this.updateUserConfig({
configVersion: app.getVersion().toString(),
buildNumber,
...rest,
} as IConfig);
}
this.userConfig = this.parseConfigData(
fs.readFileSync(this.userConfigPath, 'utf8'),
);
logger.info(`config-handler: User configuration: `, this.userConfig);
}
/**
* Reads a stores the global config file
*/
private readGlobalConfig() {
if (!fs.existsSync(this.globalConfigPath)) {
throw new Error(
`Global config file missing! App will not run as expected!`,
);
}
this.globalConfig = this.parseConfigData(
fs.readFileSync(this.globalConfigPath, 'utf8'),
);
logger.info(`config-handler: Global configuration: `, this.globalConfig);
}
/**
* Reads the install variant from a file
*/
private readInstallVariant() {
this.installVariant = fs.readFileSync(this.installVariantPath, 'utf8');
logger.info(`config-handler: Install variant: `, this.installVariant);
}
/**
* Reads and stores the cloud config file
*
* If cloud config doesn't exits?
* this creates a new one with { }
*/
private async readCloudConfig() {
if (!fs.existsSync(this.cloudConfigPath)) {
await app.whenReady();
await this.updateCloudConfig({
configVersion: app.getVersion().toString(),
});
}
this.cloudConfig = this.parseConfigData(
fs.readFileSync(this.cloudConfigPath, 'utf8'),
);
// recalculate cloud config when we the application starts
this.filterCloudConfig();
logger.info(`config-handler: Cloud configuration: `, this.userConfig);
}
/**
* Verifies if the application is launched for the first time
*/
private async checkFirstTimeLaunch() {
logger.info('config-handler: checking first time launch');
const installVariant =
(this.userConfig && (this.userConfig as IConfig).installVariant) || null;
if (!installVariant) {
logger.info(
`config-handler: there's no install variant found, this is a first time launch`,
);
this.isFirstTime = true;
this.bootCount = 0;
return;
}
/**
* Returns the specified fields from user config file
*
* @param fields {Array}
*/
public getUserConfigFields(fields: string[]): IConfig {
const userConfigData = pick(this.userConfig, fields) as IConfig;
logger.info(`config-handler: getting user config values for the fields ${fields}`, userConfigData);
return userConfigData;
if (
installVariant &&
typeof installVariant === 'string' &&
installVariant !== this.installVariant
) {
logger.info(
`config-handler: install variant found is of a different instance, this is a first time launch`,
);
this.isFirstTime = true;
this.bootCount = 0;
return;
}
/**
* Returns the specified fields from global config file
*
* @param fields {Array}
*/
public getGlobalConfigFields(fields: string[]): IGlobalConfig {
const globalConfigData = pick(this.globalConfig, fields) as IGlobalConfig;
logger.info(`config-handler: getting global config values for the fields ${fields}`, globalConfigData);
return globalConfigData;
}
/**
* Returns filtered & prioritised fields from cloud config file
*
* @param fields {Array}
*/
public getFilteredCloudConfigFields(fields: string[]): IConfig | {} {
const filteredCloudConfigData = pick(this.filteredCloudConfig, fields) as IConfig;
logger.info(`config-handler: getting filtered cloud config values for the ${fields}`, filteredCloudConfigData);
return filteredCloudConfigData;
}
/**
* Returns the actual cloud config with priority
* @param fields
*/
public getCloudConfigFields(fields: string[]): IConfig {
const { acpFeatureLevelEntitlements, podLevelEntitlements, pmpEntitlements } = this.cloudConfig as ICloudConfig;
const cloudConfig = { ...acpFeatureLevelEntitlements, ...podLevelEntitlements, ...pmpEntitlements };
logger.info(`config-handler: prioritized cloud config data`, cloudConfig);
const cloudConfigData = pick(cloudConfig, fields) as IConfig;
logger.info(`config-handler: getting prioritized cloud config values for the fields ${fields}`, cloudConfigData);
return cloudConfigData;
}
/**
* updates new data to the user config
*
* @param data {IConfig}
*/
public async updateUserConfig(data: Partial<IConfig>): Promise<void> {
logger.info(`config-handler: updating user config values with the data`, JSON.stringify(data));
this.userConfig = { ...this.userConfig, ...data };
try {
await writeFile(this.userConfigPath, JSON.stringify(this.userConfig, null, 2), { encoding: 'utf8' });
logger.info(`config-handler: updated user config values with the data ${JSON.stringify(data)}`);
} catch (error) {
logger.error(`config-handler: failed to update user config file with ${JSON.stringify(data)}`, error);
dialog.showErrorBox(`Update failed`, `Failed to update user config due to error: ${error}`);
}
}
/**
* updates new data to the cloud config
*
* @param data {IConfig}
*/
public async updateCloudConfig(data: Partial<ICloudConfig>): Promise<void> {
logger.info(`config-handler: Updating the cloud config data from SFE: `, data);
this.cloudConfig = { ...this.cloudConfig, ...data };
// recalculate cloud config when we have data from SFE
this.filterCloudConfig();
logger.info(`config-handler: prioritized and filtered cloud config: `, this.filteredCloudConfig);
try {
await writeFile(this.cloudConfigPath, JSON.stringify(this.cloudConfig, null, 2), { encoding: 'utf8' });
logger.info(`config-handler: writing cloud config values to file`);
} catch (error) {
logger.error(`config-handler: failed to update cloud config file with ${data}`, error);
}
}
/**
* Return true if the app is launched for the first time
* otherwise false
*/
public isFirstTimeLaunch(): boolean {
return this.isFirstTime;
}
/**
* Method that updates user config file
* by modifying the old config file
*/
public async setUpFirstTimeLaunch(): Promise<void> {
const execPath = path.dirname(this.appPath);
const shouldUpdateUserConfig = execPath.indexOf('AppData\\Local\\Programs') !== -1 || isMac;
if (shouldUpdateUserConfig) {
const {
minimizeOnClose,
launchOnStartup,
alwaysOnTop,
memoryRefresh,
bringToFront,
isCustomTitleBar,
...filteredFields }: IConfig = this.userConfig as IConfig;
// update to the new build number
filteredFields.buildNumber = buildNumber;
filteredFields.installVariant = this.installVariant;
filteredFields.bootCount = 0;
logger.info(`config-handler: setting first time launch for build`, buildNumber);
return await this.updateUserConfig(filteredFields);
}
await this.updateUserConfig({ buildNumber, installVariant: this.installVariant, bootCount: this.bootCount });
}
/**
* Gets the boot count for an SDA installation
*/
public getBootCount(): number | undefined {
logger.info(`config-handler: Current boot count is ${this.bootCount}`);
return this.bootCount;
}
/**
* Updates user config on start by fetching new settings from the global config
* @private
*/
public async updateUserConfigOnStart() {
logger.info(`config-handler: updating user config with the latest global config values`);
const latestGlobalConfig = this.globalConfig as IConfig;
// The properties set below are typically controlled by IT admins, so, we copy
// all the values from global config to the user config on SDA relaunch
await this.updateUserConfig({
whitelistUrl: latestGlobalConfig.whitelistUrl,
memoryThreshold: latestGlobalConfig.memoryThreshold,
devToolsEnabled: latestGlobalConfig.devToolsEnabled,
ctWhitelist: latestGlobalConfig.ctWhitelist,
podWhitelist: latestGlobalConfig.podWhitelist,
permissions: latestGlobalConfig.permissions,
autoLaunchPath: latestGlobalConfig.autoLaunchPath,
customFlags: latestGlobalConfig.customFlags,
});
}
/**
* filters out the cloud config
*/
private filterCloudConfig(): void {
const { acpFeatureLevelEntitlements, podLevelEntitlements, pmpEntitlements } = this.cloudConfig as ICloudConfig;
// Filter out some values
const filteredACP = filterOutSelectedValues(acpFeatureLevelEntitlements, [true, 'NOT_SET', '', []]);
const filteredPod = filterOutSelectedValues(podLevelEntitlements, [true, 'NOT_SET', '', []]);
const filteredPMP = filterOutSelectedValues(pmpEntitlements, [true, 'NOT_SET', '', []]);
// priority is PMP > ACP > SDA
this.filteredCloudConfig = { ...filteredACP, ...filteredPod, ...filteredPMP };
}
/**
* Parses the config data string
*
* @param data
*/
private parseConfigData(data: string): object {
let parsedData;
if (!data) {
logger.error(`config-handler: unable to read config file`);
throw new Error('unable to read user config file');
}
try {
parsedData = JSON.parse(data);
logger.info(`config-handler: parsed JSON file with data`, parsedData);
} catch (e) {
logger.error(`config-handler: parsing JSON file failed due to error ${e}`);
throw new Error(e);
}
return parsedData;
}
/**
* Reads a stores the user config file
*
* If user config doesn't exits?
* this creates a new one with { configVersion: current_app_version, buildNumber: current_app_build_number }
*/
private async readUserConfig() {
if (!fs.existsSync(this.userConfigPath)) {
// Need to wait until app ready event to access user data
await app.whenReady();
await this.readGlobalConfig();
logger.info(`config-handler: user config doesn't exist! will create new one and update config`);
const { url, ...rest } = this.globalConfig as IConfig;
await this.updateUserConfig({ configVersion: app.getVersion().toString(), buildNumber, ...rest } as IConfig);
}
this.userConfig = this.parseConfigData(fs.readFileSync(this.userConfigPath, 'utf8'));
logger.info(`config-handler: User configuration: `, this.userConfig);
}
/**
* Reads a stores the global config file
*/
private readGlobalConfig() {
if (!fs.existsSync(this.globalConfigPath)) {
throw new Error(`Global config file missing! App will not run as expected!`);
}
this.globalConfig = this.parseConfigData(fs.readFileSync(this.globalConfigPath, 'utf8'));
logger.info(`config-handler: Global configuration: `, this.globalConfig);
}
/**
* Reads the install variant from a file
*/
private readInstallVariant() {
this.installVariant = fs.readFileSync(this.installVariantPath, 'utf8');
logger.info(`config-handler: Install variant: `, this.installVariant);
}
/**
* Reads and stores the cloud config file
*
* If cloud config doesn't exits?
* this creates a new one with { }
*/
private async readCloudConfig() {
if (!fs.existsSync(this.cloudConfigPath)) {
await app.whenReady();
await this.updateCloudConfig({ configVersion: app.getVersion().toString() });
}
this.cloudConfig = this.parseConfigData(fs.readFileSync(this.cloudConfigPath, 'utf8'));
// recalculate cloud config when we the application starts
this.filterCloudConfig();
logger.info(`config-handler: Cloud configuration: `, this.userConfig);
}
/**
* Verifies if the application is launched for the first time
*/
private async checkFirstTimeLaunch() {
logger.info('config-handler: checking first time launch');
const installVariant = this.userConfig && (this.userConfig as IConfig).installVariant || null;
if (!installVariant) {
logger.info(`config-handler: there's no install variant found, this is a first time launch`);
this.isFirstTime = true;
this.bootCount = 0;
return;
}
if (installVariant && typeof installVariant === 'string' && installVariant !== this.installVariant) {
logger.info(`config-handler: install variant found is of a different instance, this is a first time launch`);
this.isFirstTime = true;
this.bootCount = 0;
return;
}
logger.info(`config-handler: install variant is the same as the existing one, not a first time launch`);
this.isFirstTime = false;
this.bootCount = (this.getConfigFields(['bootCount']) as IConfig).bootCount;
if (this.bootCount !== undefined) {
this.bootCount++;
await this.updateUserConfig({ bootCount: this.bootCount });
} else {
await this.updateUserConfig({ bootCount: 0 });
}
logger.info(
`config-handler: install variant is the same as the existing one, not a first time launch`,
);
this.isFirstTime = false;
this.bootCount = (this.getConfigFields(['bootCount']) as IConfig).bootCount;
if (this.bootCount !== undefined) {
this.bootCount++;
await this.updateUserConfig({ bootCount: this.bootCount });
} else {
await this.updateUserConfig({ bootCount: 0 });
}
}
}
const config = new Config();
export {
config,
};
export { config };

View File

@ -9,126 +9,202 @@ import { logger } from '../common/logger';
const TAG_LENGTH = 16;
const arch = process.arch === 'ia32';
const winLibraryPath = isDevEnv ? path.join(__dirname, '..', '..', 'library') : path.join(execPath, 'library');
const macLibraryPath = isDevEnv ? path.join(__dirname, '..', '..', '..', 'library') : path.join(execPath, '..', 'library');
const winLibraryPath = isDevEnv
? path.join(__dirname, '..', '..', 'library')
: path.join(execPath, 'library');
const macLibraryPath = isDevEnv
? path.join(__dirname, '..', '..', '..', 'library')
: path.join(execPath, '..', 'library');
const cryptoLibPath = isMac ?
path.join(macLibraryPath, 'cryptoLib.dylib') :
(arch ? path.join(winLibraryPath, 'libsymphonysearch-x86.dll') : path.join(winLibraryPath, 'libsymphonysearch-x64.dll'));
const cryptoLibPath = isMac
? path.join(macLibraryPath, 'cryptoLib.dylib')
: arch
? path.join(winLibraryPath, 'libsymphonysearch-x86.dll')
: path.join(winLibraryPath, 'libsymphonysearch-x64.dll');
const library = new Library((cryptoLibPath), {
AESEncryptGCM: [types.int32, [
refType(types.uchar),
types.int32,
refType(types.uchar),
types.int32,
refType(types.uchar),
refType(types.uchar),
types.uint32,
refType(types.uchar),
refType(types.uchar),
types.uint32,
]],
const library = new Library(cryptoLibPath, {
AESEncryptGCM: [
types.int32,
[
refType(types.uchar),
types.int32,
refType(types.uchar),
types.int32,
refType(types.uchar),
refType(types.uchar),
types.uint32,
refType(types.uchar),
refType(types.uchar),
types.uint32,
],
],
AESDecryptGCM: [types.int32, [
refType(types.uchar),
types.int32,
refType(types.uchar),
types.int32,
refType(types.uchar),
types.uint32,
refType(types.uchar),
refType(types.uchar),
types.uint32,
refType(types.uchar),
]],
AESDecryptGCM: [
types.int32,
[
refType(types.uchar),
types.int32,
refType(types.uchar),
types.int32,
refType(types.uchar),
types.uint32,
refType(types.uchar),
refType(types.uchar),
types.uint32,
refType(types.uchar),
],
],
getVersion: [types.CString, []],
getVersion: [types.CString, []],
});
interface ICryptoLib {
AESGCMEncrypt: (name: string, base64IV: string, base64AAD: string, base64Key: string, base64In: string) => string | null;
AESGCMDecrypt: (base64IV: string, base64AAD: string, base64Key: string, base64In: string) => string | null;
AESGCMEncrypt: (
name: string,
base64IV: string,
base64AAD: string,
base64Key: string,
base64In: string,
) => string | null;
AESGCMDecrypt: (
base64IV: string,
base64AAD: string,
base64Key: string,
base64In: string,
) => string | null;
}
class CryptoLibrary implements ICryptoLib {
/**
* Encrypt / Decrypt
*
* @param name {string} Method name to execute
* @param base64IV {string}
* @param base64AAD {string}
* @param base64Key {string}
* @param base64In {string}
* @constructor
*/
public static EncryptDecrypt(
name: string,
base64IV: string,
base64AAD: string,
base64Key: string,
base64In: string,
): string | null {
let base64Input = base64In;
/**
* Encrypt / Decrypt
*
* @param name {string} Method name to execute
* @param base64IV {string}
* @param base64AAD {string}
* @param base64Key {string}
* @param base64In {string}
* @constructor
*/
public static EncryptDecrypt(name: string, base64IV: string, base64AAD: string, base64Key: string, base64In: string): string | null {
let base64Input = base64In;
if (!base64Input) {
base64Input = '';
}
const IV: Buffer = Buffer.from(base64IV, 'base64');
const AAD: Buffer = Buffer.from(base64AAD, 'base64');
const KEY: Buffer = Buffer.from(base64Key, 'base64');
const IN: Buffer = Buffer.from(base64Input, 'base64');
if (name === 'AESGCMEncrypt') {
const outPtr: Buffer = Buffer.alloc(IN.length);
const TAG: Buffer = Buffer.alloc(TAG_LENGTH);
const resultCode = library.AESEncryptGCM(IN, IN.length, AAD, AAD.length, KEY, IV, IV.length, outPtr, TAG, TAG_LENGTH);
if (resultCode < 0) {
logger.error(`AESEncryptGCM, Failed to encrypt with exit code ${resultCode}`);
}
const bufferArray = [outPtr, TAG];
return Buffer.concat(bufferArray).toString('base64');
}
if (name === 'AESGCMDecrypt') {
const cipherTextLen = IN.length - TAG_LENGTH;
const TAG = Buffer.from(IN.slice(IN.length - 16, IN.length));
const outPtr = Buffer.alloc(IN.length - TAG_LENGTH);
const resultCode = library.AESDecryptGCM(IN, cipherTextLen, AAD, AAD.length, TAG, TAG_LENGTH, KEY, IV, IV.length, outPtr);
if (resultCode < 0) {
logger.error(`AESDecryptGCM, Failed to decrypt with exit code ${resultCode}`);
}
return outPtr.toString('base64');
}
return null;
if (!base64Input) {
base64Input = '';
}
/**
* Encrypts the given data
*
* @param base64IV {string}
* @param base64AAD {string}
* @param base64Key {string}
* @param base64In {string}
* @constructor
*/
public AESGCMEncrypt(base64IV: string, base64AAD: string, base64Key: string, base64In: string): string | null {
return CryptoLibrary.EncryptDecrypt('AESGCMEncrypt', base64IV, base64AAD, base64Key, base64In);
const IV: Buffer = Buffer.from(base64IV, 'base64');
const AAD: Buffer = Buffer.from(base64AAD, 'base64');
const KEY: Buffer = Buffer.from(base64Key, 'base64');
const IN: Buffer = Buffer.from(base64Input, 'base64');
if (name === 'AESGCMEncrypt') {
const outPtr: Buffer = Buffer.alloc(IN.length);
const TAG: Buffer = Buffer.alloc(TAG_LENGTH);
const resultCode = library.AESEncryptGCM(
IN,
IN.length,
AAD,
AAD.length,
KEY,
IV,
IV.length,
outPtr,
TAG,
TAG_LENGTH,
);
if (resultCode < 0) {
logger.error(
`AESEncryptGCM, Failed to encrypt with exit code ${resultCode}`,
);
}
const bufferArray = [outPtr, TAG];
return Buffer.concat(bufferArray).toString('base64');
}
/**
* Decrypts the give data
*
* @param base64IV {string}
* @param base64AAD {string}
* @param base64Key {string}
* @param base64In {string}
* @constructor
*/
public AESGCMDecrypt(base64IV: string, base64AAD: string, base64Key: string, base64In: string): string | null {
return CryptoLibrary.EncryptDecrypt('AESGCMDecrypt', base64IV, base64AAD, base64Key, base64In);
if (name === 'AESGCMDecrypt') {
const cipherTextLen = IN.length - TAG_LENGTH;
const TAG = Buffer.from(IN.slice(IN.length - 16, IN.length));
const outPtr = Buffer.alloc(IN.length - TAG_LENGTH);
const resultCode = library.AESDecryptGCM(
IN,
cipherTextLen,
AAD,
AAD.length,
TAG,
TAG_LENGTH,
KEY,
IV,
IV.length,
outPtr,
);
if (resultCode < 0) {
logger.error(
`AESDecryptGCM, Failed to decrypt with exit code ${resultCode}`,
);
}
return outPtr.toString('base64');
}
return null;
}
/**
* Encrypts the given data
*
* @param base64IV {string}
* @param base64AAD {string}
* @param base64Key {string}
* @param base64In {string}
* @constructor
*/
public AESGCMEncrypt(
base64IV: string,
base64AAD: string,
base64Key: string,
base64In: string,
): string | null {
return CryptoLibrary.EncryptDecrypt(
'AESGCMEncrypt',
base64IV,
base64AAD,
base64Key,
base64In,
);
}
/**
* Decrypts the give data
*
* @param base64IV {string}
* @param base64AAD {string}
* @param base64Key {string}
* @param base64In {string}
* @constructor
*/
public AESGCMDecrypt(
base64IV: string,
base64AAD: string,
base64Key: string,
base64In: string,
): string | null {
return CryptoLibrary.EncryptDecrypt(
'AESGCMDecrypt',
base64IV,
base64AAD,
base64Key,
base64In,
);
}
}
const cryptoLibrary = new CryptoLibrary();

View File

@ -1,5 +1,4 @@
import * as electron from 'electron';
import { app } from 'electron';
import { app, BrowserWindow, dialog } from 'electron';
import { i18n } from '../common/i18n';
import { logger } from '../common/logger';
@ -7,40 +6,48 @@ import { CloudConfigDataTypes, config } from './config-handler';
import { ICustomBrowserWindow, windowHandler } from './window-handler';
import { windowExists } from './window-utils';
let currentAuthURL;
let currentAuthURL: string;
let tries = 0;
electron.app.on('login', (event, webContents, request, authInfo, callback) => {
event.preventDefault();
app.on('login', (event, webContents, request, authInfo, callback) => {
event.preventDefault();
// This check is to determine whether the request is for the same
// host if so then increase the login tries from which we can
// display invalid credentials
if (currentAuthURL !== request.url) {
currentAuthURL = request.url;
tries = 0;
} else {
tries++;
}
// This check is to determine whether the request is for the same
// host if so then increase the login tries from which we can
// display invalid credentials
if (currentAuthURL !== request.url) {
currentAuthURL = request.url;
tries = 0;
} else {
tries++;
}
// name of the host to display
const hostname = authInfo.host || authInfo.realm;
const browserWin: ICustomBrowserWindow = electron.BrowserWindow.fromWebContents(webContents) as ICustomBrowserWindow;
// name of the host to display
const hostname = authInfo.host || authInfo.realm;
const browserWin: ICustomBrowserWindow = BrowserWindow.fromWebContents(
webContents,
) as ICustomBrowserWindow;
/**
* Method that resets currentAuthURL and tries
* if user closes the auth window
*/
const clearSettings = () => {
currentAuthURL = '';
tries = 0;
};
/**
* Method that resets currentAuthURL and tries
* if user closes the auth window
*/
const clearSettings = () => {
currentAuthURL = '';
tries = 0;
};
/**
* Opens an electron modal window in which
* user can enter credentials fot the host
*/
windowHandler.createBasicAuthWindow(browserWin, hostname, tries === 0, clearSettings, callback);
/**
* Opens an electron modal window in which
* user can enter credentials fot the host
*/
windowHandler.createBasicAuthWindow(
browserWin,
hostname,
tries === 0,
clearSettings,
callback,
);
});
let ignoreAllCertErrors = false;
@ -53,41 +60,40 @@ let ignoreAllCertErrors = false;
* Note: the dialog is synchronous so further processing is blocked until
* user provides a response.
*/
electron.app.on('certificate-error', async (event, webContents, url, error, _certificate, callback) => {
app.on(
'certificate-error',
async (event, webContents, url, error, _certificate, callback) => {
// TODO: Add logic verify custom certificate
if (ignoreAllCertErrors) {
event.preventDefault();
callback(true);
return;
event.preventDefault();
callback(true);
return;
}
logger.warn(`Certificate error: ${error} for url: ${url}`);
event.preventDefault();
const browserWin = electron.BrowserWindow.fromWebContents(webContents);
const browserWin = BrowserWindow.fromWebContents(webContents);
if (browserWin && windowExists(browserWin)) {
const { response } = await electron.dialog.showMessageBox(browserWin, {
type: 'warning',
buttons: [
i18n.t('Allow')(),
i18n.t('Deny')(),
i18n.t('Ignore All')(),
],
defaultId: 1,
cancelId: 1,
noLink: true,
title: i18n.t('Certificate Error')(),
message: `${i18n.t('Certificate Error')()}: ${error}\nURL: ${url}`,
});
if (response === 2) {
ignoreAllCertErrors = true;
}
const { response } = await dialog.showMessageBox(browserWin, {
type: 'warning',
buttons: [i18n.t('Allow')(), i18n.t('Deny')(), i18n.t('Ignore All')()],
defaultId: 1,
cancelId: 1,
noLink: true,
title: i18n.t('Certificate Error')(),
message: `${i18n.t('Certificate Error')()}: ${error}\nURL: ${url}`,
});
if (response === 2) {
ignoreAllCertErrors = true;
}
callback(response !== 1);
callback(response !== 1);
}
});
},
);
/**
* Show dialog pinned to given window when loading error occurs
@ -99,34 +105,45 @@ electron.app.on('certificate-error', async (event, webContents, url, error, _cer
* @param retryCallback {function} Callback when user clicks reload
* @param showDialog {Boolean} Indicates if a dialog need to be show to a user
*/
export const showLoadFailure = async (browserWindow: Electron.BrowserWindow, url: string, errorDesc: string, errorCode: number, retryCallback: () => void, showDialog: boolean): Promise<void> => {
let message = url ? `${i18n.t('Error loading URL')()}:\n${url}` : i18n.t('Error loading window')();
if (errorDesc) {
message += `\n\n${errorDesc}`;
}
if (errorCode) {
message += `\n\nError Code: ${errorCode}`;
}
export const showLoadFailure = async (
browserWindow: Electron.BrowserWindow,
url: string,
errorDesc: string,
errorCode: number,
retryCallback: () => void,
showDialog: boolean,
): Promise<void> => {
let message = url
? `${i18n.t('Error loading URL')()}:\n${url}`
: i18n.t('Error loading window')();
if (errorDesc) {
message += `\n\n${errorDesc}`;
}
if (errorCode) {
message += `\n\nError Code: ${errorCode}`;
}
if (showDialog) {
const { response } = await electron.dialog.showMessageBox(browserWindow, {
type: 'error',
buttons: [i18n.t('Reload')(), i18n.t('Ignore')()],
defaultId: 0,
cancelId: 1,
noLink: true,
title: i18n.t('Loading Error')(),
message,
});
if (showDialog) {
const { response } = await dialog.showMessageBox(browserWindow, {
type: 'error',
buttons: [i18n.t('Reload')(), i18n.t('Ignore')()],
defaultId: 0,
cancelId: 1,
noLink: true,
title: i18n.t('Loading Error')(),
message,
});
// async handle of user input
// retry if hitting button index 0 (i.e., reload)
if (response === 0 && typeof retryCallback === 'function') {
retryCallback();
}
// async handle of user input
// retry if hitting button index 0 (i.e., reload)
if (response === 0 && typeof retryCallback === 'function') {
retryCallback();
}
}
logger.warn(`Load failure msg: ${errorDesc} errorCode: ${errorCode} for url: ${url}`);
logger.warn(
`Load failure msg: ${errorDesc} errorCode: ${errorCode} for url: ${url}`,
);
};
/**
@ -136,9 +153,15 @@ export const showLoadFailure = async (browserWindow: Electron.BrowserWindow, url
* @param url {String} Url that failed
* @param retryCallback {function} Callback when user clicks reload
*/
export const showNetworkConnectivityError = (browserWindow: Electron.BrowserWindow, url: string = '', retryCallback: () => void): void => {
const errorDesc = i18n.t('Network connectivity has been lost. Check your internet connection.')();
showLoadFailure(browserWindow, url, errorDesc, 0, retryCallback, true);
export const showNetworkConnectivityError = (
browserWindow: Electron.BrowserWindow,
url: string = '',
retryCallback: () => void,
): void => {
const errorDesc = i18n.t(
'Network connectivity has been lost. Check your internet connection.',
)();
showLoadFailure(browserWindow, url, errorDesc, 0, retryCallback, true);
};
/**
@ -147,26 +170,32 @@ export const showNetworkConnectivityError = (browserWindow: Electron.BrowserWind
*
* @param isNativeStyle {boolean}
*/
export const titleBarChangeDialog = async (isNativeStyle: CloudConfigDataTypes) => {
const focusedWindow = electron.BrowserWindow.getFocusedWindow();
if (!focusedWindow || !windowExists(focusedWindow)) {
return;
}
const options = {
type: 'question',
title: i18n.t('Relaunch Application')(),
message: i18n.t('Updating Title bar style requires Symphony to relaunch.')(),
detail: i18n.t('Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the Alt key.')(),
buttons: [i18n.t('Relaunch')(), i18n.t('Cancel')()],
cancelId: 1,
};
const { response } = await electron.dialog.showMessageBox(focusedWindow, options);
if (response === 0) {
logger.error(`test`, isNativeStyle);
await config.updateUserConfig({ isCustomTitleBar: isNativeStyle });
app.relaunch();
app.exit();
}
export const titleBarChangeDialog = async (
isNativeStyle: CloudConfigDataTypes,
) => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (!focusedWindow || !windowExists(focusedWindow)) {
return;
}
const options = {
type: 'question',
title: i18n.t('Relaunch Application')(),
message: i18n.t(
'Updating Title bar style requires Symphony to relaunch.',
)(),
detail: i18n.t(
'Note: When Hamburger menu is disabled, you can trigger the main menu by pressing the Alt key.',
)(),
buttons: [i18n.t('Relaunch')(), i18n.t('Cancel')()],
cancelId: 1,
};
const { response } = await dialog.showMessageBox(focusedWindow, options);
if (response === 0) {
logger.error(`test`, isNativeStyle);
await config.updateUserConfig({ isCustomTitleBar: isNativeStyle });
app.relaunch();
app.exit();
}
};
/**
@ -174,21 +203,23 @@ export const titleBarChangeDialog = async (isNativeStyle: CloudConfigDataTypes)
* @param disableGpu
*/
export const gpuRestartDialog = async (disableGpu: boolean) => {
const focusedWindow = electron.BrowserWindow.getFocusedWindow();
if (!focusedWindow || !windowExists(focusedWindow)) {
return;
}
const options = {
type: 'question',
title: i18n.t('Relaunch Application')(),
message: i18n.t('Would you like to restart and apply these new settings now?')(),
buttons: [i18n.t('Restart')(), i18n.t('Later')()],
cancelId: 1,
};
const { response } = await electron.dialog.showMessageBox(focusedWindow, options);
await config.updateUserConfig({ disableGpu });
if (response === 0) {
app.relaunch();
app.exit();
}
const focusedWindow = BrowserWindow.getFocusedWindow();
if (!focusedWindow || !windowExists(focusedWindow)) {
return;
}
const options = {
type: 'question',
title: i18n.t('Relaunch Application')(),
message: i18n.t(
'Would you like to restart and apply these new settings now?',
)(),
buttons: [i18n.t('Restart')(), i18n.t('Later')()],
cancelId: 1,
};
const { response } = await dialog.showMessageBox(focusedWindow, options);
await config.updateUserConfig({ disableGpu });
if (response === 0) {
app.relaunch();
app.exit();
}
};

View File

@ -7,135 +7,141 @@ import { windowExists } from './window-utils';
const DOWNLOAD_MANAGER_NAMESPACE = 'DownloadManager';
export interface IDownloadItem {
_id: string;
fileName: string;
savedPath: string;
total: string;
_id: string;
fileName: string;
savedPath: string;
total: string;
}
class DownloadHandler {
/**
* Show dialog for failed cases
*/
private static async showDialog(): Promise<void> {
const focusedWindow = BrowserWindow.getFocusedWindow();
const message = i18n.t('The file you are trying to open cannot be found in the specified path.', DOWNLOAD_MANAGER_NAMESPACE)();
const title = i18n.t('File not Found', DOWNLOAD_MANAGER_NAMESPACE)();
/**
* Show dialog for failed cases
*/
private static async showDialog(): Promise<void> {
const focusedWindow = BrowserWindow.getFocusedWindow();
const message = i18n.t(
'The file you are trying to open cannot be found in the specified path.',
DOWNLOAD_MANAGER_NAMESPACE,
)();
const title = i18n.t('File not Found', DOWNLOAD_MANAGER_NAMESPACE)();
if (!focusedWindow || !windowExists(focusedWindow)) {
return;
}
await dialog.showMessageBox(focusedWindow, {
message,
title,
type: 'error',
});
if (!focusedWindow || !windowExists(focusedWindow)) {
return;
}
await dialog.showMessageBox(focusedWindow, {
message,
title,
type: 'error',
});
}
private window!: Electron.WebContents | null;
private items: IDownloadItem[] = [];
/**
* Sets the window for the download handler
* @param window Window object
*/
public setWindow(window: Electron.WebContents): void {
this.window = window;
logger.info(`download-handler: Initialized download handler`);
}
/**
* Opens the downloaded file
*
* @param id {string} File ID
*/
public async openFile(id: string): Promise<void> {
const filePath = this.getFilePath(id);
const openResponse = fs.existsSync(`${filePath}`);
if (openResponse) {
const result = await shell.openPath(`${filePath}`);
if (result === '') {
return;
}
}
private window!: Electron.WebContents | null;
private items: IDownloadItem[] = [];
await DownloadHandler.showDialog();
}
/**
* Sets the window for the download handler
* @param window Window object
*/
public setWindow(window: Electron.WebContents): void {
this.window = window;
logger.info(`download-handler: Initialized download handler`);
/**
* Opens the downloaded file in finder/explorer
*
* @param id {string} File ID
*/
public showInFinder(id: string): void {
const filePath = this.getFilePath(id);
if (fs.existsSync(filePath)) {
shell.showItemInFolder(filePath);
return;
}
/**
* Opens the downloaded file
*
* @param id {string} File ID
*/
public async openFile(id: string): Promise<void> {
const filePath = this.getFilePath(id);
DownloadHandler.showDialog();
}
const openResponse = fs.existsSync(`${filePath}`);
if (openResponse) {
const result = await shell.openPath(`${filePath}`);
if (result === '') {
return;
}
}
/**
* Clears download items
*/
public clearDownloadedItems(): void {
this.items = [];
}
await DownloadHandler.showDialog();
/**
* Handle a successful download
* @param item Download item
*/
public onDownloadSuccess(item: IDownloadItem): void {
this.items.push(item);
this.sendDownloadCompleted(item);
}
/**
* Handle a failed download
*/
public onDownloadFailed(): void {
this.sendDownloadFailed();
}
/**
* Send download completed event to the renderer process
*/
private sendDownloadCompleted(item: IDownloadItem): void {
if (this.window && !this.window.isDestroyed()) {
logger.info(
`download-handler: Download completed! Informing the client!`,
);
this.window.send('download-completed', {
id: item._id,
fileDisplayName: item.fileName,
fileSize: item.total,
});
}
}
/**
* Opens the downloaded file in finder/explorer
*
* @param id {string} File ID
*/
public showInFinder(id: string): void {
const filePath = this.getFilePath(id);
if (fs.existsSync(filePath)) {
shell.showItemInFolder(filePath);
return;
}
DownloadHandler.showDialog();
/**
* Send download failed event to the renderer process
*/
private sendDownloadFailed(): void {
if (this.window && !this.window.isDestroyed()) {
logger.info(`download-handler: Download failed! Informing the client!`);
this.window.send('download-failed');
}
}
/**
* Clears download items
*/
public clearDownloadedItems(): void {
this.items = [];
}
/**
* Handle a successful download
* @param item Download item
*/
public onDownloadSuccess(item: IDownloadItem): void {
this.items.push(item);
this.sendDownloadCompleted(item);
}
/**
* Handle a failed download
*/
public onDownloadFailed(): void {
this.sendDownloadFailed();
}
/**
* Send download completed event to the renderer process
*/
private sendDownloadCompleted(item: IDownloadItem): void {
if (this.window && !this.window.isDestroyed()) {
logger.info(`download-handler: Download completed! Informing the client!`);
this.window.send('download-completed', {
id: item._id, fileDisplayName: item.fileName, fileSize: item.total,
});
}
}
/**
* Send download failed event to the renderer process
*/
private sendDownloadFailed(): void {
if (this.window && !this.window.isDestroyed()) {
logger.info(`download-handler: Download failed! Informing the client!`);
this.window.send('download-failed');
}
}
/**
* Get file path for the given item
* @param id ID of the item
*/
private getFilePath(id: string): string {
const fileIndex = this.items.findIndex((item) => {
return item._id === id;
});
return this.items[fileIndex].savedPath;
}
/**
* Get file path for the given item
* @param id ID of the item
*/
private getFilePath(id: string): string {
const fileIndex = this.items.findIndex((item) => {
return item._id === id;
});
return this.items[fileIndex].savedPath;
}
}
const downloadHandler = new DownloadHandler();

View File

@ -7,12 +7,18 @@ import { getCommandLineArgs } from '../common/utils';
import { appStats } from './stats';
// Handle custom user data path from process.argv
const userDataPathArg: string | null = getCommandLineArgs(process.argv, '--userDataPath=', false);
const userDataPath = userDataPathArg && userDataPathArg.substring(userDataPathArg.indexOf('=') + 1);
const userDataPathArg: string | null = getCommandLineArgs(
process.argv,
'--userDataPath=',
false,
);
const userDataPath =
userDataPathArg &&
userDataPathArg.substring(userDataPathArg.indexOf('=') + 1);
// force sandbox: true for all BrowserWindow instances.
if (!isNodeEnv) {
app.enableSandbox();
app.enableSandbox();
}
// need to set this explicitly if using Squirrel
@ -21,14 +27,14 @@ app.setAppUserModelId('com.symphony.electron-desktop');
// Set user data path before app ready event
if (isDevEnv) {
const devDataPath = path.join(app.getPath('appData'), 'Symphony-dev');
logger.info(`init: Setting user data path to`, devDataPath);
app.setPath('userData', devDataPath);
const devDataPath = path.join(app.getPath('appData'), 'Symphony-dev');
logger.info(`init: Setting user data path to`, devDataPath);
app.setPath('userData', devDataPath);
}
if (userDataPath) {
logger.info(`init: Setting user data path to`, userDataPath);
app.setPath('userData', userDataPath);
logger.info(`init: Setting user data path to`, userDataPath);
app.setPath('userData', userDataPath);
}
logger.info(`init: Fetch user data path`, app.getPath('userData'));

View File

@ -1,6 +1,11 @@
import { BrowserWindow, ipcMain } from 'electron';
import { apiCmds, apiName, IApiArgs, INotificationData } from '../common/api-interface';
import {
apiCmds,
apiName,
IApiArgs,
INotificationData,
} from '../common/api-interface';
import { LocaleType } from '../common/i18n';
import { logger } from '../common/logger';
import { activityDetection } from './activity-detection';
@ -16,214 +21,252 @@ import { screenSnippet } from './screen-snippet-handler';
import { activate, handleKeyPress } from './window-actions';
import { ICustomBrowserWindow, windowHandler } from './window-handler';
import {
downloadManagerAction,
isValidWindow,
sanitize,
setDataUrl,
showBadgeCount,
showPopupMenu,
updateFeaturesForCloudConfig,
updateLocale,
windowExists,
downloadManagerAction,
isValidWindow,
sanitize,
setDataUrl,
showBadgeCount,
showPopupMenu,
updateFeaturesForCloudConfig,
updateLocale,
windowExists,
} from './window-utils';
/**
* Handle API related ipc messages from renderers. Only messages from windows
* we have created are allowed.
*/
ipcMain.on(apiName.symphonyApi, async (event: Electron.IpcMainEvent, arg: IApiArgs) => {
ipcMain.on(
apiName.symphonyApi,
async (event: Electron.IpcMainEvent, arg: IApiArgs) => {
if (!isValidWindow(BrowserWindow.fromWebContents(event.sender))) {
logger.error(`main-api-handler: invalid window try to perform action, ignoring action`, arg.cmd);
return;
logger.error(
`main-api-handler: invalid window try to perform action, ignoring action`,
arg.cmd,
);
return;
}
if (!arg) {
return;
return;
}
switch (arg.cmd) {
case apiCmds.isOnline:
if (typeof arg.isOnline === 'boolean') {
windowHandler.isOnline = arg.isOnline;
}
break;
case apiCmds.setBadgeCount:
if (typeof arg.count === 'number') {
showBadgeCount(arg.count);
}
break;
case apiCmds.registerProtocolHandler:
protocolHandler.setPreloadWebContents(event.sender);
// Since we register the prococol handler window upon login,
// we make use of it and update the pod version info on SDA
windowHandler.updateVersionInfo();
// Set this to false once the SFE is completely loaded
// so, we can prevent from showing error banners
windowHandler.isWebPageLoading = false;
windowHandler.isLoggedIn = true;
break;
case apiCmds.registerLogRetriever:
registerLogRetriever(event.sender, arg.logName);
break;
case apiCmds.sendLogs:
finalizeLogExports(arg.logs);
break;
case apiCmds.badgeDataUrl:
if (typeof arg.dataUrl === 'string' && typeof arg.count === 'number') {
setDataUrl(arg.dataUrl, arg.count);
}
break;
case apiCmds.activate:
if (typeof arg.windowName === 'string') {
activate(arg.windowName);
}
break;
case apiCmds.registerLogger:
// renderer window that has a registered logger from JS.
logger.setLoggerWindow(event.sender);
break;
case apiCmds.registerActivityDetection:
if (typeof arg.period === 'number') {
// renderer window that has a registered activity detection from JS.
activityDetection.setWindowAndThreshold(event.sender, arg.period);
}
break;
case apiCmds.registerDownloadHandler:
downloadHandler.setWindow(event.sender);
break;
case apiCmds.showNotificationSettings:
if (typeof arg.windowName === 'string') {
const theme = arg.theme ? arg.theme : 'light';
windowHandler.createNotificationSettingsWindow(arg.windowName, theme);
}
break;
case apiCmds.sanitize:
if (typeof arg.windowName === 'string') {
sanitize(arg.windowName);
}
windowHandler.isWebPageLoading = true;
break;
case apiCmds.bringToFront:
// validates the user bring to front config and activates the wrapper
if (typeof arg.reason === 'string' && arg.reason === 'notification') {
const { bringToFront } = config.getConfigFields(['bringToFront']);
if (bringToFront === CloudConfigDataTypes.ENABLED) {
activate(arg.windowName, false);
}
}
break;
case apiCmds.openScreenPickerWindow:
if (Array.isArray(arg.sources) && typeof arg.id === 'number') {
windowHandler.createScreenPickerWindow(event.sender, arg.sources, arg.id);
}
break;
case apiCmds.popupMenu: {
const browserWin = BrowserWindow.fromWebContents(event.sender) as ICustomBrowserWindow;
if (browserWin && windowExists(browserWin) && browserWin.winName === apiName.mainWindowName) {
showPopupMenu({ window: browserWin });
}
break;
case apiCmds.isOnline:
if (typeof arg.isOnline === 'boolean') {
windowHandler.isOnline = arg.isOnline;
}
case apiCmds.setLocale:
if (typeof arg.locale === 'string') {
updateLocale(arg.locale as LocaleType);
}
break;
case apiCmds.keyPress:
if (typeof arg.keyCode === 'number') {
handleKeyPress(arg.keyCode);
}
break;
case apiCmds.openScreenSnippet:
screenSnippet.capture(event.sender);
break;
case apiCmds.closeScreenSnippet:
screenSnippet.cancelCapture();
break;
case apiCmds.closeWindow:
windowHandler.closeWindow(arg.windowType, arg.winKey);
break;
case apiCmds.openScreenSharingIndicator:
const { displayId, id, streamId } = arg;
if (typeof displayId === 'string' && typeof id === 'number') {
windowHandler.createScreenSharingIndicatorWindow(event.sender, displayId, id, streamId);
}
break;
case apiCmds.downloadManagerAction:
if (typeof arg.path === 'string') {
downloadManagerAction(arg.type, arg.path);
}
break;
case apiCmds.openDownloadedItem:
if (typeof arg.id === 'string') {
downloadHandler.openFile(arg.id);
}
break;
case apiCmds.showDownloadedItem:
if (typeof arg.id === 'string') {
downloadHandler.showInFinder(arg.id);
}
break;
case apiCmds.clearDownloadedItems:
downloadHandler.clearDownloadedItems();
break;
case apiCmds.restartApp:
appStateHandler.restart();
break;
case apiCmds.isMisspelled:
if (typeof arg.word === 'string') {
event.returnValue = windowHandler.spellchecker ? windowHandler.spellchecker.isMisspelled(arg.word) : false;
}
break;
case apiCmds.setIsInMeeting:
if (typeof arg.isInMeeting === 'boolean') {
memoryMonitor.setMeetingStatus(arg.isInMeeting);
}
break;
case apiCmds.memoryInfo:
if (typeof arg.memoryInfo === 'object') {
memoryMonitor.setMemoryInfo(arg.memoryInfo);
}
break;
case apiCmds.getConfigUrl:
const { url } = config.getGlobalConfigFields(['url']);
event.returnValue = url;
break;
case apiCmds.registerAnalyticsHandler:
analytics.registerPreloadWindow(event.sender);
break;
case apiCmds.setCloudConfig:
const { podLevelEntitlements, acpFeatureLevelEntitlements, pmpEntitlements, ...rest } = arg.cloudConfig as ICloudConfig;
if (podLevelEntitlements && podLevelEntitlements.autoLaunchPath && podLevelEntitlements.autoLaunchPath.match(/\\\\/g)) {
podLevelEntitlements.autoLaunchPath = podLevelEntitlements.autoLaunchPath.replace(/\\+/g, '\\');
}
logger.info('main-api-handler: ignored other values from SFE', rest);
await config.updateCloudConfig({ podLevelEntitlements, acpFeatureLevelEntitlements, pmpEntitlements });
await updateFeaturesForCloudConfig();
if (windowHandler.appMenu) {
windowHandler.appMenu.buildMenu();
}
break;
case apiCmds.setIsMana:
if (typeof arg.isMana === 'boolean') {
windowHandler.isMana = arg.isMana;
logger.info('window-handler: isMana: ' + windowHandler.isMana);
}
break;
case apiCmds.showNotification:
if (typeof arg.notificationOpts === 'object') {
const opts = arg.notificationOpts as INotificationData;
notificationHelper.showNotification(opts);
}
break;
case apiCmds.closeNotification:
if (typeof arg.notificationId === 'number') {
await notificationHelper.closeNotification(arg.notificationId);
}
break;
default:
break;
}
break;
case apiCmds.setBadgeCount:
if (typeof arg.count === 'number') {
showBadgeCount(arg.count);
}
break;
case apiCmds.registerProtocolHandler:
protocolHandler.setPreloadWebContents(event.sender);
// Since we register the prococol handler window upon login,
// we make use of it and update the pod version info on SDA
windowHandler.updateVersionInfo();
});
// Set this to false once the SFE is completely loaded
// so, we can prevent from showing error banners
windowHandler.isWebPageLoading = false;
windowHandler.isLoggedIn = true;
break;
case apiCmds.registerLogRetriever:
registerLogRetriever(event.sender, arg.logName);
break;
case apiCmds.sendLogs:
finalizeLogExports(arg.logs);
break;
case apiCmds.badgeDataUrl:
if (typeof arg.dataUrl === 'string' && typeof arg.count === 'number') {
setDataUrl(arg.dataUrl, arg.count);
}
break;
case apiCmds.activate:
if (typeof arg.windowName === 'string') {
activate(arg.windowName);
}
break;
case apiCmds.registerLogger:
// renderer window that has a registered logger from JS.
logger.setLoggerWindow(event.sender);
break;
case apiCmds.registerActivityDetection:
if (typeof arg.period === 'number') {
// renderer window that has a registered activity detection from JS.
activityDetection.setWindowAndThreshold(event.sender, arg.period);
}
break;
case apiCmds.registerDownloadHandler:
downloadHandler.setWindow(event.sender);
break;
case apiCmds.showNotificationSettings:
if (typeof arg.windowName === 'string') {
const theme = arg.theme ? arg.theme : 'light';
windowHandler.createNotificationSettingsWindow(arg.windowName, theme);
}
break;
case apiCmds.sanitize:
if (typeof arg.windowName === 'string') {
sanitize(arg.windowName);
}
windowHandler.isWebPageLoading = true;
break;
case apiCmds.bringToFront:
// validates the user bring to front config and activates the wrapper
if (typeof arg.reason === 'string' && arg.reason === 'notification') {
const { bringToFront } = config.getConfigFields(['bringToFront']);
if (bringToFront === CloudConfigDataTypes.ENABLED) {
activate(arg.windowName, false);
}
}
break;
case apiCmds.openScreenPickerWindow:
if (Array.isArray(arg.sources) && typeof arg.id === 'number') {
windowHandler.createScreenPickerWindow(
event.sender,
arg.sources,
arg.id,
);
}
break;
case apiCmds.popupMenu: {
const browserWin = BrowserWindow.fromWebContents(
event.sender,
) as ICustomBrowserWindow;
if (
browserWin &&
windowExists(browserWin) &&
browserWin.winName === apiName.mainWindowName
) {
showPopupMenu({ window: browserWin });
}
break;
}
case apiCmds.setLocale:
if (typeof arg.locale === 'string') {
updateLocale(arg.locale as LocaleType);
}
break;
case apiCmds.keyPress:
if (typeof arg.keyCode === 'number') {
handleKeyPress(arg.keyCode);
}
break;
case apiCmds.openScreenSnippet:
screenSnippet.capture(event.sender);
break;
case apiCmds.closeScreenSnippet:
screenSnippet.cancelCapture();
break;
case apiCmds.closeWindow:
windowHandler.closeWindow(arg.windowType, arg.winKey);
break;
case apiCmds.openScreenSharingIndicator:
const { displayId, id, streamId } = arg;
if (typeof displayId === 'string' && typeof id === 'number') {
windowHandler.createScreenSharingIndicatorWindow(
event.sender,
displayId,
id,
streamId,
);
}
break;
case apiCmds.downloadManagerAction:
if (typeof arg.path === 'string') {
downloadManagerAction(arg.type, arg.path);
}
break;
case apiCmds.openDownloadedItem:
if (typeof arg.id === 'string') {
downloadHandler.openFile(arg.id);
}
break;
case apiCmds.showDownloadedItem:
if (typeof arg.id === 'string') {
downloadHandler.showInFinder(arg.id);
}
break;
case apiCmds.clearDownloadedItems:
downloadHandler.clearDownloadedItems();
break;
case apiCmds.restartApp:
appStateHandler.restart();
break;
case apiCmds.isMisspelled:
if (typeof arg.word === 'string') {
event.returnValue = windowHandler.spellchecker
? windowHandler.spellchecker.isMisspelled(arg.word)
: false;
}
break;
case apiCmds.setIsInMeeting:
if (typeof arg.isInMeeting === 'boolean') {
memoryMonitor.setMeetingStatus(arg.isInMeeting);
}
break;
case apiCmds.memoryInfo:
if (typeof arg.memoryInfo === 'object') {
memoryMonitor.setMemoryInfo(arg.memoryInfo);
}
break;
case apiCmds.getConfigUrl:
const { url } = config.getGlobalConfigFields(['url']);
event.returnValue = url;
break;
case apiCmds.registerAnalyticsHandler:
analytics.registerPreloadWindow(event.sender);
break;
case apiCmds.setCloudConfig:
const {
podLevelEntitlements,
acpFeatureLevelEntitlements,
pmpEntitlements,
...rest
} = arg.cloudConfig as ICloudConfig;
if (
podLevelEntitlements &&
podLevelEntitlements.autoLaunchPath &&
podLevelEntitlements.autoLaunchPath.match(/\\\\/g)
) {
podLevelEntitlements.autoLaunchPath = podLevelEntitlements.autoLaunchPath.replace(
/\\+/g,
'\\',
);
}
logger.info('main-api-handler: ignored other values from SFE', rest);
await config.updateCloudConfig({
podLevelEntitlements,
acpFeatureLevelEntitlements,
pmpEntitlements,
});
await updateFeaturesForCloudConfig();
if (windowHandler.appMenu) {
windowHandler.appMenu.buildMenu();
}
break;
case apiCmds.setIsMana:
if (typeof arg.isMana === 'boolean') {
windowHandler.isMana = arg.isMana;
logger.info('window-handler: isMana: ' + windowHandler.isMana);
}
break;
case apiCmds.showNotification:
if (typeof arg.notificationOpts === 'object') {
const opts = arg.notificationOpts as INotificationData;
notificationHelper.showNotification(opts);
}
break;
case apiCmds.closeNotification:
if (typeof arg.notificationId === 'number') {
await notificationHelper.closeNotification(arg.notificationId);
}
break;
default:
break;
}
},
);

View File

@ -18,37 +18,42 @@ import { ICustomBrowserWindow, windowHandler } from './window-handler';
// Set automatic period substitution to false because of a bug in draft js on the client app
// See https://perzoinc.atlassian.net/browse/SDA-2215 for more details
if (isMac) {
systemPreferences.setUserDefault('NSAutomaticPeriodSubstitutionEnabled', 'string', 'false');
systemPreferences.setUserDefault(
'NSAutomaticPeriodSubstitutionEnabled',
'string',
'false',
);
}
logger.info(`App started with the args ${JSON.stringify(process.argv)}`);
const allowMultiInstance: string | boolean = getCommandLineArgs(process.argv, '--multiInstance', true) || isDevEnv;
const allowMultiInstance: string | boolean =
getCommandLineArgs(process.argv, '--multiInstance', true) || isDevEnv;
let isAppAlreadyOpen: boolean = false;
// Setting the env path child_process issue https://github.com/electron/electron/issues/7688
(async () => {
try {
const paths = await shellPath();
if (paths) {
return process.env.PATH = paths;
}
if (isMac) {
process.env.PATH = [
'./node_modules/.bin',
'/usr/local/bin',
process.env.PATH,
].join(':');
}
} catch (e) {
if (isMac) {
process.env.PATH = [
'./node_modules/.bin',
'/usr/local/bin',
process.env.PATH,
].join(':');
}
try {
const paths = await shellPath();
if (paths) {
return (process.env.PATH = paths);
}
if (isMac) {
process.env.PATH = [
'./node_modules/.bin',
'/usr/local/bin',
process.env.PATH,
].join(':');
}
} catch (e) {
if (isMac) {
process.env.PATH = [
'./node_modules/.bin',
'/usr/local/bin',
process.env.PATH,
].join(':');
}
}
})();
electronDownloader();
@ -58,12 +63,12 @@ setChromeFlags();
// Need this to prevent blank pop-out from 8.x versions
// Refer - SDA-1877 - https://github.com/electron/electron/issues/18397
if (!isElectronQA) {
app.allowRendererProcessReuse = true;
app.allowRendererProcessReuse = true;
}
// Electron sets the default protocol
if (!isDevEnv) {
app.setAsDefaultProtocolClient('symphony');
app.setAsDefaultProtocolClient('symphony');
}
/**
@ -71,62 +76,74 @@ if (!isDevEnv) {
*/
let oneStart = false;
const startApplication = async () => {
if (config.isFirstTimeLaunch()) {
logger.info(`main: This is a first time launch! will update config and handle auto launch`);
await config.setUpFirstTimeLaunch();
if (!isLinux) {
await autoLaunchInstance.handleAutoLaunch();
}
}
await app.whenReady();
if (oneStart) {
return;
if (config.isFirstTimeLaunch()) {
logger.info(
`main: This is a first time launch! will update config and handle auto launch`,
);
await config.setUpFirstTimeLaunch();
if (!isLinux) {
await autoLaunchInstance.handleAutoLaunch();
}
}
await app.whenReady();
if (oneStart) {
return;
}
logger.info('main: app is ready, performing initial checks oneStart: ' + oneStart);
oneStart = true;
createAppCacheFile();
// Picks global config values and updates them in the user config
await config.updateUserConfigOnStart();
setSessionProperties();
await windowHandler.createApplication();
logger.info(`main: created application`);
logger.info(
'main: app is ready, performing initial checks oneStart: ' + oneStart,
);
oneStart = true;
createAppCacheFile();
// Picks global config values and updates them in the user config
await config.updateUserConfigOnStart();
setSessionProperties();
await windowHandler.createApplication();
logger.info(`main: created application`);
};
// Handle multiple/single instances
if (!allowMultiInstance) {
logger.info('main: Multiple instances are not allowed, requesting lock', { allowMultiInstance });
const gotTheLock = app.requestSingleInstanceLock();
logger.info('main: Multiple instances are not allowed, requesting lock', {
allowMultiInstance,
});
const gotTheLock = app.requestSingleInstanceLock();
// quit if another instance is already running, ignore for dev env or if app was started with multiInstance flag
if (!gotTheLock) {
logger.info(`main: got the lock hence closing the new instance`, { gotTheLock });
app.exit();
} else {
logger.info(`main: Creating the first instance of the application`);
app.on('second-instance', (_event, argv) => {
// Someone tried to run a second instance, we should focus our window.
logger.info(`main: We've got a second instance of the app, will check if it's allowed and exit if not`);
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
if (isMac) {
logger.info(`main: We are on mac, so, showing the existing window`);
return mainWindow.show();
}
if (mainWindow.isMinimized()) {
logger.info(`main: our main window is minimised, will restore it!`);
mainWindow.restore();
}
mainWindow.focus();
isAppAlreadyOpen = true;
protocolHandler.processArgv(argv, isAppAlreadyOpen);
}
});
startApplication();
}
} else {
logger.info(`main: multi instance allowed, creating second instance`, { allowMultiInstance });
// quit if another instance is already running, ignore for dev env or if app was started with multiInstance flag
if (!gotTheLock) {
logger.info(`main: got the lock hence closing the new instance`, {
gotTheLock,
});
app.exit();
} else {
logger.info(`main: Creating the first instance of the application`);
app.on('second-instance', (_event, argv) => {
// Someone tried to run a second instance, we should focus our window.
logger.info(
`main: We've got a second instance of the app, will check if it's allowed and exit if not`,
);
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
if (isMac) {
logger.info(`main: We are on mac, so, showing the existing window`);
return mainWindow.show();
}
if (mainWindow.isMinimized()) {
logger.info(`main: our main window is minimised, will restore it!`);
mainWindow.restore();
}
mainWindow.focus();
isAppAlreadyOpen = true;
protocolHandler.processArgv(argv, isAppAlreadyOpen);
}
});
startApplication();
}
} else {
logger.info(`main: multi instance allowed, creating second instance`, {
allowMultiInstance,
});
startApplication();
}
/**
@ -134,22 +151,22 @@ if (!allowMultiInstance) {
* In which case we quit the app
*/
app.on('window-all-closed', () => {
logger.info(`main: all windows are closed, quitting the app!`);
app.quit();
logger.info(`main: all windows are closed, quitting the app!`);
app.quit();
});
/**
* Creates a new empty cache file when the app is quit
*/
app.on('quit', () => {
logger.info(`main: quitting the app!`);
cleanUpAppCache();
logger.info(`main: quitting the app!`);
cleanUpAppCache();
});
/**
* Cleans up reference before quiting
*/
app.on('before-quit', () => windowHandler.willQuitApp = true);
app.on('before-quit', () => (windowHandler.willQuitApp = true));
/**
* Is triggered when the application is launched
@ -158,14 +175,16 @@ app.on('before-quit', () => windowHandler.willQuitApp = true);
* This event is emitted only on macOS at this moment
*/
app.on('activate', () => {
const mainWindow: ICustomBrowserWindow | null = windowHandler.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) {
logger.info(`main: main window not existing or destroyed, creating a new instance of the main window!`);
startApplication();
return;
}
logger.info(`main: activating & showing main window now!`);
mainWindow.show();
const mainWindow: ICustomBrowserWindow | null = windowHandler.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) {
logger.info(
`main: main window not existing or destroyed, creating a new instance of the main window!`,
);
startApplication();
return;
}
logger.info(`main: activating & showing main window now!`);
mainWindow.show();
});
/**
@ -174,6 +193,8 @@ app.on('activate', () => {
* This event is emitted only on macOS at this moment
*/
app.on('open-url', (_event, url) => {
logger.info(`main: we got a protocol request with url ${url}! processing the request!`);
protocolHandler.sendProtocol(url);
logger.info(
`main: we got a protocol request with url ${url}! processing the request!`,
);
protocolHandler.sendProtocol(url);
});

View File

@ -7,118 +7,144 @@ import { windowHandler } from './window-handler';
import { windowExists } from './window-utils';
class MemoryMonitor {
private memoryInfo: Electron.ProcessMemoryInfo | undefined = undefined;
private isInMeeting: boolean;
private canReload: boolean;
private lastReloadTime?: number;
private memoryThreshold: number;
private memoryInfo: Electron.ProcessMemoryInfo | undefined = undefined;
private isInMeeting: boolean;
private canReload: boolean;
private lastReloadTime?: number;
private memoryThreshold: number;
private readonly maxIdleTime: number;
private readonly memoryRefreshThreshold: number;
private readonly maxIdleTime: number;
private readonly memoryRefreshThreshold: number;
constructor() {
this.isInMeeting = false;
this.canReload = true;
this.maxIdleTime = 4 * 60 * 60 * 1000; // user activity threshold 4 hours
this.memoryThreshold = 800 * 1024; // 800MB
this.memoryRefreshThreshold = 24 * 60 * 60 * 1000; // 24 hour
constructor() {
this.isInMeeting = false;
this.canReload = true;
this.maxIdleTime = 4 * 60 * 60 * 1000; // user activity threshold 4 hours
this.memoryThreshold = 800 * 1024; // 800MB
this.memoryRefreshThreshold = 24 * 60 * 60 * 1000; // 24 hour
}
/**
* Sets process memory from ipc events every hour
* and refreshes the client if the conditions passes
*
* @param memoryInfo {Electron.ProcessMemoryInfo}
*/
public setMemoryInfo(memoryInfo: Electron.ProcessMemoryInfo): void {
this.memoryInfo = memoryInfo;
logger.info(
`memory-monitor: setting memory info to ${JSON.stringify(memoryInfo)}`,
);
this.validateMemory();
}
/**
* Sets the web app's RTC meeting status
*
* @param isInMeeting {boolean} whether user is in an active RTC meeting
*/
public setMeetingStatus(isInMeeting: boolean): void {
this.isInMeeting = isInMeeting;
logger.info(`memory-monitor: setting meeting status to ${isInMeeting}`);
}
/**
* Sets the memory threshold
*
* @param memoryThreshold
*/
public setMemoryThreshold(memoryThreshold: number): void {
this.memoryThreshold = memoryThreshold * 1024;
}
/**
* Validates the predefined conditions and refreshes the client
*/
private validateMemory(): void {
logger.info(`memory-monitor: validating memory refresh conditions`);
const { memoryRefresh } = config.getConfigFields(['memoryRefresh']);
if (memoryRefresh !== CloudConfigDataTypes.ENABLED) {
logger.info(
`memory-monitor: memory reload is disabled in the config, not going to refresh!`,
);
return;
}
/**
* Sets process memory from ipc events every hour
* and refreshes the client if the conditions passes
*
* @param memoryInfo {Electron.ProcessMemoryInfo}
*/
public setMemoryInfo(memoryInfo: Electron.ProcessMemoryInfo): void {
this.memoryInfo = memoryInfo;
logger.info(`memory-monitor: setting memory info to ${JSON.stringify(memoryInfo)}`);
this.validateMemory();
const time = electron.powerMonitor.getSystemIdleTime();
const idleTime = time * 1000;
// for MacOS use private else use residentSet
const memoryConsumption = isMac
? this.memoryInfo && this.memoryInfo.private
: this.memoryInfo && this.memoryInfo.residentSet;
logger.info(
`memory-monitor: Checking different conditions to see if we should auto reload the app`,
);
logger.info(`memory-monitor: Is in meeting: `, this.isInMeeting);
logger.info(`memory-monitor: Is Network online: `, windowHandler.isOnline);
logger.info(`memory-monitor: Memory consumption: `, memoryConsumption);
logger.info(`memory-monitor: Idle Time: `, idleTime);
logger.info(`memory-monitor: Last Reload time: `, this.lastReloadTime);
if (this.isInMeeting) {
logger.info(
`memory-monitor: NOT RELOADING -> User is currently in a meeting. Meeting status from client: `,
this.isInMeeting,
);
return;
}
/**
* Sets the web app's RTC meeting status
*
* @param isInMeeting {boolean} whether user is in an active RTC meeting
*/
public setMeetingStatus(isInMeeting: boolean): void {
this.isInMeeting = isInMeeting;
logger.info(`memory-monitor: setting meeting status to ${isInMeeting}`);
if (!windowHandler.isOnline) {
logger.info(
`memory-monitor: NOT RELOADING -> Not connected to network. Network status: `,
windowHandler.isOnline,
);
return;
}
/**
* Sets the memory threshold
*
* @param memoryThreshold
*/
public setMemoryThreshold(memoryThreshold: number): void {
this.memoryThreshold = memoryThreshold * 1024;
if (!(memoryConsumption && memoryConsumption > this.memoryThreshold)) {
logger.info(
`memory-monitor: NOT RELOADING -> Memory consumption ${memoryConsumption} is lesser than the threshold ${this.memoryThreshold}`,
);
return;
}
/**
* Validates the predefined conditions and refreshes the client
*/
private validateMemory(): void {
logger.info(`memory-monitor: validating memory refresh conditions`);
const { memoryRefresh } = config.getConfigFields([ 'memoryRefresh' ]);
if (memoryRefresh !== CloudConfigDataTypes.ENABLED) {
logger.info(`memory-monitor: memory reload is disabled in the config, not going to refresh!`);
return;
}
const time = electron.powerMonitor.getSystemIdleTime();
const idleTime = time * 1000;
// for MacOS use private else use residentSet
const memoryConsumption = isMac ? (this.memoryInfo && this.memoryInfo.private) : (this.memoryInfo && this.memoryInfo.residentSet);
logger.info(`memory-monitor: Checking different conditions to see if we should auto reload the app`);
logger.info(`memory-monitor: Is in meeting: `, this.isInMeeting);
logger.info(`memory-monitor: Is Network online: `, windowHandler.isOnline);
logger.info(`memory-monitor: Memory consumption: `, memoryConsumption);
logger.info(`memory-monitor: Idle Time: `, idleTime);
logger.info(`memory-monitor: Last Reload time: `, this.lastReloadTime);
if (this.isInMeeting) {
logger.info(`memory-monitor: NOT RELOADING -> User is currently in a meeting. Meeting status from client: `, this.isInMeeting);
return;
}
if (!windowHandler.isOnline) {
logger.info(`memory-monitor: NOT RELOADING -> Not connected to network. Network status: `, windowHandler.isOnline);
return;
}
if (!(memoryConsumption && memoryConsumption > this.memoryThreshold)) {
logger.info(`memory-monitor: NOT RELOADING -> Memory consumption ${memoryConsumption} is lesser than the threshold ${this.memoryThreshold}`);
return;
}
if (!(idleTime > this.maxIdleTime)) {
logger.info(`memory-monitor: NOT RELOADING -> User is not idle for: `, idleTime);
return;
}
if (!this.canReload) {
logger.info(`memory-monitor: NOT RELOADING -> Already refreshed at: `, this.lastReloadTime);
return;
}
const mainWindow = windowHandler.getMainWindow();
if (!(mainWindow && windowExists(mainWindow))) {
logger.info(`memory-monitor: NOT RELOADING -> Main window doesn't exist!`);
return;
}
logger.info(`memory-monitor: RELOADING -> auto reloading the app as all the conditions are satisfied`);
windowHandler.setIsAutoReload(true);
mainWindow.reload();
this.canReload = false;
this.lastReloadTime = new Date().getTime();
setTimeout(() => {
this.canReload = true;
}, this.memoryRefreshThreshold); // prevents multiple reloading of the client within 24hrs
if (!(idleTime > this.maxIdleTime)) {
logger.info(
`memory-monitor: NOT RELOADING -> User is not idle for: `,
idleTime,
);
return;
}
if (!this.canReload) {
logger.info(
`memory-monitor: NOT RELOADING -> Already refreshed at: `,
this.lastReloadTime,
);
return;
}
const mainWindow = windowHandler.getMainWindow();
if (!(mainWindow && windowExists(mainWindow))) {
logger.info(
`memory-monitor: NOT RELOADING -> Main window doesn't exist!`,
);
return;
}
logger.info(
`memory-monitor: RELOADING -> auto reloading the app as all the conditions are satisfied`,
);
windowHandler.setIsAutoReload(true);
mainWindow.reload();
this.canReload = false;
this.lastReloadTime = new Date().getTime();
setTimeout(() => {
this.canReload = true;
}, this.memoryRefreshThreshold); // prevents multiple reloading of the client within 24hrs
}
}
const memoryMonitor = new MemoryMonitor();

View File

@ -1,40 +1,44 @@
import { Notification, NotificationConstructorOptions } from 'electron';
import { ElectronNotificationData, INotificationData, NotificationActions } from '../../common/api-interface';
import {
ElectronNotificationData,
INotificationData,
NotificationActions,
} from '../../common/api-interface';
export class ElectronNotification extends Notification {
private callback: (
actionType: NotificationActions,
data: INotificationData,
notificationData?: ElectronNotificationData,
) => void;
private options: INotificationData;
private callback: (
actionType: NotificationActions,
data: INotificationData,
notificationData?: ElectronNotificationData,
) => void;
private options: INotificationData;
constructor(options: INotificationData, callback) {
super(options as NotificationConstructorOptions);
this.callback = callback;
this.options = options;
constructor(options: INotificationData, callback) {
super(options as NotificationConstructorOptions);
this.callback = callback;
this.options = options;
this.once('click', this.onClick);
this.once('reply', this.onReply);
}
this.once('click', this.onClick);
this.once('reply', this.onReply);
}
/**
* Notification on click handler
* @param _event
* @private
*/
private onClick(_event: Event) {
this.callback(NotificationActions.notificationClicked, this.options);
}
/**
* Notification on click handler
* @param _event
* @private
*/
private onClick(_event: Event) {
this.callback(NotificationActions.notificationClicked, this.options);
}
/**
* Notification reply handler
* @param _event
* @param reply
* @private
*/
private onReply(_event: Event, reply: string) {
this.callback(NotificationActions.notificationReply, this.options, reply);
}
/**
* Notification reply handler
* @param _event
* @param reply
* @private
*/
private onReply(_event: Event, reply: string) {
this.callback(NotificationActions.notificationReply, this.options, reply);
}
}

View File

@ -1,4 +1,8 @@
import { ElectronNotificationData, INotificationData, NotificationActions } from '../../common/api-interface';
import {
ElectronNotificationData,
INotificationData,
NotificationActions,
} from '../../common/api-interface';
import { isWindowsOS } from '../../common/env';
import { notification } from '../../renderer/notification';
import { windowHandler } from '../window-handler';
@ -6,85 +10,94 @@ import { windowExists } from '../window-utils';
import { ElectronNotification } from './electron-notification';
class NotificationHelper {
private electronNotification: Map<number, ElectronNotification>;
private activeElectronNotification: Map<string, ElectronNotification>;
private electronNotification: Map<number, ElectronNotification>;
private activeElectronNotification: Map<string, ElectronNotification>;
constructor() {
this.electronNotification = new Map<number, ElectronNotification>();
this.activeElectronNotification = new Map<string, ElectronNotification>();
}
constructor() {
this.electronNotification = new Map<number, ElectronNotification>();
this.activeElectronNotification = new Map<string, ElectronNotification>();
}
/**
* Displays Electron/HTML notification based on the
* isElectronNotification flag
*
* @param options {INotificationData}
*/
public showNotification(options: INotificationData) {
if (options.isElectronNotification) {
// MacOS: Electron notification only supports static image path
options.icon = this.getIcon(options);
/**
* Displays Electron/HTML notification based on the
* isElectronNotification flag
*
* @param options {INotificationData}
*/
public showNotification(options: INotificationData) {
if (options.isElectronNotification) {
// MacOS: Electron notification only supports static image path
options.icon = this.getIcon(options);
// This is replace notification with same tag
if (this.activeElectronNotification.has(options.tag)) {
const electronNotification = this.activeElectronNotification.get(options.tag);
if (electronNotification) {
electronNotification.close();
}
this.activeElectronNotification.delete(options.tag);
}
const electronToast = new ElectronNotification(options, this.notificationCallback);
this.electronNotification.set(options.id, electronToast);
this.activeElectronNotification.set(options.tag, electronToast);
electronToast.show();
return;
// This is replace notification with same tag
if (this.activeElectronNotification.has(options.tag)) {
const electronNotification = this.activeElectronNotification.get(
options.tag,
);
if (electronNotification) {
electronNotification.close();
}
notification.showNotification(options, this.notificationCallback);
}
this.activeElectronNotification.delete(options.tag);
}
/**
* Closes a specific notification by id
*
* @param id {number} - unique id assigned to a specific notification
*/
public async closeNotification(id: number) {
if (this.electronNotification.has(id)) {
const electronNotification = this.electronNotification.get(id);
if (electronNotification) {
electronNotification.close();
}
return;
}
await notification.hideNotification(id);
const electronToast = new ElectronNotification(
options,
this.notificationCallback,
);
this.electronNotification.set(options.id, electronToast);
this.activeElectronNotification.set(options.tag, electronToast);
electronToast.show();
return;
}
notification.showNotification(options, this.notificationCallback);
}
/**
* Sends the notification actions event to the web client
*
* @param event {NotificationActions}
* @param data {ElectronNotificationData}
* @param notificationData {ElectronNotificationData}
*/
public notificationCallback(
event: NotificationActions,
data: ElectronNotificationData,
notificationData: ElectronNotificationData,
) {
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && windowExists(mainWindow) && mainWindow.webContents) {
mainWindow.webContents.send('notification-actions', { event, data, notificationData });
}
/**
* Closes a specific notification by id
*
* @param id {number} - unique id assigned to a specific notification
*/
public async closeNotification(id: number) {
if (this.electronNotification.has(id)) {
const electronNotification = this.electronNotification.get(id);
if (electronNotification) {
electronNotification.close();
}
return;
}
await notification.hideNotification(id);
}
/**
* Return the correct icon based on platform
* @param options
* @private
*/
private getIcon(options: INotificationData): string | undefined {
return isWindowsOS ? options.icon : undefined;
/**
* Sends the notification actions event to the web client
*
* @param event {NotificationActions}
* @param data {ElectronNotificationData}
* @param notificationData {ElectronNotificationData}
*/
public notificationCallback(
event: NotificationActions,
data: ElectronNotificationData,
notificationData: ElectronNotificationData,
) {
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && windowExists(mainWindow) && mainWindow.webContents) {
mainWindow.webContents.send('notification-actions', {
event,
data,
notificationData,
});
}
}
/**
* Return the correct icon based on platform
* @param options
* @private
*/
private getIcon(options: INotificationData): string | undefined {
return isWindowsOS ? options.icon : undefined;
}
}
const notificationHelper = new NotificationHelper();

View File

@ -3,14 +3,24 @@ import { logger } from '../common/logger';
import { CloudConfigDataTypes, config, IConfig } from './config-handler';
export const handlePerformanceSettings = () => {
const { customFlags } = config.getCloudConfigFields([ 'customFlags' ]) as IConfig;
const { disableThrottling } = config.getCloudConfigFields([ 'disableThrottling' ]) as any;
const { customFlags } = config.getCloudConfigFields([
'customFlags',
]) as IConfig;
const { disableThrottling } = config.getCloudConfigFields([
'disableThrottling',
]) as any;
if ((customFlags && customFlags.disableThrottling === CloudConfigDataTypes.ENABLED) || disableThrottling === CloudConfigDataTypes.ENABLED) {
logger.info(`perf-handler: Disabling power throttling!`);
powerSaveBlocker.start('prevent-display-sleep');
return;
}
if (
(customFlags &&
customFlags.disableThrottling === CloudConfigDataTypes.ENABLED) ||
disableThrottling === CloudConfigDataTypes.ENABLED
) {
logger.info(`perf-handler: Disabling power throttling!`);
powerSaveBlocker.start('prevent-display-sleep');
return;
}
logger.info(`perf-handler: Power throttling enabled as config is not set to override power throttling!`);
logger.info(
`perf-handler: Power throttling enabled as config is not set to override power throttling!`,
);
};

View File

@ -5,77 +5,95 @@ import { getCommandLineArgs } from '../common/utils';
import { activate } from './window-actions';
enum protocol {
SymphonyProtocol = 'symphony://',
SymphonyProtocol = 'symphony://',
}
class ProtocolHandler {
private static isValidProtocolUri = (uri: string): boolean =>
!!(uri && uri.startsWith(protocol.SymphonyProtocol));
private static isValidProtocolUri = (uri: string): boolean => !!(uri && uri.startsWith(protocol.SymphonyProtocol));
private preloadWebContents: Electron.WebContents | null = null;
private protocolUri: string | null = null;
private preloadWebContents: Electron.WebContents | null = null;
private protocolUri: string | null = null;
constructor() {
this.processArgv();
}
constructor() {
this.processArgv();
/**
* Stores the web contents of the preload
*
* @param webContents {Electron.WebContents}
*/
public setPreloadWebContents(webContents: Electron.WebContents): void {
this.preloadWebContents = webContents;
logger.info(
`protocol handler: SFE is active and we have a valid protocol window with web contents!`,
);
if (this.protocolUri) {
this.sendProtocol(this.protocolUri);
logger.info(
`protocol handler: we have a cached url ${this.protocolUri}, so, processed the request to SFE!`,
);
this.protocolUri = null;
}
}
/**
* Sends the protocol uri to the web app to further process
*
* @param url {String}
* @param isAppRunning {Boolean} - whether the application is running
*/
public sendProtocol(url: string, isAppRunning: boolean = true): void {
if (url && url.length > 2083) {
logger.info(
`protocol-handler: protocol handler url length is greater than 2083, not performing any action!`,
);
return;
}
logger.info(
`protocol handler: processing protocol request for the url ${url}!`,
);
if (!this.preloadWebContents || !isAppRunning) {
logger.info(
`protocol handler: app was started from the protocol request. Caching the URL ${url}!`,
);
this.protocolUri = url;
return;
}
// This is needed for mac OS as it brings pop-outs to foreground
// (if it has been previously focused) instead of main window
if (isMac) {
activate(apiName.mainWindowName);
}
/**
* Stores the web contents of the preload
*
* @param webContents {Electron.WebContents}
*/
public setPreloadWebContents(webContents: Electron.WebContents): void {
this.preloadWebContents = webContents;
logger.info(`protocol handler: SFE is active and we have a valid protocol window with web contents!`);
if (this.protocolUri) {
this.sendProtocol(this.protocolUri);
logger.info(`protocol handler: we have a cached url ${this.protocolUri}, so, processed the request to SFE!`);
this.protocolUri = null;
}
if (ProtocolHandler.isValidProtocolUri(url)) {
logger.info(
`protocol handler: our protocol request is a valid url ${url}! sending request to SFE for further action!`,
);
this.preloadWebContents.send('protocol-action', url);
}
}
/**
* Sends the protocol uri to the web app to further process
*
* @param url {String}
* @param isAppRunning {Boolean} - whether the application is running
*/
public sendProtocol(url: string, isAppRunning: boolean = true): void {
if (url && url.length > 2083) {
logger.info(`protocol-handler: protocol handler url length is greater than 2083, not performing any action!`);
return;
}
logger.info(`protocol handler: processing protocol request for the url ${url}!`);
if (!this.preloadWebContents || !isAppRunning) {
logger.info(`protocol handler: app was started from the protocol request. Caching the URL ${url}!`);
this.protocolUri = url;
return;
}
// This is needed for mac OS as it brings pop-outs to foreground
// (if it has been previously focused) instead of main window
if (isMac) {
activate(apiName.mainWindowName);
}
if (ProtocolHandler.isValidProtocolUri(url)) {
logger.info(`protocol handler: our protocol request is a valid url ${url}! sending request to SFE for further action!`);
this.preloadWebContents.send('protocol-action', url);
}
}
/**
* Handles protocol uri from process.argv
*
* @param argv {String[]} - data received from process.argv
*/
public processArgv(argv?: string[], isAppAlreadyOpen: boolean = false): void {
logger.info(`protocol handler: processing protocol args!`);
const protocolUriFromArgv = getCommandLineArgs(argv || process.argv, protocol.SymphonyProtocol, false);
if (protocolUriFromArgv) {
logger.info(`protocol handler: we have a protocol request for the url ${protocolUriFromArgv}!`);
this.sendProtocol(protocolUriFromArgv, isAppAlreadyOpen);
}
/**
* Handles protocol uri from process.argv
*
* @param argv {String[]} - data received from process.argv
*/
public processArgv(argv?: string[], isAppAlreadyOpen: boolean = false): void {
logger.info(`protocol handler: processing protocol args!`);
const protocolUriFromArgv = getCommandLineArgs(
argv || process.argv,
protocol.SymphonyProtocol,
false,
);
if (protocolUriFromArgv) {
logger.info(
`protocol handler: we have a protocol request for the url ${protocolUriFromArgv}!`,
);
this.sendProtocol(protocolUriFromArgv, isAppAlreadyOpen);
}
}
}
const protocolHandler = new ProtocolHandler();

View File

@ -1,6 +1,5 @@
import * as archiver from 'archiver';
import { app, BrowserWindow, dialog, shell } from 'electron';
import * as electron from 'electron';
import { app, BrowserWindow, crashReporter, dialog, shell } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
@ -18,78 +17,87 @@ import { logger } from '../common/logger';
* @param fileExtensions {Array} array of file ext
* @return {Promise<void>}
*/
const generateArchiveForDirectory = (source: string, destination: string, fileExtensions: string[], retrievedLogs: ILogs[]): Promise<void> => {
const generateArchiveForDirectory = (
source: string,
destination: string,
fileExtensions: string[],
retrievedLogs: ILogs[],
): Promise<void> => {
return new Promise((resolve, reject) => {
logger.info(`reports-handler: generating archive for directory ${source}`);
const output = fs.createWriteStream(destination);
const archive = archiver('zip', { zlib: { level: 9 } });
const filesForCleanup: string[] = [];
return new Promise((resolve, reject) => {
logger.info(`reports-handler: generating archive for directory ${source}`);
const output = fs.createWriteStream(destination);
const archive = archiver('zip', { zlib: { level: 9 } });
const filesForCleanup: string[] = [];
output.on('close', () => {
for (const file of filesForCleanup) {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
}
logger.info(`reports-handler: generated archive for directory ${source}`);
return resolve();
});
archive.on('error', (err: Error) => {
for (const file of filesForCleanup) {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
}
logger.error(`reports-handler: error archiving directory for ${source} with error ${err}`);
return reject(err);
});
archive.pipe(output);
const files = fs.readdirSync(source);
files
.filter((file) => fileExtensions.indexOf(path.extname(file)) !== -1)
.forEach((file) => {
switch (path.extname(file)) {
case '.log':
archive.file(source + '/' + file, { name: 'logs/' + file });
break;
case '.dmp':
case '.txt': // on Windows .txt files will be created as part of crash dump
archive.file(source + '/' + file, { name: 'crashes/' + file });
break;
default:
break;
}
});
for (const logs of retrievedLogs) {
for (const logFile of logs.logFiles) {
const file = path.join( source, logFile.filename );
fs.writeFileSync(file, logFile.contents );
archive.file(file, { name: 'logs/' + logFile.filename });
filesForCleanup.push(file);
}
output.on('close', () => {
for (const file of filesForCleanup) {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
archive.finalize();
}
logger.info(`reports-handler: generated archive for directory ${source}`);
return resolve();
});
archive.on('error', (err: Error) => {
for (const file of filesForCleanup) {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
}
logger.error(
`reports-handler: error archiving directory for ${source} with error ${err}`,
);
return reject(err);
});
archive.pipe(output);
const files = fs.readdirSync(source);
files
.filter((file) => fileExtensions.indexOf(path.extname(file)) !== -1)
.forEach((file) => {
switch (path.extname(file)) {
case '.log':
archive.file(source + '/' + file, { name: 'logs/' + file });
break;
case '.dmp':
case '.txt': // on Windows .txt files will be created as part of crash dump
archive.file(source + '/' + file, { name: 'crashes/' + file });
break;
default:
break;
}
});
for (const logs of retrievedLogs) {
for (const logFile of logs.logFiles) {
const file = path.join(source, logFile.filename);
fs.writeFileSync(file, logFile.contents);
archive.file(file, { name: 'logs/' + logFile.filename });
filesForCleanup.push(file);
}
}
archive.finalize();
});
};
let logWebContents: Electron.WebContents;
const logTypes: string[] = [];
const receivedLogs: ILogs[] = [];
export const registerLogRetriever = (sender: Electron.WebContents, logName: string): void => {
logWebContents = sender;
logTypes.push( logName );
export const registerLogRetriever = (
sender: Electron.WebContents,
logName: string,
): void => {
logWebContents = sender;
logTypes.push(logName);
};
export const collectLogs = (): void => {
receivedLogs.length = 0;
logWebContents.send('collect-logs' );
receivedLogs.length = 0;
logWebContents.send('collect-logs');
};
/**
@ -99,103 +107,120 @@ export const collectLogs = (): void => {
* Windows - AppData\Roaming\Symphony\logs
*/
export const packageLogs = (retrievedLogs: ILogs[]): void => {
const FILE_EXTENSIONS = [ '.log' ];
const MAC_LOGS_PATH = '/Library/Logs/Symphony/';
const LINUX_LOGS_PATH = '/.config/Symphony/';
const WINDOWS_LOGS_PATH = '\\AppData\\Local\\Symphony\\Symphony\\logs';
const FILE_EXTENSIONS = ['.log'];
const MAC_LOGS_PATH = '/Library/Logs/Symphony/';
const LINUX_LOGS_PATH = '/.config/Symphony/';
const WINDOWS_LOGS_PATH = '\\AppData\\Local\\Symphony\\Symphony\\logs';
const logsPath = isMac ? MAC_LOGS_PATH : isLinux ? LINUX_LOGS_PATH : WINDOWS_LOGS_PATH;
const source = app.getPath('home') + logsPath;
const focusedWindow = BrowserWindow.getFocusedWindow();
const logsPath = isMac
? MAC_LOGS_PATH
: isLinux
? LINUX_LOGS_PATH
: WINDOWS_LOGS_PATH;
const source = app.getPath('home') + logsPath;
const focusedWindow = BrowserWindow.getFocusedWindow();
if (!fs.existsSync(source) && focusedWindow && !focusedWindow.isDestroyed()) {
logger.error(`reports-handler: Can't find any logs to share!`);
if (!fs.existsSync(source) && focusedWindow && !focusedWindow.isDestroyed()) {
logger.error(`reports-handler: Can't find any logs to share!`);
dialog.showMessageBox(focusedWindow, {
message: i18n.t(`Can't find any logs to share!`)(),
title: i18n.t('Failed!')(),
type: 'error',
});
return;
}
const destPath = isMac || isLinux ? '/logs_symphony_' : '\\logs_symphony_';
const timestamp = new Date().getTime();
const destination = app.getPath('downloads') + destPath + timestamp + '.zip';
generateArchiveForDirectory(
source,
destination,
FILE_EXTENSIONS,
retrievedLogs,
)
.then(() => {
shell.showItemInFolder(destination);
})
.catch((err) => {
if (focusedWindow && !focusedWindow.isDestroyed()) {
logger.error(`reports-handler: Can't share logs due to error ${err}`);
dialog.showMessageBox(focusedWindow, {
message: i18n.t(`Can't find any logs to share!`)(),
title: i18n.t('Failed!')(),
type: 'error',
});
return;
}
const destPath = (isMac || isLinux) ? '/logs_symphony_' : '\\logs_symphony_';
const timestamp = new Date().getTime();
const destination = app.getPath('downloads') + destPath + timestamp + '.zip';
generateArchiveForDirectory(source, destination, FILE_EXTENSIONS, retrievedLogs)
.then(() => {
shell.showItemInFolder(destination);
})
.catch((err) => {
if (focusedWindow && !focusedWindow.isDestroyed()) {
logger.error(`reports-handler: Can't share logs due to error ${err}`);
dialog.showMessageBox(focusedWindow, {
message: `${i18n.t('Unable to generate logs due to ')()} ${err}`,
title: i18n.t('Failed!')(),
type: 'error',
});
}
message: `${i18n.t('Unable to generate logs due to ')()} ${err}`,
title: i18n.t('Failed!')(),
type: 'error',
});
}
});
};
export const finalizeLogExports = (logs: ILogs) => {
receivedLogs.push(logs);
receivedLogs.push(logs);
let allReceived = true;
for (const logType of logTypes) {
const found = receivedLogs.some((log) => log.logName === logType);
if (!found) {
allReceived = false;
}
let allReceived = true;
for (const logType of logTypes) {
const found = receivedLogs.some((log) => log.logName === logType);
if (!found) {
allReceived = false;
}
}
if (allReceived) {
packageLogs(receivedLogs);
receivedLogs.length = 0;
}
if (allReceived) {
packageLogs(receivedLogs);
receivedLogs.length = 0;
}
};
export const exportLogs = (): void => {
if (logTypes.length > 0) {
collectLogs();
} else {
packageLogs([]);
}
if (logTypes.length > 0) {
collectLogs();
} else {
packageLogs([]);
}
};
/**
* Compress and export crash dump stored under system crashes directory
*/
export const exportCrashDumps = (): void => {
const FILE_EXTENSIONS = isMac ? [ '.dmp' ] : [ '.dmp', '.txt' ];
const crashesDirectory = (electron.crashReporter as any).getCrashesDirectory();
const source = isMac ? crashesDirectory + '/completed' : crashesDirectory;
const focusedWindow = BrowserWindow.getFocusedWindow();
const FILE_EXTENSIONS = isMac ? ['.dmp'] : ['.dmp', '.txt'];
const crashesDirectory = (crashReporter as any).getCrashesDirectory();
const source = isMac ? crashesDirectory + '/completed' : crashesDirectory;
const focusedWindow = BrowserWindow.getFocusedWindow();
if (!fs.existsSync(source) || fs.readdirSync(source).length === 0 && focusedWindow && !focusedWindow.isDestroyed()) {
electron.dialog.showMessageBox(focusedWindow as BrowserWindow, {
message: i18n.t('No crashes available to share')(),
title: i18n.t('Failed!')(),
type: 'error',
});
return;
}
const destPath = (isMac || isLinux) ? '/crashes_symphony_' : '\\crashes_symphony_';
const timestamp = new Date().getTime();
const destination = electron.app.getPath('downloads') + destPath + timestamp + '.zip';
generateArchiveForDirectory(source, destination, FILE_EXTENSIONS, [])
.then(() => {
electron.shell.showItemInFolder(destination);
})
.catch((err) => {
if (focusedWindow && !focusedWindow.isDestroyed()) {
electron.dialog.showMessageBox(focusedWindow, {
message: `${i18n.t('Unable to generate crash reports due to ')()} ${err}`,
title: i18n.t('Failed!')(),
type: 'error',
});
}
if (
!fs.existsSync(source) ||
(fs.readdirSync(source).length === 0 &&
focusedWindow &&
!focusedWindow.isDestroyed())
) {
dialog.showMessageBox(focusedWindow as BrowserWindow, {
message: i18n.t('No crashes available to share')(),
title: i18n.t('Failed!')(),
type: 'error',
});
return;
}
const destPath =
isMac || isLinux ? '/crashes_symphony_' : '\\crashes_symphony_';
const timestamp = new Date().getTime();
const destination = app.getPath('downloads') + destPath + timestamp + '.zip';
generateArchiveForDirectory(source, destination, FILE_EXTENSIONS, [])
.then(() => {
shell.showItemInFolder(destination);
})
.catch((err) => {
if (focusedWindow && !focusedWindow.isDestroyed()) {
dialog.showMessageBox(focusedWindow, {
message: `${i18n.t(
'Unable to generate crash reports due to ',
)()} ${err}`,
title: i18n.t('Failed!')(),
type: 'error',
});
}
});
};

View File

@ -15,7 +15,11 @@ import {
} from '../common/env';
import { i18n } from '../common/i18n';
import { logger } from '../common/logger';
import { analytics, AnalyticsElements, ScreenSnippetActionTypes } from './analytics-handler';
import {
analytics,
AnalyticsElements,
ScreenSnippetActionTypes,
} from './analytics-handler';
import { updateAlwaysOnTop } from './window-actions';
import { windowHandler } from './window-handler';
import { windowExists } from './window-utils';
@ -43,19 +47,31 @@ class ScreenSnippet {
this.captureUtil = isMac
? '/usr/sbin/screencapture'
: isDevEnv
? path.join(
? path.join(
__dirname,
'../../../node_modules/screen-snippet/ScreenSnippet.exe',
)
: path.join(path.dirname(app.getPath('exe')), 'ScreenSnippet.exe');
: path.join(path.dirname(app.getPath('exe')), 'ScreenSnippet.exe');
if (isLinux) {
this.captureUtil = '/usr/bin/gnome-screenshot';
}
ipcMain.on('snippet-analytics-data', async (_event, eventData: { element: AnalyticsElements, type: ScreenSnippetActionTypes }) => {
analytics.track({ element: eventData.element, action_type: eventData.type });
});
ipcMain.on(
'snippet-analytics-data',
async (
_event,
eventData: {
element: AnalyticsElements;
type: ScreenSnippetActionTypes;
},
) => {
analytics.track({
element: eventData.element,
action_type: eventData.type,
});
},
);
}
/**
@ -110,9 +126,15 @@ class ScreenSnippet {
try {
await this.execCmd(this.captureUtil, this.captureUtilArgs);
if (windowHandler.isMana) {
logger.info('screen-snippet-handler: Attempting to extract image dimensions from: ' + this.outputFilePath);
logger.info(
'screen-snippet-handler: Attempting to extract image dimensions from: ' +
this.outputFilePath,
);
const dimensions = this.getImageDimensions(this.outputFilePath);
logger.info('screen-snippet-handler: Extracted dimensions from image: ' + JSON.stringify(dimensions));
logger.info(
'screen-snippet-handler: Extracted dimensions from image: ' +
JSON.stringify(dimensions),
);
if (!dimensions) {
logger.error('screen-snippet-handler: Could not get image size');
return;
@ -190,17 +212,17 @@ class ScreenSnippet {
private execCmd(
captureUtil: string,
captureUtilArgs: ReadonlyArray<string>,
): Promise<ChildProcess> {
): Promise<void> {
logger.info(
`screen-snippet-handlers: execCmd ${captureUtil} ${captureUtilArgs}`,
);
return new Promise<ChildProcess>((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
return (this.child = execFile(
captureUtil,
captureUtilArgs,
(error: ExecException | null) => {
if (error && error.killed) {
// processs was killed, just resolve with no data.
// process was killed, just resolve with no data.
return reject(error);
}
resolve();
@ -261,7 +283,9 @@ class ScreenSnippet {
* Gets the dimensions of an image
* @param filePath path to file to get image dimensions of
*/
private getImageDimensions(filePath: string): {
private getImageDimensions(
filePath: string,
): {
height: number;
width: number;
} {
@ -276,25 +300,33 @@ class ScreenSnippet {
* @param webContents A browser window's web contents object
*/
private uploadSnippet(webContents: Electron.webContents) {
ipcMain.once('upload-snippet', async (_event, snippetData: { screenSnippetPath: string, mergedImageData: string }) => {
try {
windowHandler.closeSnippingToolWindow();
const [type, data] = snippetData.mergedImageData.split(',');
const payload = {
message: 'SUCCESS',
data,
type,
};
logger.info('screen-snippet-handler: Snippet uploaded correctly, sending payload to SFE');
webContents.send('screen-snippet-data', payload);
await this.verifyAndUpdateAlwaysOnTop();
} catch (error) {
await this.verifyAndUpdateAlwaysOnTop();
logger.error(
`screen-snippet-handler: upload of screen capture failed with error: ${error}!`,
);
}
});
ipcMain.once(
'upload-snippet',
async (
_event,
snippetData: { screenSnippetPath: string; mergedImageData: string },
) => {
try {
windowHandler.closeSnippingToolWindow();
const [type, data] = snippetData.mergedImageData.split(',');
const payload = {
message: 'SUCCESS',
data,
type,
};
logger.info(
'screen-snippet-handler: Snippet uploaded correctly, sending payload to SFE',
);
webContents.send('screen-snippet-data', payload);
await this.verifyAndUpdateAlwaysOnTop();
} catch (error) {
await this.verifyAndUpdateAlwaysOnTop();
logger.error(
`screen-snippet-handler: upload of screen capture failed with error: ${error}!`,
);
}
},
);
}
}

View File

@ -1,7 +1,11 @@
import { app, MenuItem } from 'electron';
import * as path from 'path';
import { ContextMenuBuilder, DictionarySync, SpellCheckHandler } from 'electron-spellchecker';
import {
ContextMenuBuilder,
DictionarySync,
SpellCheckHandler,
} from 'electron-spellchecker';
import { isDevEnv, isMac } from '../common/env';
import { i18n, LocaleType } from '../common/i18n';
import { logger } from '../common/logger';
@ -9,123 +13,139 @@ import { ICustomBrowserWindow } from './window-handler';
import { reloadWindow } from './window-utils';
export class SpellChecker {
public locale: LocaleType = 'en-US';
private readonly spellCheckHandler: SpellCheckHandler;
private readonly dictionaryPath: string | undefined;
private readonly dictionarySync: DictionarySync;
public locale: LocaleType = 'en-US';
private readonly spellCheckHandler: SpellCheckHandler;
private readonly dictionaryPath: string | undefined;
private readonly dictionarySync: DictionarySync;
constructor() {
const dictionariesDirName = 'dictionaries';
if (isDevEnv) {
this.dictionaryPath = path.join(app.getAppPath(), dictionariesDirName);
} else {
const execPath = path.dirname(app.getPath('exe'));
this.dictionaryPath = path.join(execPath, (isMac) ? '..' : '', dictionariesDirName);
}
this.dictionarySync = new DictionarySync(this.dictionaryPath);
this.spellCheckHandler = new SpellCheckHandler(this.dictionarySync);
this.spellCheckHandler.automaticallyIdentifyLanguages = false;
// language is switched w.r.t to the current system language.
if (!isMac) {
const sysLocale = app.getLocale() || 'en-US';
this.spellCheckHandler.switchLanguage(sysLocale);
}
app.on('web-contents-created', (_event, webContents): void => {
this.attachToWebContents(webContents);
});
constructor() {
const dictionariesDirName = 'dictionaries';
if (isDevEnv) {
this.dictionaryPath = path.join(app.getAppPath(), dictionariesDirName);
} else {
const execPath = path.dirname(app.getPath('exe'));
this.dictionaryPath = path.join(
execPath,
isMac ? '..' : '',
dictionariesDirName,
);
}
this.dictionarySync = new DictionarySync(this.dictionaryPath);
this.spellCheckHandler = new SpellCheckHandler(this.dictionarySync);
this.spellCheckHandler.automaticallyIdentifyLanguages = false;
// language is switched w.r.t to the current system language.
if (!isMac) {
const sysLocale = app.getLocale() || 'en-US';
this.spellCheckHandler.switchLanguage(sysLocale);
}
/**
* Attaches context-menu event for every webContents
*
* @param webContents {Electron.WebContents}
*/
public attachToWebContents(webContents: Electron.WebContents): void {
const contextMenuBuilder = new ContextMenuBuilder(this.spellCheckHandler, webContents, false, this.processMenu);
app.on('web-contents-created', (_event, webContents): void => {
this.attachToWebContents(webContents);
});
}
/**
* Attaches context-menu event for every webContents
*
* @param webContents {Electron.WebContents}
*/
public attachToWebContents(webContents: Electron.WebContents): void {
const contextMenuBuilder = new ContextMenuBuilder(
this.spellCheckHandler,
webContents,
false,
this.processMenu,
);
contextMenuBuilder.setAlternateStringFormatter(this.getStringTable());
this.locale = i18n.getLocale();
logger.info(
`spell-check-handler: Building context menu with locale ${this.locale}!`,
);
const contextMenuListener = (_event, info) => {
if (this.locale !== i18n.getLocale()) {
contextMenuBuilder.setAlternateStringFormatter(this.getStringTable());
this.locale = i18n.getLocale();
logger.info(`spell-check-handler: Building context menu with locale ${this.locale}!`);
const contextMenuListener = (_event, info) => {
if (this.locale !== i18n.getLocale()) {
contextMenuBuilder.setAlternateStringFormatter(this.getStringTable());
this.locale = i18n.getLocale();
}
contextMenuBuilder.showPopupMenu(info);
};
}
contextMenuBuilder.showPopupMenu(info);
};
webContents.on('context-menu', contextMenuListener);
webContents.once('destroyed', () => {
webContents.removeListener('context-menu', contextMenuListener);
});
webContents.on('context-menu', contextMenuListener);
webContents.once('destroyed', () => {
webContents.removeListener('context-menu', contextMenuListener);
});
}
/**
* Predicts if the given text is misspelled
* @param text
*/
public isMisspelled(text: string): boolean {
if (!this.spellCheckHandler) {
return false;
}
return this.spellCheckHandler.isMisspelled(text);
}
/**
* Predicts if the given text is misspelled
* @param text
*/
public isMisspelled(text: string): boolean {
if (!this.spellCheckHandler) {
return false;
}
return this.spellCheckHandler.isMisspelled(text);
}
/**
* Builds the string table for context menu
*
* @return {Object} - String table for context menu
*/
private getStringTable(): object {
const namespace = 'ContextMenu';
return {
copyMail: () => i18n.t('Copy Email Address', namespace)(),
copyLinkUrl: () => i18n.t('Copy Link', namespace)(),
openLinkUrl: () => i18n.t('Open Link', namespace)(),
copyImageUrl: () => i18n.t('Copy Image URL', namespace)(),
copyImage: () => i18n.t('Copy Image', namespace)(),
addToDictionary: () => i18n.t('Add to Dictionary', namespace)(),
lookUpDefinition: (lookup) => {
const formattedString = i18n.t('Look Up {searchText}', namespace)( { searchText: lookup.word });
return formattedString || `Look Up ${lookup.word}`;
},
searchGoogle: () => i18n.t('Search with Google', namespace)(),
cut: () => i18n.t('Cut')(),
copy: () => i18n.t('Copy')(),
paste: () => i18n.t('Paste')(),
inspectElement: () => i18n.t('Inspect Element', namespace)(),
};
}
/**
* Method to add default menu items to the
* menu that was generated by ContextMenuBuilder
*
* This method get invoked by electron-spellchecker
* before showing the context menu
*
* @param menu
* @returns menu
*/
private processMenu(menu: Electron.Menu): Electron.Menu {
let isLink = false;
menu.items.map((item) => {
if (item.label === 'Copy Link') {
isLink = true;
}
return item;
});
if (!isLink) {
menu.append(new MenuItem({ type: 'separator' }));
menu.append(new MenuItem({
accelerator: 'CmdOrCtrl+R',
label: i18n.t('Reload')(),
click: (_menuItem, browserWindow , _event) => {
reloadWindow(browserWindow as ICustomBrowserWindow);
},
}));
}
return menu;
/**
* Builds the string table for context menu
*
* @return {Object} - String table for context menu
*/
private getStringTable(): object {
const namespace = 'ContextMenu';
return {
copyMail: () => i18n.t('Copy Email Address', namespace)(),
copyLinkUrl: () => i18n.t('Copy Link', namespace)(),
openLinkUrl: () => i18n.t('Open Link', namespace)(),
copyImageUrl: () => i18n.t('Copy Image URL', namespace)(),
copyImage: () => i18n.t('Copy Image', namespace)(),
addToDictionary: () => i18n.t('Add to Dictionary', namespace)(),
lookUpDefinition: (lookup) => {
const formattedString = i18n.t(
'Look Up {searchText}',
namespace,
)({ searchText: lookup.word });
return formattedString || `Look Up ${lookup.word}`;
},
searchGoogle: () => i18n.t('Search with Google', namespace)(),
cut: () => i18n.t('Cut')(),
copy: () => i18n.t('Copy')(),
paste: () => i18n.t('Paste')(),
inspectElement: () => i18n.t('Inspect Element', namespace)(),
};
}
/**
* Method to add default menu items to the
* menu that was generated by ContextMenuBuilder
*
* This method get invoked by electron-spellchecker
* before showing the context menu
*
* @param menu
* @returns menu
*/
private processMenu(menu: Electron.Menu): Electron.Menu {
let isLink = false;
menu.items.map((item) => {
if (item.label === 'Copy Link') {
isLink = true;
}
return item;
});
if (!isLink) {
menu.append(new MenuItem({ type: 'separator' }));
menu.append(
new MenuItem({
accelerator: 'CmdOrCtrl+R',
label: i18n.t('Reload')(),
click: (_menuItem, browserWindow, _event) => {
reloadWindow(browserWindow as ICustomBrowserWindow);
},
}),
);
}
return menu;
}
}

View File

@ -3,102 +3,119 @@ import * as os from 'os';
import { logger } from '../common/logger';
export class AppStats {
private MB_IN_BYTES = 1048576;
private MB_IN_BYTES = 1048576;
/**
* Logs all statistics of the app
*/
public logStats() {
this.logSystemStats();
this.logProcessInfo();
this.logGPUStats();
this.logAppMetrics();
this.logConfigurationData();
this.logAppEvents();
}
/**
* Logs all statistics of the app
*/
public logStats() {
this.logSystemStats();
this.logProcessInfo();
this.logGPUStats();
this.logAppMetrics();
this.logConfigurationData();
this.logAppEvents();
}
/**
* Logs system related statistics
*/
private logSystemStats() {
logger.info(
`-----------------Gathering system information-----------------`,
);
logger.info(`Network Info -> `, os.networkInterfaces());
logger.info(`CPU Info -> `, os.cpus());
logger.info(`Operating System -> `, os.type());
logger.info(`Platform -> `, os.platform());
logger.info(`Architecture -> `, os.arch());
logger.info(`Hostname -> `, os.hostname());
logger.info(`Temp Directory -> `, os.tmpdir());
logger.info(`Home Directory -> `, os.homedir());
logger.info(`Total Memory (MB) -> `, os.totalmem() / this.MB_IN_BYTES);
logger.info(`Free Memory (MB) -> `, os.freemem() / this.MB_IN_BYTES);
logger.info(`Load Average -> `, os.loadavg());
logger.info(`Uptime -> `, os.uptime());
logger.info(`User Info (OS Returned) -> `, os.userInfo());
}
/**
* Logs system related statistics
*/
private logSystemStats() {
logger.info(`-----------------Gathering system information-----------------`);
logger.info( `Network Info -> `, os.networkInterfaces());
logger.info( `CPU Info -> `, os.cpus());
logger.info( `Operating System -> `, os.type());
logger.info( `Platform -> `, os.platform());
logger.info( `Architecture -> `, os.arch());
logger.info( `Hostname -> `, os.hostname());
logger.info( `Temp Directory -> `, os.tmpdir());
logger.info( `Home Directory -> `, os.homedir());
logger.info( `Total Memory (MB) -> `, os.totalmem() / this.MB_IN_BYTES);
logger.info( `Free Memory (MB) -> `, os.freemem() / this.MB_IN_BYTES);
logger.info( `Load Average -> `, os.loadavg());
logger.info( `Uptime -> `, os.uptime());
logger.info( `User Info (OS Returned) -> `, os.userInfo());
}
/**
* Logs GPU Statistics
*/
private logGPUStats() {
logger.info(`-----------------Gathering GPU information-----------------`);
logger.info(`GPU Feature Status -> `, app.getGPUFeatureStatus());
}
/**
* Logs GPU Statistics
*/
private logGPUStats() {
logger.info( `-----------------Gathering GPU information-----------------`);
logger.info( `GPU Feature Status -> `, app.getGPUFeatureStatus());
}
/**
* Logs Configuration Data
*/
private logConfigurationData() {
logger.info(
`-----------------App Configuration Information-----------------`,
);
logger.info(`stats: Is app packaged? ${app.isPackaged}`);
}
/**
* Logs Configuration Data
*/
private logConfigurationData() {
logger.info(`-----------------App Configuration Information-----------------`);
logger.info(`stats: Is app packaged? ${app.isPackaged}`);
}
/**
* Logs App metrics
*/
private logAppMetrics() {
logger.info(`-----------------Gathering App Metrics-----------------`);
const metrics = app.getAppMetrics();
metrics.forEach((metric) => {
logger.info(
`stats: PID -> ${metric.pid}, Type -> ${metric.type}, CPU Usage -> `,
metric.cpu,
);
});
}
/**
* Logs App metrics
*/
private logAppMetrics() {
logger.info(`-----------------Gathering App Metrics-----------------`);
const metrics = app.getAppMetrics();
metrics.forEach((metric) => {
logger.info(`stats: PID -> ${metric.pid}, Type -> ${metric.type}, CPU Usage -> `, metric.cpu);
});
}
/**
* Logs App events as they occur dynamically
*/
private logAppEvents() {
const events = [
'will-finish-launching',
'ready',
'window-all-closed',
'before-quit',
'will-quit',
'quit',
'open-file',
'open-url',
'activate',
'browser-window-created',
'web-contents-created',
'certificate-error',
'login',
'gpu-process-crashed',
'accessibility-support-changed',
'session-created',
'second-instance',
];
/**
* Logs App events as they occur dynamically
*/
private logAppEvents() {
const events = [
'will-finish-launching', 'ready', 'window-all-closed', 'before-quit', 'will-quit', 'quit',
'open-file', 'open-url', 'activate',
'browser-window-created', 'web-contents-created', 'certificate-error', 'login', 'gpu-process-crashed',
'accessibility-support-changed', 'session-created', 'second-instance',
];
events.forEach((appEvent: any) => {
app.on(appEvent, () => {
logger.info(`stats: App Event Occurred: ${appEvent}`);
});
});
}
events.forEach((appEvent: any) => {
app.on(appEvent, () => {
logger.info(`stats: App Event Occurred: ${appEvent}`);
});
});
}
/**
* Logs process info
*/
private logProcessInfo() {
logger.info(`-----------------Gathering Process Info-----------------`);
logger.info(`stats: Is default app? ${process.defaultApp}`);
logger.info(`stats: Is Mac Store app? ${process.mas}`);
logger.info(`stats: Is Windows Store app? ${process.windowsStore}`);
logger.info(`stats: Resources Path? ${process.resourcesPath}`);
logger.info(`stats: Chrome Version? ${process.versions.chrome}`);
logger.info(`stats: Electron Version? ${process.versions.electron}`);
}
/**
* Logs process info
*/
private logProcessInfo() {
logger.info(`-----------------Gathering Process Info-----------------`);
logger.info(`stats: Is default app? ${process.defaultApp}`);
logger.info(`stats: Is Mac Store app? ${process.mas}`);
logger.info(`stats: Is Windows Store app? ${process.windowsStore}`);
logger.info(`stats: Resources Path? ${process.resourcesPath}`);
logger.info(`stats: Chrome Version? ${process.versions.chrome}`);
logger.info(`stats: Electron Version? ${process.versions.electron}`);
}
}
const appStats = new AppStats();
export {
appStats,
};
export { appStats };

View File

@ -3,26 +3,28 @@ import { logger } from '../common/logger';
const ttlExpiryTime = -1;
export const getExpiryTime = (): number => {
return ttlExpiryTime;
return ttlExpiryTime;
};
/**
* Checks to see if the build is expired against a TTL expiry time
*/
export const checkIfBuildExpired = (): boolean => {
logger.info(`ttl-handler: Checking for build expiry`);
logger.info(`ttl-handler: Checking for build expiry`);
const expiryTime = getExpiryTime();
const expiryTime = getExpiryTime();
if (expiryTime <= -1) {
logger.info(`ttl-handler: Expiry not applicable for this build`);
return false;
}
if (expiryTime <= -1) {
logger.info(`ttl-handler: Expiry not applicable for this build`);
return false;
}
const currentDate: Date = new Date();
const expiryDate: Date = new Date(expiryTime);
logger.info(`ttl-handler: Current Time: ${currentDate.getTime()}, Expiry Time: ${expiryDate.getTime()}`);
const currentDate: Date = new Date();
const expiryDate: Date = new Date(expiryTime);
logger.info(
`ttl-handler: Current Time: ${currentDate.getTime()}, Expiry Time: ${expiryDate.getTime()}`,
);
const buildExpired = currentDate.getTime() > expiryDate.getTime();
return buildExpired;
const buildExpired = currentDate.getTime() > expiryDate.getTime();
return buildExpired;
};

View File

@ -1,217 +1,263 @@
import { net } from 'electron';
import * as nodeURL from 'url';
import { buildNumber, clientVersion, optionalDependencies, searchAPIVersion, sfeClientType, sfeVersion, version } from '../../package.json';
import {
buildNumber,
clientVersion,
optionalDependencies,
searchAPIVersion,
sfeClientType,
sfeVersion,
version,
} from '../../package.json';
import { logger } from '../common/logger';
import { config, IGlobalConfig } from './config-handler';
interface IVersionInfo {
clientVersion: string;
buildNumber: string;
sfeVersion: string;
sfeClientType: string;
sdaVersion: string;
sdaBuildNumber: string;
electronVersion: string;
chromeVersion: string;
v8Version: string;
nodeVersion: string;
openSslVersion: string;
zlibVersion: string;
uvVersion: string;
aresVersion: string;
httpParserVersion: string;
swiftSearchVersion: string;
swiftSearchSupportedVersion: string;
clientVersion: string;
buildNumber: string;
sfeVersion: string;
sfeClientType: string;
sdaVersion: string;
sdaBuildNumber: string;
electronVersion: string;
chromeVersion: string;
v8Version: string;
nodeVersion: string;
openSslVersion: string;
zlibVersion: string;
uvVersion: string;
aresVersion: string;
httpParserVersion: string;
swiftSearchVersion: string;
swiftSearchSupportedVersion: string;
}
class VersionHandler {
public versionInfo: IVersionInfo;
private serverVersionInfo: any;
private mainUrl;
private sfeVersionInfo: any;
public versionInfo: IVersionInfo;
private serverVersionInfo: any;
private mainUrl;
private sfeVersionInfo: any;
constructor() {
this.versionInfo = {
clientVersion,
buildNumber,
sfeVersion,
sfeClientType,
sdaVersion: version,
sdaBuildNumber: buildNumber,
electronVersion: process.versions.electron,
chromeVersion: process.versions.chrome,
v8Version: process.versions.v8,
nodeVersion: process.versions.node,
openSslVersion: process.versions.openssl,
zlibVersion: process.versions.zlib,
uvVersion: process.versions.uv,
aresVersion: process.versions.ares,
httpParserVersion: process.versions.http_parser,
swiftSearchVersion: optionalDependencies['swift-search'],
swiftSearchSupportedVersion: searchAPIVersion,
};
this.mainUrl = null;
}
constructor() {
this.versionInfo = {
clientVersion,
buildNumber,
sfeVersion,
sfeClientType,
sdaVersion: version,
sdaBuildNumber: buildNumber,
electronVersion: process.versions.electron,
chromeVersion: process.versions.chrome,
v8Version: process.versions.v8,
nodeVersion: process.versions.node,
openSslVersion: process.versions.openssl,
zlibVersion: process.versions.zlib,
uvVersion: process.versions.uv,
aresVersion: process.versions.ares,
httpParserVersion: process.versions.http_parser,
swiftSearchVersion: optionalDependencies['swift-search'],
swiftSearchSupportedVersion: searchAPIVersion,
};
this.mainUrl = null;
}
/**
* Get Symphony version from the pod
*/
public getClientVersion(
fetchFromServer: boolean = false,
mainUrl?: string,
): Promise<IVersionInfo> {
return new Promise((resolve) => {
if (this.serverVersionInfo && !fetchFromServer) {
this.versionInfo.clientVersion =
this.serverVersionInfo['Implementation-Version'] ||
this.versionInfo.clientVersion;
this.versionInfo.buildNumber =
this.serverVersionInfo['Implementation-Build'] ||
this.versionInfo.buildNumber;
resolve(this.versionInfo);
return;
}
/**
* Get Symphony version from the pod
*/
public getClientVersion(fetchFromServer: boolean = false, mainUrl?: string): Promise<IVersionInfo> {
return new Promise((resolve) => {
if (mainUrl) {
this.mainUrl = mainUrl;
}
if (this.serverVersionInfo && !fetchFromServer) {
this.versionInfo.clientVersion = this.serverVersionInfo['Implementation-Version'] || this.versionInfo.clientVersion;
this.versionInfo.buildNumber = this.serverVersionInfo['Implementation-Build'] || this.versionInfo.buildNumber;
resolve(this.versionInfo);
return;
}
const { url: podUrl }: IGlobalConfig = config.getGlobalConfigFields([
'url',
]);
if (mainUrl) {
this.mainUrl = mainUrl;
}
if (!this.mainUrl || !nodeURL.parse(this.mainUrl)) {
this.mainUrl = podUrl;
}
const { url: podUrl }: IGlobalConfig = config.getGlobalConfigFields(['url']);
if (!this.mainUrl) {
logger.error(
`version-handler: Unable to get pod url for getting version data from server! Setting defaults!`,
);
logger.info(
`version-handler: Setting defaults -> ${JSON.stringify(
this.versionInfo,
)}`,
);
resolve(this.versionInfo);
return;
}
if (!this.mainUrl || !nodeURL.parse(this.mainUrl)) {
this.mainUrl = podUrl;
}
const hostname = nodeURL.parse(this.mainUrl).hostname;
const protocol = nodeURL.parse(this.mainUrl).protocol;
if (!this.mainUrl) {
logger.error(`version-handler: Unable to get pod url for getting version data from server! Setting defaults!`);
logger.info(`version-handler: Setting defaults -> ${JSON.stringify(this.versionInfo)}`);
resolve(this.versionInfo);
return;
}
if (protocol && protocol.startsWith('file')) {
return;
}
const hostname = nodeURL.parse(this.mainUrl).hostname;
const protocol = nodeURL.parse(this.mainUrl).protocol;
const versionApiPath = '/webcontroller/HealthCheck/version/advanced';
if (protocol && protocol.startsWith('file')) {
return;
}
const url = `${protocol}//${hostname}${versionApiPath}`;
logger.info(
`version-handler: Trying to get version info for the URL: ${url}`,
);
const versionApiPath = '/webcontroller/HealthCheck/version/advanced';
const url = `${protocol}//${hostname}${versionApiPath}`;
logger.info(`version-handler: Trying to get version info for the URL: ${url}`);
const request = net.request(url);
request.on('response', (res) => {
let body: string = '';
res.on('data', (d: Buffer) => {
body += d;
});
res.on('end', () => {
try {
this.serverVersionInfo = JSON.parse(body)[0];
this.versionInfo.clientVersion = this.serverVersionInfo['Implementation-Version'] || this.versionInfo.clientVersion;
this.versionInfo.buildNumber = this.serverVersionInfo['Implementation-Build'] || this.versionInfo.buildNumber;
logger.info(`version-handler: Updated version info from server! ${JSON.stringify(this.versionInfo)}`);
resolve(this.versionInfo);
} catch (error) {
logger.error(`version-handler: Error getting version data from the server! ${error}`);
resolve(this.versionInfo);
return;
}
});
res.on('error', (error: Error) => {
logger.error(`version-handler: Error getting version data from the server! ${error}`);
resolve(this.versionInfo);
return;
});
});
request.on('error', (error: Error) => {
logger.error(`version-handler: Error getting version data from the server! ${error}`);
resolve(this.versionInfo);
return;
});
request.on('close', () => {
logger.info(`version-handler: Request closed!!`);
});
request.on('finish', () => {
logger.info(`version-handler: Request finished!!`);
});
request.end();
logger.info('version-handler: mainUrl: ' + mainUrl);
logger.info('version-handler: hostname: ' + hostname);
/* Get SFE version */
let urlSfeVersion;
if (mainUrl?.includes('/client-bff/')) {
if (mainUrl?.includes('/client-bff/daily/')) {
urlSfeVersion = `${protocol}//${hostname}/client-bff/daily/version.json`;
} else {
urlSfeVersion = `${protocol}//${hostname}/client-bff/version.json`;
}
this.versionInfo.sfeClientType = '2.0';
} else {
urlSfeVersion = `${protocol}//${hostname}/client/version.json`;
this.versionInfo.sfeClientType = '1.5';
}
logger.info(`version-handler: Trying to get SFE version info for the URL: ${urlSfeVersion}`);
const requestSfeVersion = net.request(urlSfeVersion);
requestSfeVersion.on('response', (res) => {
let body: string = '';
res.on('data', (d: Buffer) => {
body += d;
});
res.on('end', () => {
try {
this.sfeVersionInfo = JSON.parse(body);
const key = 'version';
this.versionInfo.sfeVersion = this.sfeVersionInfo[key];
logger.info('version-handler: SFE-version: ' + this.sfeVersionInfo[key]);
logger.info(`version-handler: Updated SFE version info from server! ${JSON.stringify(this.versionInfo, null, 3)}`);
resolve(this.versionInfo);
} catch (error) {
logger.error(`version-handler: Error getting SFE version data from the server! ${error}`);
resolve(this.versionInfo);
return;
}
});
res.on('error', (error: Error) => {
logger.error(`version-handler: Error getting SFE version data from the server! ${error}`);
resolve(this.versionInfo);
return;
});
});
requestSfeVersion.on('error', (error: Error) => {
logger.error(`version-handler: Error getting SFE version data from the server! ${error}`);
resolve(this.versionInfo);
return;
});
requestSfeVersion.on('close', () => {
logger.info(`version-handler: Request closed!!`);
});
requestSfeVersion.on('finish', () => {
logger.info(`version-handler: Request finished!!`);
});
requestSfeVersion.end();
const request = net.request(url);
request.on('response', (res) => {
let body: string = '';
res.on('data', (d: Buffer) => {
body += d;
});
}
res.on('end', () => {
try {
this.serverVersionInfo = JSON.parse(body)[0];
this.versionInfo.clientVersion =
this.serverVersionInfo['Implementation-Version'] ||
this.versionInfo.clientVersion;
this.versionInfo.buildNumber =
this.serverVersionInfo['Implementation-Build'] ||
this.versionInfo.buildNumber;
logger.info(
`version-handler: Updated version info from server! ${JSON.stringify(
this.versionInfo,
)}`,
);
resolve(this.versionInfo);
} catch (error) {
logger.error(
`version-handler: Error getting version data from the server! ${error}`,
);
resolve(this.versionInfo);
return;
}
});
res.on('error', (error: Error) => {
logger.error(
`version-handler: Error getting version data from the server! ${error}`,
);
resolve(this.versionInfo);
return;
});
});
request.on('error', (error: Error) => {
logger.error(
`version-handler: Error getting version data from the server! ${error}`,
);
resolve(this.versionInfo);
return;
});
request.on('close', () => {
logger.info(`version-handler: Request closed!!`);
});
request.on('finish', () => {
logger.info(`version-handler: Request finished!!`);
});
request.end();
logger.info('version-handler: mainUrl: ' + mainUrl);
logger.info('version-handler: hostname: ' + hostname);
/* Get SFE version */
let urlSfeVersion: string;
if (mainUrl?.includes('/client-bff/')) {
urlSfeVersion = mainUrl?.includes('/client-bff/daily/')
? `${protocol}//${hostname}/client-bff/daily/version.json`
: `${protocol}//${hostname}/client-bff/version.json`;
this.versionInfo.sfeClientType = '2.0';
} else {
urlSfeVersion = `${protocol}//${hostname}/client/version.json`;
this.versionInfo.sfeClientType = '1.5';
}
logger.info(
`version-handler: Trying to get SFE version info for the URL: ${urlSfeVersion}`,
);
const requestSfeVersion = net.request(urlSfeVersion);
requestSfeVersion.on('response', (res) => {
let body: string = '';
res.on('data', (d: Buffer) => {
body += d;
});
res.on('end', () => {
try {
this.sfeVersionInfo = JSON.parse(body);
const key = 'version';
this.versionInfo.sfeVersion = this.sfeVersionInfo[key];
logger.info(
'version-handler: SFE-version: ' + this.sfeVersionInfo[key],
);
logger.info(
`version-handler: Updated SFE version info from server! ${JSON.stringify(
this.versionInfo,
null,
3,
)}`,
);
resolve(this.versionInfo);
} catch (error) {
logger.error(
`version-handler: Error getting SFE version data from the server! ${error}`,
);
resolve(this.versionInfo);
return;
}
});
res.on('error', (error: Error) => {
logger.error(
`version-handler: Error getting SFE version data from the server! ${error}`,
);
resolve(this.versionInfo);
return;
});
});
requestSfeVersion.on('error', (error: Error) => {
logger.error(
`version-handler: Error getting SFE version data from the server! ${error}`,
);
resolve(this.versionInfo);
return;
});
requestSfeVersion.on('close', () => {
logger.info(`version-handler: Request closed!!`);
});
requestSfeVersion.on('finish', () => {
logger.info(`version-handler: Request finished!!`);
});
requestSfeVersion.end();
});
}
}
const versionHandler = new VersionHandler();

View File

@ -1,4 +1,9 @@
import { BrowserWindow, dialog, PermissionRequestHandlerHandlerDetails, systemPreferences } from 'electron';
import {
BrowserWindow,
dialog,
PermissionRequestHandlerHandlerDetails,
systemPreferences,
} from 'electron';
import { apiName, IBoundsChange, KeyCodes } from '../common/api-interface';
import { isLinux, isMac, isWindowsOS } from '../common/env';
@ -11,72 +16,95 @@ import { ICustomBrowserWindow, windowHandler } from './window-handler';
import { showPopupMenu, windowExists } from './window-utils';
enum Permissions {
MEDIA = 'media',
LOCATION = 'geolocation',
NOTIFICATIONS = 'notifications',
MIDI_SYSEX = 'midiSysex',
POINTER_LOCK = 'pointerLock',
FULL_SCREEN = 'fullscreen',
OPEN_EXTERNAL = 'openExternal',
MEDIA = 'media',
LOCATION = 'geolocation',
NOTIFICATIONS = 'notifications',
MIDI_SYSEX = 'midiSysex',
POINTER_LOCK = 'pointerLock',
FULL_SCREEN = 'fullscreen',
OPEN_EXTERNAL = 'openExternal',
}
const PERMISSIONS_NAMESPACE = 'Permissions';
const saveWindowSettings = async (): Promise<void> => {
const browserWindow = BrowserWindow.getFocusedWindow() as ICustomBrowserWindow;
const mainWindow = windowHandler.getMainWindow();
const browserWindow = BrowserWindow.getFocusedWindow() as ICustomBrowserWindow;
const mainWindow = windowHandler.getMainWindow();
if (browserWindow && windowExists(browserWindow)) {
let [ x, y ] = browserWindow.getPosition();
let [ width, height ] = browserWindow.getSize();
if (x && y && width && height) {
// Only send bound changes over to client for pop-out windows
if (browserWindow.winName !== apiName.mainWindowName && mainWindow && windowExists(mainWindow)) {
mainWindow.webContents.send('boundsChange', { x, y, width, height, windowName: browserWindow.winName } as IBoundsChange);
}
if (browserWindow && windowExists(browserWindow)) {
let [x, y] = browserWindow.getPosition();
let [width, height] = browserWindow.getSize();
if (x && y && width && height) {
// Only send bound changes over to client for pop-out windows
if (
browserWindow.winName !== apiName.mainWindowName &&
mainWindow &&
windowExists(mainWindow)
) {
const boundsChange: IBoundsChange = {
x,
y,
width,
height,
windowName: browserWindow.winName,
};
mainWindow.webContents.send('boundsChange', boundsChange);
}
// Update the config file
if (browserWindow.winName === apiName.mainWindowName) {
const isMaximized = browserWindow.isMaximized();
const isFullScreen = browserWindow.isFullScreen();
const { mainWinPos } = config.getUserConfigFields([ 'mainWinPos' ]);
// Update the config file
if (browserWindow.winName === apiName.mainWindowName) {
const isMaximized = browserWindow.isMaximized();
const isFullScreen = browserWindow.isFullScreen();
const { mainWinPos } = config.getUserConfigFields(['mainWinPos']);
if (isMaximized || isFullScreen) {
// Keep the original size and position when window is maximized or full screen
if (mainWinPos !== undefined && mainWinPos.x !== undefined && mainWinPos.y !== undefined && mainWinPos.width !== undefined && mainWinPos.height !== undefined) {
x = mainWinPos.x;
y = mainWinPos.y;
width = mainWinPos.width;
height = mainWinPos.height;
}
}
await config.updateUserConfig({ mainWinPos: { ...mainWinPos, ...{ height, width, x, y, isMaximized, isFullScreen } } });
}
if (isMaximized || isFullScreen) {
// Keep the original size and position when window is maximized or full screen
if (
mainWinPos !== undefined &&
mainWinPos.x !== undefined &&
mainWinPos.y !== undefined &&
mainWinPos.width !== undefined &&
mainWinPos.height !== undefined
) {
x = mainWinPos.x;
y = mainWinPos.y;
width = mainWinPos.width;
height = mainWinPos.height;
}
}
}
await config.updateUserConfig({
mainWinPos: {
...mainWinPos,
...{ height, width, x, y, isMaximized, isFullScreen },
},
});
}
}
}
};
const windowMaximized = async (): Promise<void> => {
const browserWindow = BrowserWindow.getFocusedWindow() as ICustomBrowserWindow;
if (browserWindow && windowExists(browserWindow)) {
const isMaximized = browserWindow.isMaximized();
const isFullScreen = browserWindow.isFullScreen();
if (browserWindow.winName === apiName.mainWindowName) {
const { mainWinPos } = config.getUserConfigFields([ 'mainWinPos' ]);
await config.updateUserConfig({ mainWinPos: { ...mainWinPos, ...{ isMaximized, isFullScreen } } });
}
const browserWindow = BrowserWindow.getFocusedWindow() as ICustomBrowserWindow;
if (browserWindow && windowExists(browserWindow)) {
const isMaximized = browserWindow.isMaximized();
const isFullScreen = browserWindow.isFullScreen();
if (browserWindow.winName === apiName.mainWindowName) {
const { mainWinPos } = config.getUserConfigFields(['mainWinPos']);
await config.updateUserConfig({
mainWinPos: { ...mainWinPos, ...{ isMaximized, isFullScreen } },
});
}
}
};
const throttledWindowChanges = throttle(async () => {
await saveWindowSettings();
await windowMaximized();
notification.moveNotificationToTop();
await saveWindowSettings();
await windowMaximized();
notification.moveNotificationToTop();
}, 1000);
const throttledWindowRestore = throttle(async () => {
notification.moveNotificationToTop();
notification.moveNotificationToTop();
}, 1000);
/**
@ -85,20 +113,34 @@ const throttledWindowRestore = throttle(async () => {
* @param childWindow {BrowserWindow} - window created via new-window event
*/
export const sendInitialBoundChanges = (childWindow: BrowserWindow): void => {
logger.info(`window-actions: Sending initial bounds`);
const mainWindow = windowHandler.getMainWindow();
if (!mainWindow || !windowExists(mainWindow)) {
return;
}
logger.info(`window-actions: Sending initial bounds`);
const mainWindow = windowHandler.getMainWindow();
if (!mainWindow || !windowExists(mainWindow)) {
return;
}
if (!childWindow || !windowExists(childWindow)) {
logger.error(`window-actions: child window has already been destroyed - not sending bound change`);
return;
}
const { x, y, width, height } = childWindow.getBounds();
const windowName = (childWindow as ICustomBrowserWindow).winName;
mainWindow.webContents.send('boundsChange', { x, y, width, height, windowName } as IBoundsChange);
logger.info(`window-actions: Initial bounds sent for ${(childWindow as ICustomBrowserWindow).winName}`, { x, y, width, height });
if (!childWindow || !windowExists(childWindow)) {
logger.error(
`window-actions: child window has already been destroyed - not sending bound change`,
);
return;
}
const { x, y, width, height } = childWindow.getBounds();
const windowName = (childWindow as ICustomBrowserWindow).winName;
const boundsChange: IBoundsChange = {
x,
y,
width,
height,
windowName,
};
mainWindow.webContents.send('boundsChange', boundsChange);
logger.info(
`window-actions: Initial bounds sent for ${
(childWindow as ICustomBrowserWindow).winName
}`,
{ x, y, width, height },
);
};
/**
@ -110,35 +152,38 @@ export const sendInitialBoundChanges = (childWindow: BrowserWindow): void => {
* @param {Boolean} shouldFocus whether to get window to focus or just show
* without giving focus
*/
export const activate = (windowName: string, shouldFocus: boolean = true): void => {
export const activate = (
windowName: string,
shouldFocus: boolean = true,
): void => {
// Electron-136: don't activate when the app is reloaded programmatically
if (windowHandler.isAutoReload) {
return;
}
// Electron-136: don't activate when the app is reloaded programmatically
if (windowHandler.isAutoReload) {
return;
}
const windows = windowHandler.getAllWindows();
for (const key in windows) {
if (Object.prototype.hasOwnProperty.call(windows, key)) {
const window = windows[ key ];
if (window && !window.isDestroyed() && window.winName === windowName) {
// Bring the window to the top without focusing
// Flash task bar icon in Windows for windows
if (!shouldFocus) {
return (isMac || isLinux) ? window.showInactive() : window.flashFrame(true);
}
// Note: On window just focusing will preserve window snapped state
// Hiding the window and just calling the focus() won't display the window
if (isWindowsOS) {
return window.isMinimized() ? window.restore() : window.focus();
}
return window.isMinimized() ? window.restore() : window.show();
}
const windows = windowHandler.getAllWindows();
for (const key in windows) {
if (Object.prototype.hasOwnProperty.call(windows, key)) {
const window = windows[key];
if (window && !window.isDestroyed() && window.winName === windowName) {
// Bring the window to the top without focusing
// Flash task bar icon in Windows for windows
if (!shouldFocus) {
return isMac || isLinux
? window.showInactive()
: window.flashFrame(true);
}
// Note: On window just focusing will preserve window snapped state
// Hiding the window and just calling the focus() won't display the window
if (isWindowsOS) {
return window.isMinimized() ? window.restore() : window.focus();
}
return window.isMinimized() ? window.restore() : window.show();
}
}
}
};
/**
@ -149,29 +194,35 @@ export const activate = (windowName: string, shouldFocus: boolean = true): void
* @param shouldUpdateUserConfig {boolean} - whether to update config file
*/
export const updateAlwaysOnTop = async (
shouldSetAlwaysOnTop: boolean,
shouldActivateMainWindow: boolean = true,
shouldUpdateUserConfig: boolean = true,
shouldSetAlwaysOnTop: boolean,
shouldActivateMainWindow: boolean = true,
shouldUpdateUserConfig: boolean = true,
): Promise<void> => {
logger.info(`window-actions: Should we set always on top? ${shouldSetAlwaysOnTop}!`);
const browserWins: ICustomBrowserWindow[] = BrowserWindow.getAllWindows() as ICustomBrowserWindow[];
if (shouldUpdateUserConfig) {
await config.updateUserConfig({ alwaysOnTop: shouldSetAlwaysOnTop ? CloudConfigDataTypes.ENABLED : CloudConfigDataTypes.NOT_SET });
}
if (browserWins.length > 0) {
browserWins
.filter((browser) => typeof browser.notificationData !== 'object')
.forEach((browser) => browser.setAlwaysOnTop(shouldSetAlwaysOnTop));
logger.info(
`window-actions: Should we set always on top? ${shouldSetAlwaysOnTop}!`,
);
const browserWins: ICustomBrowserWindow[] = BrowserWindow.getAllWindows() as ICustomBrowserWindow[];
if (shouldUpdateUserConfig) {
await config.updateUserConfig({
alwaysOnTop: shouldSetAlwaysOnTop
? CloudConfigDataTypes.ENABLED
: CloudConfigDataTypes.NOT_SET,
});
}
if (browserWins.length > 0) {
browserWins
.filter((browser) => typeof browser.notificationData !== 'object')
.forEach((browser) => browser.setAlwaysOnTop(shouldSetAlwaysOnTop));
// An issue where changing the alwaysOnTop property
// focus the pop-out window
// Issue - Electron-209/470
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && mainWindow.winName && shouldActivateMainWindow) {
activate(mainWindow.winName);
logger.info(`window-actions: activated main window!`);
}
// An issue where changing the alwaysOnTop property
// focus the pop-out window
// Issue - Electron-209/470
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && mainWindow.winName && shouldActivateMainWindow) {
activate(mainWindow.winName);
logger.info(`window-actions: activated main window!`);
}
}
};
/**
@ -180,29 +231,37 @@ export const updateAlwaysOnTop = async (
* @param key {number}
*/
export const handleKeyPress = (key: number): void => {
switch (key) {
case KeyCodes.Esc: {
const focusedWindow = BrowserWindow.getFocusedWindow();
switch (key) {
case KeyCodes.Esc: {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow && !focusedWindow.isDestroyed() && focusedWindow.isFullScreen()) {
logger.info(`window-actions: exiting fullscreen by esc key action`);
focusedWindow.setFullScreen(false);
}
break;
}
case KeyCodes.Alt:
if (isMac || isLinux || windowHandler.isCustomTitleBar) {
return;
}
const browserWin = BrowserWindow.getFocusedWindow() as ICustomBrowserWindow;
if (browserWin && windowExists(browserWin) && browserWin.winName === apiName.mainWindowName) {
logger.info(`window-actions: popping up menu by alt key action`);
showPopupMenu({ window: browserWin });
}
break;
default:
break;
if (
focusedWindow &&
!focusedWindow.isDestroyed() &&
focusedWindow.isFullScreen()
) {
logger.info(`window-actions: exiting fullscreen by esc key action`);
focusedWindow.setFullScreen(false);
}
break;
}
case KeyCodes.Alt:
if (isMac || isLinux || windowHandler.isCustomTitleBar) {
return;
}
const browserWin = BrowserWindow.getFocusedWindow() as ICustomBrowserWindow;
if (
browserWin &&
windowExists(browserWin) &&
browserWin.winName === apiName.mainWindowName
) {
logger.info(`window-actions: popping up menu by alt key action`);
showPopupMenu({ window: browserWin });
}
break;
default:
break;
}
};
/**
@ -210,11 +269,19 @@ export const handleKeyPress = (key: number): void => {
* on fullscreen state
*/
const setSpecificAlwaysOnTop = () => {
const browserWindow = BrowserWindow.getFocusedWindow();
if (isMac && browserWindow && windowExists(browserWindow) && browserWindow.isAlwaysOnTop()) {
// Set the focused window's always on top level based on fullscreen state
browserWindow.setAlwaysOnTop(true, browserWindow.isFullScreen() ? 'modal-panel' : 'floating');
}
const browserWindow = BrowserWindow.getFocusedWindow();
if (
isMac &&
browserWindow &&
windowExists(browserWindow) &&
browserWindow.isAlwaysOnTop()
) {
// Set the focused window's always on top level based on fullscreen state
browserWindow.setAlwaysOnTop(
true,
browserWindow.isFullScreen() ? 'modal-panel' : 'floating',
);
}
};
/**
@ -223,36 +290,38 @@ const setSpecificAlwaysOnTop = () => {
* @param window {BrowserWindow}
*/
export const monitorWindowActions = (window: BrowserWindow): void => {
if (windowHandler.shouldShowWelcomeScreen) {
logger.info(`Not saving window position as we are showing the welcome window!`);
return;
if (windowHandler.shouldShowWelcomeScreen) {
logger.info(
`Not saving window position as we are showing the welcome window!`,
);
return;
}
if (!window || window.isDestroyed()) {
return;
}
const eventNames = ['move', 'resize'];
eventNames.forEach((event: string) => {
if (window) {
// @ts-ignore
window.on(event, throttledWindowChanges);
}
if (!window || window.isDestroyed()) {
return;
}
const eventNames = [ 'move', 'resize' ];
eventNames.forEach((event: string) => {
if (window) {
// @ts-ignore
window.on(event, throttledWindowChanges);
}
});
window.on('enter-full-screen', throttledWindowChanges);
window.on('maximize', throttledWindowChanges);
});
window.on('enter-full-screen', throttledWindowChanges);
window.on('maximize', throttledWindowChanges);
window.on('leave-full-screen', throttledWindowChanges);
window.on('unmaximize', throttledWindowChanges);
window.on('leave-full-screen', throttledWindowChanges);
window.on('unmaximize', throttledWindowChanges);
if ((window as ICustomBrowserWindow).winName === apiName.mainWindowName) {
window.on('restore', throttledWindowRestore);
}
if ((window as ICustomBrowserWindow).winName === apiName.mainWindowName) {
window.on('restore', throttledWindowRestore);
}
// Workaround for an issue with MacOS + AlwaysOnTop
// Issue: SDA-1665
if (isMac) {
window.on('enter-full-screen', setSpecificAlwaysOnTop);
window.on('leave-full-screen', setSpecificAlwaysOnTop);
}
// Workaround for an issue with MacOS + AlwaysOnTop
// Issue: SDA-1665
if (isMac) {
window.on('enter-full-screen', setSpecificAlwaysOnTop);
window.on('leave-full-screen', setSpecificAlwaysOnTop);
}
};
/**
@ -261,28 +330,28 @@ export const monitorWindowActions = (window: BrowserWindow): void => {
* @param window
*/
export const removeWindowEventListener = (window: BrowserWindow): void => {
if (!window || window.isDestroyed()) {
return;
if (!window || window.isDestroyed()) {
return;
}
const eventNames = ['move', 'resize'];
eventNames.forEach((event: string) => {
if (window) {
// @ts-ignore
window.removeListener(event, throttledWindowChanges);
}
const eventNames = [ 'move', 'resize' ];
eventNames.forEach((event: string) => {
if (window) {
// @ts-ignore
window.removeListener(event, throttledWindowChanges);
}
});
window.removeListener('enter-full-screen', throttledWindowChanges);
window.removeListener('maximize', throttledWindowChanges);
});
window.removeListener('enter-full-screen', throttledWindowChanges);
window.removeListener('maximize', throttledWindowChanges);
window.removeListener('leave-full-screen', throttledWindowChanges);
window.removeListener('unmaximize', throttledWindowChanges);
window.removeListener('leave-full-screen', throttledWindowChanges);
window.removeListener('unmaximize', throttledWindowChanges);
// Workaround for and issue with MacOS + AlwaysOnTop
// Issue: SDA-1665
if (isMac) {
window.removeListener('enter-full-screen', setSpecificAlwaysOnTop);
window.removeListener('leave-full-screen', setSpecificAlwaysOnTop);
}
// Workaround for and issue with MacOS + AlwaysOnTop
// Issue: SDA-1665
if (isMac) {
window.removeListener('enter-full-screen', setSpecificAlwaysOnTop);
window.removeListener('leave-full-screen', setSpecificAlwaysOnTop);
}
};
/**
@ -293,18 +362,29 @@ export const removeWindowEventListener = (window: BrowserWindow): void => {
* @param message {string} - custom message displayed to the user
* @param callback {function}
*/
export const handleSessionPermissions = async (permission: boolean, message: string, callback: (permission: boolean) => void): Promise<void> => {
logger.info(`window-action: permission is ->`, { type: message, permission });
export const handleSessionPermissions = async (
permission: boolean,
message: string,
callback: (permission: boolean) => void,
): Promise<void> => {
logger.info(`window-action: permission is ->`, { type: message, permission });
if (!permission) {
const browserWindow = BrowserWindow.getFocusedWindow();
if (browserWindow && !browserWindow.isDestroyed()) {
const response = await dialog.showMessageBox(browserWindow, { type: 'error', title: `${i18n.t('Permission Denied')()}!`, message });
logger.error(`window-actions: permissions message box closed with response`, response);
}
if (!permission) {
const browserWindow = BrowserWindow.getFocusedWindow();
if (browserWindow && !browserWindow.isDestroyed()) {
const response = await dialog.showMessageBox(browserWindow, {
type: 'error',
title: `${i18n.t('Permission Denied')()}!`,
message,
});
logger.error(
`window-actions: permissions message box closed with response`,
response,
);
}
}
return callback(permission);
return callback(permission);
};
/**
@ -318,36 +398,50 @@ export const handleSessionPermissions = async (permission: boolean, message: str
* @param callback {function}
* @param details {PermissionRequestHandlerHandlerDetails} - object passed along with certain permission types. see {@link https://www.electronjs.org/docs/api/session#sessetpermissionrequesthandlerhandler}
*/
const handleMediaPermissions = async (permission: boolean, message: string, callback: (permission: boolean) => void, details: PermissionRequestHandlerHandlerDetails): Promise<void> => {
logger.info(`window-action: permission is ->`, { type: message, permission });
let systemAudioPermission;
let systemVideoPermission;
if (isMac) {
systemAudioPermission = await systemPreferences.askForMediaAccess('microphone');
systemVideoPermission = await systemPreferences.askForMediaAccess('camera');
} else {
systemAudioPermission = true;
systemVideoPermission = true;
}
const handleMediaPermissions = async (
permission: boolean,
message: string,
callback: (permission: boolean) => void,
details: PermissionRequestHandlerHandlerDetails,
): Promise<void> => {
logger.info(`window-action: permission is ->`, { type: message, permission });
let systemAudioPermission;
let systemVideoPermission;
if (isMac) {
systemAudioPermission = await systemPreferences.askForMediaAccess(
'microphone',
);
systemVideoPermission = await systemPreferences.askForMediaAccess('camera');
} else {
systemAudioPermission = true;
systemVideoPermission = true;
}
if (!permission) {
const browserWindow = BrowserWindow.getFocusedWindow();
if (browserWindow && !browserWindow.isDestroyed()) {
const response = await dialog.showMessageBox(browserWindow, { type: 'error', title: `${i18n.t('Permission Denied')()}!`, message });
logger.error(`window-actions: permissions message box closed with response`, response);
}
if (!permission) {
const browserWindow = BrowserWindow.getFocusedWindow();
if (browserWindow && !browserWindow.isDestroyed()) {
const response = await dialog.showMessageBox(browserWindow, {
type: 'error',
title: `${i18n.t('Permission Denied')()}!`,
message,
});
logger.error(
`window-actions: permissions message box closed with response`,
response,
);
}
}
if (details.mediaTypes && isMac) {
if (details.mediaTypes.includes('audio') && !systemAudioPermission) {
return callback(false);
}
if (details.mediaTypes.includes('video') && !systemVideoPermission) {
return callback(false);
}
if (details.mediaTypes && isMac) {
if (details.mediaTypes.includes('audio') && !systemAudioPermission) {
return callback(false);
}
if (details.mediaTypes.includes('video') && !systemVideoPermission) {
return callback(false);
}
}
return callback(permission);
return callback(permission);
};
/**
@ -355,39 +449,94 @@ const handleMediaPermissions = async (permission: boolean, message: string, call
*
* @param webContents {Electron.webContents}
*/
export const handlePermissionRequests = (webContents: Electron.webContents): void => {
export const handlePermissionRequests = (
webContents: Electron.webContents,
): void => {
if (!webContents || !webContents.session) {
return;
}
const { session } = webContents;
if (!webContents || !webContents.session) {
return;
}
const { session } = webContents;
const { permissions } = config.getConfigFields(['permissions']);
if (!permissions) {
logger.error(
'permissions configuration is invalid, so, everything will be true by default!',
);
return;
}
const { permissions } = config.getConfigFields([ 'permissions' ]);
if (!permissions) {
logger.error('permissions configuration is invalid, so, everything will be true by default!');
return;
}
session.setPermissionRequestHandler((_webContents, permission, callback, details) => {
switch (permission) {
case Permissions.MEDIA:
return handleMediaPermissions(permissions.media, i18n.t('Your administrator has disabled sharing your camera, microphone, and speakers. Please contact your admin for help', PERMISSIONS_NAMESPACE)(), callback, details);
case Permissions.LOCATION:
return handleSessionPermissions(permissions.geolocation, i18n.t('Your administrator has disabled sharing your location. Please contact your admin for help', PERMISSIONS_NAMESPACE)(), callback);
case Permissions.NOTIFICATIONS:
return handleSessionPermissions(permissions.notifications, i18n.t('Your administrator has disabled notifications. Please contact your admin for help', PERMISSIONS_NAMESPACE)(), callback);
case Permissions.MIDI_SYSEX:
return handleSessionPermissions(permissions.midiSysex, i18n.t('Your administrator has disabled MIDI Sysex. Please contact your admin for help', PERMISSIONS_NAMESPACE)(), callback);
case Permissions.POINTER_LOCK:
return handleSessionPermissions(permissions.pointerLock, i18n.t('Your administrator has disabled Pointer Lock. Please contact your admin for help', PERMISSIONS_NAMESPACE)(), callback);
case Permissions.FULL_SCREEN:
return handleSessionPermissions(permissions.fullscreen, i18n.t('Your administrator has disabled Full Screen. Please contact your admin for help', PERMISSIONS_NAMESPACE)(), callback);
case Permissions.OPEN_EXTERNAL:
return handleSessionPermissions(permissions.openExternal, i18n.t('Your administrator has disabled Opening External App. Please contact your admin for help', PERMISSIONS_NAMESPACE)(), callback);
default:
return callback(false);
}
});
session.setPermissionRequestHandler(
(_webContents, permission, callback, details) => {
switch (permission) {
case Permissions.MEDIA:
return handleMediaPermissions(
permissions.media,
i18n.t(
'Your administrator has disabled sharing your camera, microphone, and speakers. Please contact your admin for help',
PERMISSIONS_NAMESPACE,
)(),
callback,
details,
);
case Permissions.LOCATION:
return handleSessionPermissions(
permissions.geolocation,
i18n.t(
'Your administrator has disabled sharing your location. Please contact your admin for help',
PERMISSIONS_NAMESPACE,
)(),
callback,
);
case Permissions.NOTIFICATIONS:
return handleSessionPermissions(
permissions.notifications,
i18n.t(
'Your administrator has disabled notifications. Please contact your admin for help',
PERMISSIONS_NAMESPACE,
)(),
callback,
);
case Permissions.MIDI_SYSEX:
return handleSessionPermissions(
permissions.midiSysex,
i18n.t(
'Your administrator has disabled MIDI Sysex. Please contact your admin for help',
PERMISSIONS_NAMESPACE,
)(),
callback,
);
case Permissions.POINTER_LOCK:
return handleSessionPermissions(
permissions.pointerLock,
i18n.t(
'Your administrator has disabled Pointer Lock. Please contact your admin for help',
PERMISSIONS_NAMESPACE,
)(),
callback,
);
case Permissions.FULL_SCREEN:
return handleSessionPermissions(
permissions.fullscreen,
i18n.t(
'Your administrator has disabled Full Screen. Please contact your admin for help',
PERMISSIONS_NAMESPACE,
)(),
callback,
);
case Permissions.OPEN_EXTERNAL:
return handleSessionPermissions(
permissions.openExternal,
i18n.t(
'Your administrator has disabled Opening External App. Please contact your admin for help',
PERMISSIONS_NAMESPACE,
)(),
callback,
);
default:
return callback(false);
}
},
);
};
/**
@ -399,41 +548,44 @@ export const handlePermissionRequests = (webContents: Electron.webContents): voi
* @param _sourceId
*/
export const onConsoleMessages = (_event, level, message, _line, _sourceId) => {
if (level === 0) {
logger.log('error', `renderer: ${message}`, [], false);
} else if (level === 1) {
logger.log('info', `renderer: ${message}`, [], false);
} else if (level === 2) {
logger.log('warn', `renderer: ${message}`, [], false);
} else if (level === 3) {
logger.log('error', `renderer: ${message}`, [], false);
} else {
logger.log('info', `renderer: ${message}`, [], false);
}
if (level === 0) {
logger.log('error', `renderer: ${message}`, [], false);
} else if (level === 1) {
logger.log('info', `renderer: ${message}`, [], false);
} else if (level === 2) {
logger.log('warn', `renderer: ${message}`, [], false);
} else if (level === 3) {
logger.log('error', `renderer: ${message}`, [], false);
} else {
logger.log('info', `renderer: ${message}`, [], false);
}
};
/**
* Unregisters renderer logs from all the available browser window
*/
export const unregisterConsoleMessages = () => {
const browserWindows = BrowserWindow.getAllWindows();
for (const browserWindow of browserWindows) {
if (!browserWindow || !windowExists(browserWindow)) {
return;
}
browserWindow.webContents.removeListener('console-message', onConsoleMessages);
const browserWindows = BrowserWindow.getAllWindows();
for (const browserWindow of browserWindows) {
if (!browserWindow || !windowExists(browserWindow)) {
return;
}
browserWindow.webContents.removeListener(
'console-message',
onConsoleMessages,
);
}
};
/**
* registers renderer logs from all the available browser window
*/
export const registerConsoleMessages = () => {
const browserWindows = BrowserWindow.getAllWindows();
for (const browserWindow of browserWindows) {
if (!browserWindow || !windowExists(browserWindow)) {
return;
}
browserWindow.webContents.on('console-message', onConsoleMessages);
const browserWindows = BrowserWindow.getAllWindows();
for (const browserWindow of browserWindows) {
if (!browserWindow || !windowExists(browserWindow)) {
return;
}
browserWindow.webContents.on('console-message', onConsoleMessages);
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,52 @@
import { logger } from './logger';
export class AnimationQueue {
private queue: any[] = [];
private running: boolean = false;
private queue: any[] = [];
private running: boolean = false;
constructor() {
this.animate = this.animate.bind(this);
}
constructor() {
this.animate = this.animate.bind(this);
}
/**
* Pushes each animation to a queue
*
* @param object
*/
public push(object): void {
if (this.running) {
this.queue.push(object);
return;
}
this.running = true;
setTimeout(() => this.animate(object), 0);
/**
* Pushes each animation to a queue
*
* @param object
*/
public push(object): void {
if (this.running) {
this.queue.push(object);
return;
}
this.running = true;
setTimeout(() => this.animate(object), 0);
}
/**
* Animates an animation that is part of the queue
* @param object
*/
public async animate(object): Promise<void> {
try {
await object.func.apply(null, object.args);
} catch (err) {
logger.error(`animation-queue: encountered an error: ${err} with stack trace: ${err.stack}`);
} finally {
if (this.queue.length > 0) {
// Run next animation
this.animate.call(this, this.queue.shift());
} else {
this.running = false;
}
}
/**
* Animates an animation that is part of the queue
* @param object
*/
public async animate(object): Promise<void> {
try {
await object.func.apply(null, object.args);
} catch (err) {
logger.error(
`animation-queue: encountered an error: ${err} with stack trace: ${err.stack}`,
);
} finally {
if (this.queue.length > 0) {
// Run next animation
this.animate.call(this, this.queue.shift());
} else {
this.running = false;
}
}
}
/**
* Clears the queue
*/
public clear(): void {
this.queue = [];
}
/**
* Clears the queue
*/
public clear(): void {
this.queue = [];
}
}

View File

@ -1,97 +1,100 @@
export enum apiCmds {
isOnline = 'is-online',
getVersionInfo = 'get-version-info',
registerLogger = 'register-logger',
setBadgeCount = 'set-badge-count',
badgeDataUrl = 'badge-data-url',
activate = 'activate',
registerBoundsChange = 'register-bounds-change',
registerProtocolHandler = 'register-protocol-handler',
registerLogRetriever = 'register-log-retriever',
sendLogs = 'send-logs',
registerAnalyticsHandler = 'register-analytics-handler',
registerActivityDetection = 'register-activity-detection',
showNotificationSettings = 'show-notification-settings',
sanitize = 'sanitize',
bringToFront = 'bring-to-front',
openScreenPickerWindow = 'open-screen-picker-window',
popupMenu = 'popup-menu',
optimizeMemoryConsumption = 'optimize-memory-consumption',
optimizeMemoryRegister = 'optimize-memory-register',
setIsInMeeting = 'set-is-in-meeting',
setLocale = 'set-locale',
openScreenSnippet = 'open-screen-snippet',
closeScreenSnippet = 'close-screen-snippet',
keyPress = 'key-press',
closeWindow = 'close-window',
openScreenSharingIndicator = 'open-screen-sharing-indicator',
closeScreenSharingIndicator = 'close-screen-sharing-indicator',
downloadManagerAction = 'download-manager-action',
getMediaSource = 'get-media-source',
notification = 'notification',
closeNotification = 'close-notification',
isMisspelled = 'is-misspelled',
memoryInfo = 'memory-info',
swiftSearch = 'swift-search',
getConfigUrl = 'get-config-url',
registerRestartFloater = 'register-restart-floater',
setCloudConfig = 'set-cloud-config',
getCPUUsage = 'get-cpu-usage',
checkMediaPermission = 'check-media-permission',
registerDownloadHandler = 'register-download-handler',
openDownloadedItem = 'open-downloaded-item',
showDownloadedItem = 'show-downloaded-item',
clearDownloadedItems = 'clear-downloaded-items',
restartApp = 'restart-app',
setIsMana = 'set-is-mana',
showNotification = 'show-notification',
isOnline = 'is-online',
getVersionInfo = 'get-version-info',
registerLogger = 'register-logger',
setBadgeCount = 'set-badge-count',
badgeDataUrl = 'badge-data-url',
activate = 'activate',
registerBoundsChange = 'register-bounds-change',
registerProtocolHandler = 'register-protocol-handler',
registerLogRetriever = 'register-log-retriever',
sendLogs = 'send-logs',
registerAnalyticsHandler = 'register-analytics-handler',
registerActivityDetection = 'register-activity-detection',
showNotificationSettings = 'show-notification-settings',
sanitize = 'sanitize',
bringToFront = 'bring-to-front',
openScreenPickerWindow = 'open-screen-picker-window',
popupMenu = 'popup-menu',
optimizeMemoryConsumption = 'optimize-memory-consumption',
optimizeMemoryRegister = 'optimize-memory-register',
setIsInMeeting = 'set-is-in-meeting',
setLocale = 'set-locale',
openScreenSnippet = 'open-screen-snippet',
closeScreenSnippet = 'close-screen-snippet',
keyPress = 'key-press',
closeWindow = 'close-window',
openScreenSharingIndicator = 'open-screen-sharing-indicator',
closeScreenSharingIndicator = 'close-screen-sharing-indicator',
downloadManagerAction = 'download-manager-action',
getMediaSource = 'get-media-source',
notification = 'notification',
closeNotification = 'close-notification',
isMisspelled = 'is-misspelled',
memoryInfo = 'memory-info',
swiftSearch = 'swift-search',
getConfigUrl = 'get-config-url',
registerRestartFloater = 'register-restart-floater',
setCloudConfig = 'set-cloud-config',
getCPUUsage = 'get-cpu-usage',
checkMediaPermission = 'check-media-permission',
registerDownloadHandler = 'register-download-handler',
openDownloadedItem = 'open-downloaded-item',
showDownloadedItem = 'show-downloaded-item',
clearDownloadedItems = 'clear-downloaded-items',
restartApp = 'restart-app',
setIsMana = 'set-is-mana',
showNotification = 'show-notification',
}
export enum apiName {
symphonyApi = 'symphony-api',
mainWindowName = 'main',
notificationWindowName = 'notification-window',
symphonyApi = 'symphony-api',
mainWindowName = 'main',
notificationWindowName = 'notification-window',
}
export const NOTIFICATION_WINDOW_TITLE = 'Notification - Symphony';
export interface IApiArgs {
memoryInfo: Electron.ProcessMemoryInfo;
word: string;
cmd: apiCmds;
isOnline: boolean;
count: number;
dataUrl: string;
windowName: string;
period: number;
reason: string;
sources: Electron.DesktopCapturerSource[];
id: number;
cpuUsage: Electron.CPUUsage;
isInMeeting: boolean;
locale: string;
keyCode: number;
windowType: WindowTypes;
winKey: string;
streamId: string;
displayId: string;
path: string;
type: string;
logName: string;
logs: ILogs;
cloudConfig: object;
isMana: boolean;
notificationOpts: object;
notificationId: number;
theme: Themes;
memoryInfo: Electron.ProcessMemoryInfo;
word: string;
cmd: apiCmds;
isOnline: boolean;
count: number;
dataUrl: string;
windowName: string;
period: number;
reason: string;
sources: Electron.DesktopCapturerSource[];
id: number;
cpuUsage: Electron.CPUUsage;
isInMeeting: boolean;
locale: string;
keyCode: number;
windowType: WindowTypes;
winKey: string;
streamId: string;
displayId: string;
path: string;
type: string;
logName: string;
logs: ILogs;
cloudConfig: object;
isMana: boolean;
notificationOpts: object;
notificationId: number;
theme: Themes;
}
export type Themes = 'light' | 'dark';
export type WindowTypes = 'screen-picker' | 'screen-sharing-indicator' | 'notification-settings';
export type WindowTypes =
| 'screen-picker'
| 'screen-sharing-indicator'
| 'notification-settings';
export interface IBadgeCount {
count: number;
count: number;
}
/**
@ -99,27 +102,27 @@ export interface IBadgeCount {
*/
export type ScreenSnippetDataType = 'ERROR' | 'image/png;base64';
export interface IScreenSnippet {
data?: string;
message?: string;
type: ScreenSnippetDataType;
data?: string;
message?: string;
type: ScreenSnippetDataType;
}
export interface IBoundsChange extends Electron.Rectangle {
windowName: string;
windowName: string;
}
/**
* Screen sharing indicator
*/
export interface IScreenSharingIndicator {
type: string;
requestId: number;
reason?: string;
type: string;
requestId: number;
reason?: string;
}
export enum KeyCodes {
Esc = 27,
Alt = 18,
Esc = 27,
Alt = 18,
}
type Theme = '' | 'light' | 'dark';
@ -128,93 +131,102 @@ type Theme = '' | 'light' | 'dark';
* Notification
*/
export interface INotificationData {
id: number;
title: string;
body: string;
image: string;
icon?: string;
flash: boolean;
color: string;
tag: string;
sticky: boolean;
company: string;
displayTime: number;
isExternal: boolean;
theme: Theme;
isElectronNotification?: boolean;
callback?: () => void;
hasReply?: boolean;
id: number;
title: string;
body: string;
image: string;
icon?: string;
flash: boolean;
color: string;
tag: string;
sticky: boolean;
company: string;
displayTime: number;
isExternal: boolean;
theme: Theme;
isElectronNotification?: boolean;
callback?: () => void;
hasReply?: boolean;
}
export enum NotificationActions {
notificationClicked = 'notification-clicked',
notificationClosed = 'notification-closed',
notificationReply = 'notification-reply',
notificationClicked = 'notification-clicked',
notificationClosed = 'notification-closed',
notificationReply = 'notification-reply',
}
/**
* Screen sharing Indicator
*/
export interface IScreenSharingIndicatorOptions {
displayId: string;
requestId: number;
streamId: string;
stream?: MediaStream;
displayId: string;
requestId: number;
streamId: string;
stream?: MediaStream;
}
export interface IVersionInfo {
containerIdentifier: string;
containerVer: string;
buildNumber: string;
apiVer: string;
searchApiVer: string;
containerIdentifier: string;
containerVer: string;
buildNumber: string;
apiVer: string;
searchApiVer: string;
}
export interface ICPUUsage {
percentCPUUsage: number;
idleWakeupsPerSecond: number;
percentCPUUsage: number;
idleWakeupsPerSecond: number;
}
export interface IDownloadManager {
_id: string;
fileName: string;
fileDisplayName: string;
savedPath: string;
total: number;
flashing?: boolean;
count?: number;
_id: string;
fileName: string;
fileDisplayName: string;
savedPath: string;
total: number;
flashing?: boolean;
count?: number;
}
export interface IMediaPermission {
camera: string;
microphone: string;
screen: string;
camera: string;
microphone: string;
screen: string;
}
export interface ILogMsg {
level: LogLevel;
details: any;
showInConsole: boolean;
startTime: number;
level: LogLevel;
details: any;
showInConsole: boolean;
startTime: number;
}
export type LogLevel = 'error' | 'warn' | 'info' | 'verbose' | 'debug' | 'silly';
export type LogLevel =
| 'error'
| 'warn'
| 'info'
| 'verbose'
| 'debug'
| 'silly';
export interface ILogFile {
filename: string;
contents: string;
filename: string;
contents: string;
}
export interface ILogs {
logName: string;
logFiles: ILogFile[];
logName: string;
logFiles: ILogFile[];
}
export interface IRestartFloaterData {
windowName: string;
bounds: Electron.Rectangle;
windowName: string;
bounds: Electron.Rectangle;
}
export type Reply = string;
export type ElectronNotificationData = Reply | object;
export type NotificationActionCallback = (event: NotificationActions, data: INotificationData) => void;
export type NotificationActionCallback = (
event: NotificationActions,
data: INotificationData,
) => void;

View File

@ -1,9 +1,10 @@
export const isDevEnv = process.env.ELECTRON_DEV ?
process.env.ELECTRON_DEV.trim().toLowerCase() === 'true' : false;
export const isDevEnv = process.env.ELECTRON_DEV
? process.env.ELECTRON_DEV.trim().toLowerCase() === 'true'
: false;
export const isElectronQA = !!process.env.ELECTRON_QA;
export const isMac = (process.platform === 'darwin');
export const isWindowsOS = (process.platform === 'win32');
export const isLinux = (process.platform === 'linux');
export const isMac = process.platform === 'darwin';
export const isWindowsOS = process.platform === 'win32';
export const isLinux = process.platform === 'linux';
export const isNodeEnv = !!process.env.NODE_ENV;

View File

@ -7,82 +7,99 @@ export type LocaleType = 'en-US' | 'ja-JP' | 'fr-FR';
type formatterFunction = (args?: object) => string;
class Translation {
/**
* Returns translated string with respect to value, resource & name space
*
* @param value {string} key field in the resources
* @param resource {string} current locale resource
* @param namespace {string} name space in the resource
*/
private static translate(
value: string,
resource: JSON | null,
namespace: string | undefined,
): string {
return resource
? Translation.getResource(resource, namespace)[value] || value
: value;
}
private static getResource = (
resource: JSON,
namespace: string | undefined,
): JSON => (namespace ? resource[namespace] : resource);
private locale: LocaleType = 'en-US';
private loadedResources: object = {};
/**
* Returns translated string with respect to value, resource & name space
*
* @param value {string} key field in the resources
* @param resource {string} current locale resource
* @param namespace {string} name space in the resource
*/
private static translate(value: string, resource: JSON | null, namespace: string | undefined): string {
return resource ? (Translation.getResource(resource, namespace)[value] || value) : value;
}
private static getResource = (resource: JSON, namespace: string | undefined): JSON => namespace ? resource[namespace] : resource;
private locale: LocaleType = 'en-US';
private loadedResources: object = {};
/**
* Apply the locale for translation
*
* @param locale
*/
public setLocale(locale: LocaleType): void {
const localeMatch: string[] | null = locale.match(localeCodeRegex);
if (!locale && (!localeMatch || localeMatch.length < 1)) {
return;
}
this.locale = locale;
/**
* Apply the locale for translation
*
* @param locale
*/
public setLocale(locale: LocaleType): void {
const localeMatch: string[] | null = locale.match(localeCodeRegex);
if (!locale && (!localeMatch || localeMatch.length < 1)) {
return;
}
/**
* Gets the current locale
*
* @return LocaleType {string}
*/
public getLocale(): LocaleType {
return this.locale;
}
this.locale = locale;
}
/**
* fetches and returns the translated value
*
* @param value {string}
* @param namespace {string}
* @example t('translate and formats {data} ', namespace)({ data: 'string' })
* @returns translate and formats string
*/
public t(value: string, namespace?: string): formatterFunction {
return (args?: object): string => {
if (this.loadedResources && this.loadedResources[this.locale]) {
return formatString(Translation.translate(value, this.loadedResources[this.locale], namespace), args);
}
const resource = this.loadResource(this.locale);
return formatString(Translation.translate(value, resource, namespace) || value, args);
};
}
/**
* Gets the current locale
*
* @return LocaleType {string}
*/
public getLocale(): LocaleType {
return this.locale;
}
/**
* Keeps ref of loaded resources from the main process
*
* @param locale {LocaleType}
* @param resource {JSON}
*/
public setResource(locale: LocaleType, resource: JSON): void {
this.locale = locale;
this.loadedResources = resource;
}
/**
* fetches and returns the translated value
*
* @param value {string}
* @param namespace {string}
* @example t('translate and formats {data} ', namespace)({ data: 'string' })
* @returns translate and formats string
*/
public t(value: string, namespace?: string): formatterFunction {
return (args?: object): string => {
if (this.loadedResources && this.loadedResources[this.locale]) {
return formatString(
Translation.translate(
value,
this.loadedResources[this.locale],
namespace,
),
args,
);
}
const resource = this.loadResource(this.locale);
return formatString(
Translation.translate(value, resource, namespace) || value,
args,
);
};
}
/**
* Reads the resources dir and returns the data
*
* @param locale
*/
private loadResource(locale: LocaleType): JSON | null {
return this.loadedResources[locale];
}
/**
* Keeps ref of loaded resources from the main process
*
* @param locale {LocaleType}
* @param resource {JSON}
*/
public setResource(locale: LocaleType, resource: JSON): void {
this.locale = locale;
this.loadedResources = resource;
}
/**
* Reads the resources dir and returns the data
*
* @param locale
*/
private loadResource(locale: LocaleType): JSON | null {
return this.loadedResources[locale];
}
}
const i18n = new Translation();

View File

@ -10,91 +10,114 @@ export type LocaleType = 'en-US' | 'ja-JP' | 'fr-FR';
type formatterFunction = (args?: object) => string;
class Translation {
/**
* Returns translated string with respect to value, resource & name space
*
* @param value {string} key field in the resources
* @param resource {string} current locale resource
* @param namespace {string} name space in the resource
*/
private static translate(value: string, resource: JSON | null, namespace: string | undefined): string {
return resource ? (Translation.getResource(resource, namespace)[value] || value) : value;
}
private static getResource = (resource: JSON, namespace: string | undefined): JSON => namespace ? resource[namespace] : resource;
public loadedResources: object = {};
private locale: LocaleType = 'en-US';
/**
* Returns translated string with respect to value, resource & name space
*
* @param value {string} key field in the resources
* @param resource {string} current locale resource
* @param namespace {string} name space in the resource
*/
private static translate(
value: string,
resource: JSON | null,
namespace: string | undefined,
): string {
return resource
? Translation.getResource(resource, namespace)[value] || value
: value;
}
private static getResource = (
resource: JSON,
namespace: string | undefined,
): JSON => (namespace ? resource[namespace] : resource);
public loadedResources: object = {};
private locale: LocaleType = 'en-US';
/**
* Apply the locale for translation
*
* @param locale
*/
public setLocale(locale: LocaleType): void {
if (!this.isValidLocale(locale)) {
return;
}
this.locale = locale;
if (!this.loadedResources[this.locale]) {
this.loadResource(this.locale);
}
/**
* Apply the locale for translation
*
* @param locale
*/
public setLocale(locale: LocaleType): void {
if (!this.isValidLocale(locale)) {
return;
}
/**
* Gets the current locale
*
* @return LocaleType {string}
*/
public getLocale(): LocaleType {
return this.locale;
}
/**
* Validates the locale using Regex
*
* @param locale {LocaleType}
*/
public isValidLocale(locale: LocaleType): boolean {
if (!locale) {
return false;
}
const localeMatch: string[] | null = locale.match(localeCodeRegex);
return !(!locale && (!localeMatch || localeMatch.length < 1));
}
/**
* fetches and returns the translated value
*
* @param value {string}
* @param namespace {string}
* @example t('translate and formats {data} ', namespace)({ data: 'string' })
* @returns translate and formats string
*/
public t(value: string, namespace?: string): formatterFunction {
return (args?: object): string => {
if (this.loadedResources && this.loadedResources[this.locale]) {
return formatString(Translation.translate(value, this.loadedResources[this.locale], namespace), args);
}
const resource = this.loadResource(this.locale);
return formatString(Translation.translate(value, resource, namespace) || value, args);
};
}
/**
* Reads the resources dir and returns the data
*
* @param locale
*/
private loadResource(locale: LocaleType): JSON | null {
const resourcePath = path.resolve(__dirname, '..', 'locale', `${locale}.json`);
if (!fs.existsSync(resourcePath)) {
return null;
}
return this.loadedResources[this.locale] = require(resourcePath);
this.locale = locale;
if (!this.loadedResources[this.locale]) {
this.loadResource(this.locale);
}
}
/**
* Gets the current locale
*
* @return LocaleType {string}
*/
public getLocale(): LocaleType {
return this.locale;
}
/**
* Validates the locale using Regex
*
* @param locale {LocaleType}
*/
public isValidLocale(locale: LocaleType): boolean {
if (!locale) {
return false;
}
const localeMatch: string[] | null = locale.match(localeCodeRegex);
return !(!locale && (!localeMatch || localeMatch.length < 1));
}
/**
* fetches and returns the translated value
*
* @param value {string}
* @param namespace {string}
* @example t('translate and formats {data} ', namespace)({ data: 'string' })
* @returns translate and formats string
*/
public t(value: string, namespace?: string): formatterFunction {
return (args?: object): string => {
if (this.loadedResources && this.loadedResources[this.locale]) {
return formatString(
Translation.translate(
value,
this.loadedResources[this.locale],
namespace,
),
args,
);
}
const resource = this.loadResource(this.locale);
return formatString(
Translation.translate(value, resource, namespace) || value,
args,
);
};
}
/**
* Reads the resources dir and returns the data
*
* @param locale
*/
private loadResource(locale: LocaleType): JSON | null {
const resourcePath = path.resolve(
__dirname,
'..',
'locale',
`${locale}.json`,
);
if (!fs.existsSync(resourcePath)) {
return null;
}
return (this.loadedResources[this.locale] = require(resourcePath));
}
}
const i18n = new Translation();

View File

@ -8,24 +8,24 @@ import { isElectronQA, isWindowsOS } from './env';
import { getCommandLineArgs } from './utils';
export interface ILogMsg {
level: LogLevel;
details: any;
showInConsole: boolean;
startTime: number;
level: LogLevel;
details: any;
showInConsole: boolean;
startTime: number;
}
interface IClientLogMsg {
msgs?: ILogMsg[];
logLevel?: LogLevel;
showInConsole?: boolean;
msgs?: ILogMsg[];
logLevel?: LogLevel;
showInConsole?: boolean;
}
const MAX_LOG_QUEUE_LENGTH = 100;
// Force log path to local path in Windows rather than roaming
if (isWindowsOS && process.env.LOCALAPPDATA) {
app.setPath('appData', process.env.LOCALAPPDATA);
app.setPath('userData', path.join(app.getPath('appData'), app.getName()));
app.setPath('appData', process.env.LOCALAPPDATA);
app.setPath('userData', path.join(app.getPath('appData'), app.getName()));
}
// Electron wants this to be called initially before calling
@ -33,233 +33,267 @@ if (isWindowsOS && process.env.LOCALAPPDATA) {
app.setAppLogsPath();
class Logger {
private readonly showInConsole: boolean = false;
private readonly desiredLogLevel?: LogLevel;
private readonly logQueue: ILogMsg[];
private readonly logPath: string;
private loggerWindow: Electron.WebContents | null;
private readonly showInConsole: boolean = false;
private readonly desiredLogLevel?: LogLevel;
private readonly logQueue: ILogMsg[];
private readonly logPath: string;
private loggerWindow: Electron.WebContents | null;
constructor() {
constructor() {
this.loggerWindow = null;
this.logQueue = [];
// If the user has specified a custom log path use it.
const customLogPathArg = getCommandLineArgs(
process.argv,
'--logPath=',
false,
);
const customLogsFolder =
customLogPathArg &&
customLogPathArg.substring(customLogPathArg.indexOf('=') + 1);
if (customLogsFolder) {
if (!fs.existsSync(customLogsFolder)) {
fs.mkdirSync(customLogsFolder, { recursive: true });
}
app.setPath('logs', customLogsFolder);
}
this.loggerWindow = null;
this.logQueue = [];
// If the user has specified a custom log path use it.
const customLogPathArg = getCommandLineArgs(process.argv, '--logPath=', false);
const customLogsFolder = customLogPathArg && customLogPathArg.substring(customLogPathArg.indexOf('=') + 1);
if (customLogsFolder) {
if (!fs.existsSync(customLogsFolder)) {
fs.mkdirSync(customLogsFolder, { recursive: true });
}
app.setPath('logs', customLogsFolder);
this.logPath = app.getPath('logs');
if (app.isPackaged) {
transports.file.file = path.join(this.logPath, `app_${Date.now()}.log`);
transports.file.level = 'debug';
transports.file.format =
'{y}-{m}-{d} {h}:{i}:{s}:{ms} {z} | {level} | {text}';
transports.file.appName = 'Symphony';
}
const logLevel = getCommandLineArgs(process.argv, '--logLevel=', false);
if (logLevel) {
const level = logLevel.split('=')[1];
if (level) {
this.desiredLogLevel = level as LogLevel;
}
}
if (getCommandLineArgs(process.argv, '--enableConsoleLogging', false)) {
this.showInConsole = true;
}
// cleans up old logs if there are any
if (app.isPackaged) {
this.cleanupOldLogs();
}
}
/**
* get instance of logQueue
*/
public getLogQueue(): ILogMsg[] {
return this.logQueue;
}
/**
* Log error
*
* @param message {string} - message to be logged
* @param data {any} - extra data that needs to be logged
*/
public error(message: string, ...data: any[]): void {
this.log('error', message, data);
}
/**
* Log warn
*
* @param message {string} - message to be logged
* @param data {any} - extra data that needs to be logged
*/
public warn(message: string, ...data: any[]): void {
this.log('warn', message, data);
}
/**
* Log info
*
* @param message {string} - message to be logged
* @param data {any} - extra data that needs to be logged
*/
public info(message: string, ...data: any[]): void {
this.log('info', message, data);
}
/**
* Log verbose
*
* @param message {string} - message to be logged
* @param data {array} - extra data that needs to be logged
*/
public verbose(message: string, ...data: any[]): void {
this.log('verbose', message, data);
}
/**
* Log debug
*
* @param message {string} - message to be logged
* @param data {any} - extra data that needs to be logged
*/
public debug(message: string, ...data: any[]): void {
this.log('debug', message, data);
}
/**
* Log silly
*
* @param message {string} - message to be logged
* @param data {any} - extra data that needs to be logged
*/
public silly(message: string, ...data: any[]): void {
this.log('silly', message, data);
}
/**
* Sets the renderer window for sending logs to the client
*
* @param window {WebContents} - renderer window
*/
public setLoggerWindow(window: Electron.WebContents): void {
this.loggerWindow = window;
if (this.loggerWindow) {
const logMsgs: IClientLogMsg = {};
if (this.logQueue.length) {
logMsgs.msgs = this.logQueue;
}
if (this.desiredLogLevel) {
logMsgs.logLevel = this.desiredLogLevel;
}
if (Object.keys(logMsgs).length) {
this.loggerWindow.send('log', logMsgs);
}
}
}
/**
* Main instance of the logger method
*
* @param logLevel {LogLevel} - Different type of log levels
* @param message {string} - Log message
* @param data {array} - extra data to be logged
* @param sendToCloud {boolean} - wehether to send the logs on to cloud
*/
public log(
logLevel: LogLevel,
message: string,
data: any[] = [],
sendToCloud: boolean = true,
): void {
if (data && data.length > 0) {
data.forEach((param) => {
message += `, '${param && typeof param}': ${JSON.stringify(param)}`;
});
}
if (!isElectronQA) {
switch (logLevel) {
case 'error':
electronLog.error(message);
break;
case 'warn':
electronLog.warn(message);
break;
case 'info':
electronLog.info(message);
break;
case 'verbose':
electronLog.verbose(message);
break;
case 'debug':
electronLog.debug(message);
break;
case 'silly':
electronLog.silly(message);
break;
default:
electronLog.info(message);
}
}
if (sendToCloud) {
this.sendToCloud(this.formatLogMsg(logLevel, message));
}
}
/**
* Formats the logs in the format that required
* to send to the client
*
* @param level {LogLevel} - Different type of log levels
* @param details {any} - log format that required to send to client
*/
private formatLogMsg(level: LogLevel, details: any): ILogMsg {
return {
details,
level,
showInConsole: this.showInConsole,
startTime: Date.now(),
};
}
/**
* This will send the logs to the client if loggerWindow
* else adds the logs to a Queue
*
* @param logMsg {ILogMsg}
*/
private sendToCloud(logMsg: ILogMsg): void {
// don't send logs if it is not desired by the user
if (this.desiredLogLevel && this.desiredLogLevel !== logMsg.level) {
return;
}
if (this.loggerWindow) {
const browserWindow = BrowserWindow.fromWebContents(this.loggerWindow);
if (
!(
!!browserWindow &&
typeof browserWindow.isDestroyed === 'function' &&
!browserWindow.isDestroyed()
)
) {
return;
}
this.loggerWindow.send('log', {
msgs: [logMsg],
logLevel: this.desiredLogLevel,
showInConsole: this.showInConsole,
});
return;
}
this.logQueue.push(logMsg);
// don't store more than 100 msgs. keep most recent log msgs.
if (this.logQueue.length > MAX_LOG_QUEUE_LENGTH) {
this.logQueue.shift();
}
}
/**
* Cleans up logs older than a day
*/
private cleanupOldLogs(): void {
const files = fs.readdirSync(this.logPath);
const deleteTimeStamp = new Date().getTime() - 5 * 24 * 60 * 60 * 1000;
files.forEach((file) => {
const filePath = path.join(this.logPath, file);
if (fs.existsSync(filePath)) {
const stat = fs.statSync(filePath);
const fileTimestamp = new Date(util.inspect(stat.mtime)).getTime();
if (fileTimestamp < deleteTimeStamp) {
fs.unlinkSync(filePath);
}
this.logPath = app.getPath('logs');
if (app.isPackaged) {
transports.file.file = path.join(this.logPath, `app_${Date.now()}.log`);
transports.file.level = 'debug';
transports.file.format = '{y}-{m}-{d} {h}:{i}:{s}:{ms} {z} | {level} | {text}';
transports.file.appName = 'Symphony';
}
const logLevel = getCommandLineArgs(process.argv, '--logLevel=', false);
if (logLevel) {
const level = logLevel.split('=')[1];
if (level) {
this.desiredLogLevel = level as LogLevel;
}
}
if (getCommandLineArgs(process.argv, '--enableConsoleLogging', false)) {
this.showInConsole = true;
}
// cleans up old logs if there are any
if (app.isPackaged) {
this.cleanupOldLogs();
}
}
/**
* get instance of logQueue
*/
public getLogQueue(): ILogMsg[] {
return this.logQueue;
}
/**
* Log error
*
* @param message {string} - message to be logged
* @param data {any} - extra data that needs to be logged
*/
public error(message: string, ...data: any[]): void {
this.log('error', message, data);
}
/**
* Log warn
*
* @param message {string} - message to be logged
* @param data {any} - extra data that needs to be logged
*/
public warn(message: string, ...data: any[]): void {
this.log('warn', message, data);
}
/**
* Log info
*
* @param message {string} - message to be logged
* @param data {any} - extra data that needs to be logged
*/
public info(message: string, ...data: any[]): void {
this.log('info', message, data);
}
/**
* Log verbose
*
* @param message {string} - message to be logged
* @param data {array} - extra data that needs to be logged
*/
public verbose(message: string, ...data: any[]): void {
this.log('verbose', message, data);
}
/**
* Log debug
*
* @param message {string} - message to be logged
* @param data {any} - extra data that needs to be logged
*/
public debug(message: string, ...data: any[]): void {
this.log('debug', message, data);
}
/**
* Log silly
*
* @param message {string} - message to be logged
* @param data {any} - extra data that needs to be logged
*/
public silly(message: string, ...data: any[]): void {
this.log('silly', message, data);
}
/**
* Sets the renderer window for sending logs to the client
*
* @param window {WebContents} - renderer window
*/
public setLoggerWindow(window: Electron.WebContents): void {
this.loggerWindow = window;
if (this.loggerWindow) {
const logMsgs: IClientLogMsg = {};
if (this.logQueue.length) {
logMsgs.msgs = this.logQueue;
}
if (this.desiredLogLevel) {
logMsgs.logLevel = this.desiredLogLevel;
}
if (Object.keys(logMsgs).length) {
this.loggerWindow.send('log', logMsgs);
}
}
}
/**
* Main instance of the logger method
*
* @param logLevel {LogLevel} - Different type of log levels
* @param message {string} - Log message
* @param data {array} - extra data to be logged
* @param sendToCloud {boolean} - wehether to send the logs on to cloud
*/
public log(logLevel: LogLevel, message: string, data: any[] = [], sendToCloud: boolean = true): void {
if (data && data.length > 0) {
data.forEach((param) => {
message += `, '${param && typeof param}': ${JSON.stringify(param)}`;
});
}
if (!isElectronQA) {
switch (logLevel) {
case 'error': electronLog.error(message); break;
case 'warn': electronLog.warn(message); break;
case 'info': electronLog.info(message); break;
case 'verbose': electronLog.verbose(message); break;
case 'debug': electronLog.debug(message); break;
case 'silly': electronLog.silly(message); break;
default: electronLog.info(message);
}
}
if (sendToCloud) {
this.sendToCloud(this.formatLogMsg(logLevel, message));
}
}
/**
* Formats the logs in the format that required
* to send to the client
*
* @param level {LogLevel} - Different type of log levels
* @param details {any} - log format that required to send to client
*/
private formatLogMsg(level: LogLevel, details: any): ILogMsg {
return {
details,
level,
showInConsole: this.showInConsole,
startTime: Date.now(),
};
}
/**
* This will send the logs to the client if loggerWindow
* else adds the logs to a Queue
*
* @param logMsg {ILogMsg}
*/
private sendToCloud(logMsg: ILogMsg): void {
// don't send logs if it is not desired by the user
if (this.desiredLogLevel && this.desiredLogLevel !== logMsg.level) {
return;
}
if (this.loggerWindow) {
const browserWindow = BrowserWindow.fromWebContents(this.loggerWindow);
if (!(!!browserWindow && typeof browserWindow.isDestroyed === 'function' && !browserWindow.isDestroyed())) {
return;
}
this.loggerWindow.send('log', { msgs: [ logMsg ], logLevel: this.desiredLogLevel, showInConsole: this.showInConsole });
return;
}
this.logQueue.push(logMsg);
// don't store more than 100 msgs. keep most recent log msgs.
if (this.logQueue.length > MAX_LOG_QUEUE_LENGTH) {
this.logQueue.shift();
}
}
/**
* Cleans up logs older than a day
*/
private cleanupOldLogs(): void {
const files = fs.readdirSync(this.logPath);
const deleteTimeStamp = new Date().getTime() - (5 * 24 * 60 * 60 * 1000);
files.forEach((file) => {
const filePath = path.join(this.logPath, file);
if (fs.existsSync(filePath)) {
const stat = fs.statSync(filePath);
const fileTimestamp = new Date(util.inspect(stat.mtime)).getTime();
if ((fileTimestamp < deleteTimeStamp)) {
fs.unlinkSync(filePath);
}
}
});
}
}
});
}
}
const logger = new Logger();

View File

@ -10,10 +10,10 @@ const patch = /-([0-9A-Za-z-.]+)/;
* @returns {String[]}
*/
const split = (v: string): string[] => {
const temp = v.replace(/^v/, '').split('.');
const arr = temp.splice(0, 2);
arr.push(temp.join('.'));
return arr;
const temp = v.replace(/^v/, '').split('.');
const arr = temp.splice(0, 2);
arr.push(temp.join('.'));
return arr;
};
/**
@ -21,7 +21,7 @@ const split = (v: string): string[] => {
* @param v Version string
*/
const tryParse = (v: string): string | number => {
return Number.isNaN(Number(v)) ? v : Number(v);
return Number.isNaN(Number(v)) ? v : Number(v);
};
/**
@ -32,10 +32,10 @@ const tryParse = (v: string): string | number => {
* @returns {number}
*/
const validate = (version: string): number => {
if (!semver.test(version)) {
return -1;
}
return 1;
if (!semver.test(version)) {
return -1;
}
return 1;
};
/**
@ -49,51 +49,57 @@ const validate = (version: string): number => {
* @returns {number}
*/
export const compareVersions = (v1: string, v2: string): number => {
if (validate(v1) === -1 || validate(v2) === -1) {
if (validate(v1) === -1 || validate(v2) === -1) {
return -1;
}
const s1 = split(v1);
const s2 = split(v2);
for (let i = 0; i < 3; i++) {
const n1 = parseInt(s1[i] || '0', 10);
const n2 = parseInt(s2[i] || '0', 10);
if (n1 > n2) {
return 1;
}
if (n2 > n1) {
return -1;
}
}
if ([s1[2], s2[2]].every(patch.test.bind(patch))) {
// @ts-ignore
const p1 = patch.exec(s1[2])[1].split('.').map(tryParse);
// @ts-ignore
const p2 = patch.exec(s2[2])[1].split('.').map(tryParse);
for (let k = 0; k < Math.max(p1.length, p2.length); k++) {
if (
p1[k] === undefined ||
(typeof p2[k] === 'string' && typeof p1[k] === 'number')
) {
return -1;
}
if (
p2[k] === undefined ||
(typeof p1[k] === 'string' && typeof p2[k] === 'number')
) {
return 1;
}
if (p1[k] > p2[k]) {
return 1;
}
if (p2[k] > p1[k]) {
return -1;
}
}
} else if ([s1[2], s2[2]].some(patch.test.bind(patch))) {
return patch.test(s1[2]) ? -1 : 1;
}
const s1 = split(v1);
const s2 = split(v2);
for (let i = 0; i < 3; i++) {
const n1 = parseInt(s1[i] || '0', 10);
const n2 = parseInt(s2[i] || '0', 10);
if (n1 > n2) {
return 1;
}
if (n2 > n1) {
return -1;
}
}
if ([s1[2], s2[2]].every(patch.test.bind(patch))) {
// @ts-ignore
const p1 = patch.exec(s1[2])[1].split('.').map(tryParse);
// @ts-ignore
const p2 = patch.exec(s2[2])[1].split('.').map(tryParse);
for (let k = 0; k < Math.max(p1.length, p2.length); k++) {
if (p1[k] === undefined || typeof p2[k] === 'string' && typeof p1[k] === 'number') {
return -1;
}
if (p2[k] === undefined || typeof p1[k] === 'string' && typeof p2[k] === 'number') {
return 1;
}
if (p1[k] > p2[k]) {
return 1;
}
if (p2[k] > p1[k]) {
return -1;
}
}
} else if ([s1[2], s2[2]].some(patch.test.bind(patch))) {
return patch.test(s1[2]) ? -1 : 1;
}
return 0;
return 0;
};
/**
@ -104,22 +110,30 @@ export const compareVersions = (v1: string, v2: string): number => {
* try finding arg that starts with argName.
* @return {String} If found, returns the arg, otherwise null.
*/
export const getCommandLineArgs = (argv: string[], argName: string, exactMatch: boolean): string | null => {
if (!Array.isArray(argv)) {
throw new Error(`get-command-line-args: TypeError invalid func arg, must be an array: ${argv}`);
export const getCommandLineArgs = (
argv: string[],
argName: string,
exactMatch: boolean,
): string | null => {
if (!Array.isArray(argv)) {
throw new Error(
`get-command-line-args: TypeError invalid func arg, must be an array: ${argv}`,
);
}
const argNameToFind = argName.toLocaleLowerCase();
for (let i = 0, len = argv.length; i < len; i++) {
const arg = argv[i].toLocaleLowerCase();
if (
(exactMatch && arg === argNameToFind) ||
(!exactMatch && arg.startsWith(argNameToFind))
) {
return argv[i];
}
}
const argNameToFind = argName.toLocaleLowerCase();
for (let i = 0, len = argv.length; i < len; i++) {
const arg = argv[i].toLocaleLowerCase();
if ((exactMatch && arg === argNameToFind) ||
(!exactMatch && arg.startsWith(argNameToFind))) {
return argv[i];
}
}
return null;
return null;
};
/**
@ -129,12 +143,11 @@ export const getCommandLineArgs = (argv: string[], argName: string, exactMatch:
* @return {String} guid value in string
*/
export const getGuid = (): string => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,
(c) => {
const r = Math.random() * 16 | 0; // tslint:disable-line:no-bitwise
const v = c === 'x' ? r : (r & 0x3 | 0x8); // tslint:disable-line:no-bitwise
return v.toString(16);
});
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0; // tslint:disable-line:no-bitwise
const v = c === 'x' ? r : (r & 0x3) | 0x8; // tslint:disable-line:no-bitwise
return v.toString(16);
});
};
/**
@ -144,13 +157,13 @@ export const getGuid = (): string => {
* @param fields Fields to be picked
*/
export const pick = (object: object, fields: string[]) => {
const obj = {};
for (const field of fields) {
if (object[field] !== undefined && object[field] !== null) {
obj[field] = object[field];
}
const obj = {};
for (const field of fields) {
if (object[field] !== undefined && object[field] !== null) {
obj[field] = object[field];
}
return obj;
}
return obj;
};
/**
@ -161,18 +174,18 @@ export const pick = (object: object, fields: string[]) => {
* @return {Object} { test1: false }
*/
export const filterOutSelectedValues = (data: object, values): object => {
if (!data) {
return {};
if (!data) {
return {};
}
return Object.keys(data).reduce((obj, key) => {
if (Array.isArray(data[key]) && data[key].length <= 0) {
return obj;
}
return Object.keys(data).reduce((obj, key) => {
if (Array.isArray(data[key]) && data[key].length <= 0) {
return obj;
}
if (values.indexOf(data[key]) <= -1) {
obj[key] = data[key];
}
return obj;
}, {});
if (values.indexOf(data[key]) <= -1) {
obj[key] = data[key];
}
return obj;
}, {});
};
/**
@ -182,28 +195,33 @@ export const filterOutSelectedValues = (data: object, values): object => {
* @param wait
* @example const throttled = throttle(anyFunc, 500);
*/
export const throttle = (func: (...args) => void, wait: number): (...args) => void => {
if (wait <= 0) {
throw Error('throttle: invalid throttleTime arg, must be a number: ' + wait);
}
export const throttle = (
func: (...args) => void,
wait: number,
): ((...args) => void) => {
if (wait <= 0) {
throw Error(
'throttle: invalid throttleTime arg, must be a number: ' + wait,
);
}
let timer: NodeJS.Timer;
let lastRan = 0;
let timer: NodeJS.Timer;
let lastRan = 0;
return (...args) => {
if (!lastRan) {
func.apply(null, args);
lastRan = Date.now();
} else {
clearTimeout(timer);
timer = setTimeout(() => {
if ((Date.now() - lastRan) >= wait) {
func.apply(null, args);
lastRan = Date.now();
}
}, wait - (Date.now() - lastRan));
return (...args) => {
if (!lastRan) {
func.apply(null, args);
lastRan = Date.now();
} else {
clearTimeout(timer);
timer = setTimeout(() => {
if (Date.now() - lastRan >= wait) {
func.apply(null, args);
lastRan = Date.now();
}
};
}, wait - (Date.now() - lastRan));
}
};
};
/**
@ -220,20 +238,19 @@ export const throttle = (func: (...args) => void, wait: number): (...args) => vo
* @return {*}
*/
export const formatString = (str: string, data?: object): string => {
if (!str || !data) {
return str;
}
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
return str.replace(/({([^}]+)})/g, (i) => {
const replacedKey = i.replace(/{/, '').replace(/}/, '');
return data[replacedKey] ? data[replacedKey] : replacedKey;
});
}
}
if (!str || !data) {
return str;
}
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
return str.replace(/({([^}]+)})/g, (i) => {
const replacedKey = i.replace(/{/, '').replace(/}/, '');
return data[replacedKey] ? data[replacedKey] : replacedKey;
});
}
}
return str;
};
/**
@ -242,5 +259,5 @@ export const formatString = (str: string, data?: object): string => {
* @param percentage
*/
export const calculatePercentage = (value: number, percentage: number) => {
return value * percentage * 0.01;
return value * percentage * 0.01;
};

View File

@ -4,143 +4,152 @@ const urlParts = /^(https?:\/\/)?([^/]*@)?(.+?)(:\d{2,5})?([/?].*)?$/;
const dot = /\./g;
interface IURLObject {
tld: string;
domain: string;
subdomain: string;
tld: string;
domain: string;
subdomain: string;
}
export class WhitelistHandler {
/**
* Loops through the list of whitelist urls
* @param url {String} - url the electron is navigated to
*
* @returns {boolean}
*/
public isWhitelisted(url: string): boolean {
const { whitelistUrl } = config.getConfigFields(['whitelistUrl']);
/**
* Loops through the list of whitelist urls
* @param url {String} - url the electron is navigated to
*
* @returns {boolean}
*/
public isWhitelisted(url: string): boolean {
const { whitelistUrl } = config.getConfigFields([ 'whitelistUrl' ]);
return this.checkWhitelist(url, whitelistUrl);
}
return this.checkWhitelist(url, whitelistUrl);
/**
* Splits the url into tld, domain, subdomain
* @param url
*
* @return {{tld: string | *, domain: string | *, subdomain: string}}
*/
public parseDomain(url): IURLObject {
let urlSplit = url.match(urlParts);
let domain = urlSplit[3];
// capture top level domain
const tld = domain.slice(domain.lastIndexOf('.'));
urlSplit = domain.slice(0, -tld.length).split(dot);
// capture domain
domain = urlSplit.pop();
// capture subdomain
const subdomain = urlSplit.join('.');
return {
tld: tld.trim(),
domain: domain.trim(),
subdomain: subdomain.trim(),
};
}
/**
* Method that compares url against a list of whitelist
* returns true if hostName or domain present in the whitelist
*
* @param url {String} - url the electron is navigated to
* @param whitelistUrl {String} - comma separated whitelists
*
* @returns {boolean}
*/
private checkWhitelist(url: string, whitelistUrl: string): boolean {
const whitelistArray: string[] = whitelistUrl.split(',');
const parsedUrl = this.parseDomain(url);
if (!parsedUrl) {
return false;
}
/**
* Splits the url into tld, domain, subdomain
* @param url
*
* @return {{tld: string | *, domain: string | *, subdomain: string}}
*/
public parseDomain(url): IURLObject {
let urlSplit = url.match(urlParts);
let domain = urlSplit[3];
// capture top level domain
const tld = domain.slice(domain.lastIndexOf('.'));
urlSplit = domain.slice(0, -tld.length).split(dot);
// capture domain
domain = urlSplit.pop();
// capture subdomain
const subdomain = urlSplit.join('.');
return {
tld: tld.trim(),
domain: domain.trim(),
subdomain: subdomain.trim(),
};
if (!whitelistUrl) {
return false;
}
/**
* Method that compares url against a list of whitelist
* returns true if hostName or domain present in the whitelist
*
* @param url {String} - url the electron is navigated to
* @param whitelistUrl {String} - comma separated whitelists
*
* @returns {boolean}
*/
private checkWhitelist(url: string, whitelistUrl: string): boolean {
const whitelistArray: string[] = whitelistUrl.split(',');
const parsedUrl = this.parseDomain(url);
if (!parsedUrl) {
return false;
}
if (!whitelistUrl) {
return false;
}
if (!whitelistArray.length || whitelistArray.indexOf('*') !== -1) {
return true;
}
return whitelistArray.some((whitelistHost) => {
const parsedWhitelist = this.parseDomain(whitelistHost);
if (!parsedWhitelist) {
return false;
}
return this.matchDomains(parsedUrl, parsedWhitelist);
});
if (!whitelistArray.length || whitelistArray.indexOf('*') !== -1) {
return true;
}
/**
* Matches the respective hostName
* @param parsedUrl {Object} - parsed url
* @param parsedWhitelist {Object} - parsed whitelist
*
* example:
* matchDomain({ subdomain: www, domain: example, tld: com }, { subdomain: app, domain: example, tld: com })
*
* @returns {boolean}
*/
private matchDomains(parsedUrl: IURLObject, parsedWhitelist: IURLObject): boolean {
if (!parsedUrl || !parsedWhitelist) {
return false;
}
if (
parsedUrl.subdomain === parsedWhitelist.subdomain
&& parsedUrl.domain === parsedWhitelist.domain
&& parsedUrl.tld === parsedWhitelist.tld
) {
return true;
}
return whitelistArray.some((whitelistHost) => {
const parsedWhitelist = this.parseDomain(whitelistHost);
const hostNameFromUrl = parsedUrl.domain + parsedUrl.tld;
const hostNameFromWhitelist = parsedWhitelist.domain + parsedWhitelist.tld;
if (!parsedWhitelist) {
return false;
}
if (!parsedWhitelist.subdomain) {
return hostNameFromUrl === hostNameFromWhitelist;
}
return this.matchDomains(parsedUrl, parsedWhitelist);
});
}
return hostNameFromUrl === hostNameFromWhitelist && this.matchSubDomains(parsedUrl.subdomain, parsedWhitelist.subdomain);
/**
* Matches the respective hostName
* @param parsedUrl {Object} - parsed url
* @param parsedWhitelist {Object} - parsed whitelist
*
* example:
* matchDomain({ subdomain: www, domain: example, tld: com }, { subdomain: app, domain: example, tld: com })
*
* @returns {boolean}
*/
private matchDomains(
parsedUrl: IURLObject,
parsedWhitelist: IURLObject,
): boolean {
if (!parsedUrl || !parsedWhitelist) {
return false;
}
if (
parsedUrl.subdomain === parsedWhitelist.subdomain &&
parsedUrl.domain === parsedWhitelist.domain &&
parsedUrl.tld === parsedWhitelist.tld
) {
return true;
}
/**
* Matches the last occurrence in the sub-domain
* @param subDomainUrl {String} - sub-domain from url
* @param subDomainWhitelist {String} - sub-domain from whitelist
*
* example: matchSubDomains('www', 'app')
*
* @returns {boolean}
*/
private matchSubDomains(subDomainUrl: string, subDomainWhitelist: string): boolean {
if (subDomainUrl === subDomainWhitelist) {
return true;
}
const hostNameFromUrl = parsedUrl.domain + parsedUrl.tld;
const hostNameFromWhitelist = parsedWhitelist.domain + parsedWhitelist.tld;
const subDomainUrlArray = subDomainUrl.split('.');
const lastCharSubDomainUrl = subDomainUrlArray[subDomainUrlArray.length - 1];
const subDomainWhitelistArray = subDomainWhitelist.split('.');
const lastCharWhitelist = subDomainWhitelistArray[subDomainWhitelistArray.length - 1];
return lastCharSubDomainUrl === lastCharWhitelist;
if (!parsedWhitelist.subdomain) {
return hostNameFromUrl === hostNameFromWhitelist;
}
return (
hostNameFromUrl === hostNameFromWhitelist &&
this.matchSubDomains(parsedUrl.subdomain, parsedWhitelist.subdomain)
);
}
/**
* Matches the last occurrence in the sub-domain
* @param subDomainUrl {String} - sub-domain from url
* @param subDomainWhitelist {String} - sub-domain from whitelist
*
* example: matchSubDomains('www', 'app')
*
* @returns {boolean}
*/
private matchSubDomains(
subDomainUrl: string,
subDomainWhitelist: string,
): boolean {
if (subDomainUrl === subDomainWhitelist) {
return true;
}
const subDomainUrlArray = subDomainUrl.split('.');
const lastCharSubDomainUrl =
subDomainUrlArray[subDomainUrlArray.length - 1];
const subDomainWhitelistArray = subDomainWhitelist.split('.');
const lastCharWhitelist =
subDomainWhitelistArray[subDomainWhitelistArray.length - 1];
return lastCharSubDomainUrl === lastCharWhitelist;
}
}
const whitelistHandler = new WhitelistHandler();

View File

@ -2,17 +2,21 @@ import { remote } from 'electron';
import { IAnalyticsData } from '../app/analytics-handler';
import {
apiCmds,
IBoundsChange,
ILogMsg,
INotificationData,
IRestartFloaterData,
IScreenSharingIndicator,
IScreenSharingIndicatorOptions,
IScreenSnippet,
LogLevel,
apiCmds,
IBoundsChange,
ILogMsg,
INotificationData,
IRestartFloaterData,
IScreenSharingIndicator,
IScreenSharingIndicatorOptions,
IScreenSnippet,
LogLevel,
} from '../common/api-interface';
import { ICustomDesktopCapturerSource, ICustomSourcesOptions, IScreenSourceError } from './desktop-capturer';
import {
ICustomDesktopCapturerSource,
ICustomSourcesOptions,
IScreenSourceError,
} from './desktop-capturer';
import { SSFApi } from './ssf-api';
const ssf = new SSFApi();
@ -20,305 +24,355 @@ const notification = remote.require('../renderer/notification').notification;
let ssInstance: any;
try {
const SSAPIBridge = remote.require('swift-search').SSAPIBridge;
ssInstance = new SSAPIBridge();
const SSAPIBridge = remote.require('swift-search').SSAPIBridge;
ssInstance = new SSAPIBridge();
} catch (e) {
ssInstance = null;
console.warn("Failed to initialize swift search. You'll need to include the search dependency. Contact the developers for more details");
ssInstance = null;
console.warn(
"Failed to initialize swift search. You'll need to include the search dependency. Contact the developers for more details",
);
}
export class AppBridge {
/**
* Validates the incoming postMessage
* events based on the host name
*
* @param event
*/
private static isValidEvent(event): boolean {
if (!event) {
return false;
}
return event.source && event.source === window;
}
/**
* Validates the incoming postMessage
* events based on the host name
*
* @param event
*/
private static isValidEvent(event): boolean {
if (!event) {
return false;
public origin: string;
private readonly callbackHandlers = {
onMessage: (event) => this.handleMessage(event),
onActivityCallback: (idleTime: number) => this.activityCallback(idleTime),
onScreenSnippetCallback: (arg: IScreenSnippet) =>
this.screenSnippetCallback(arg),
onRegisterBoundsChangeCallback: (arg: IBoundsChange) =>
this.registerBoundsChangeCallback(arg),
onRegisterLoggerCallback: (
msg: ILogMsg,
logLevel: LogLevel,
showInConsole: boolean,
) => this.registerLoggerCallback(msg, logLevel, showInConsole),
onRegisterProtocolHandlerCallback: (uri: string) =>
this.protocolHandlerCallback(uri),
onCollectLogsCallback: () => this.collectLogsCallback(),
onScreenSharingIndicatorCallback: (arg: IScreenSharingIndicator) =>
this.screenSharingIndicatorCallback(arg),
onMediaSourceCallback: (
error: IScreenSourceError | null,
source: ICustomDesktopCapturerSource | undefined,
): void => this.gotMediaSource(error, source),
onNotificationCallback: (event, data) =>
this.notificationCallback(event, data),
onAnalyticsEventCallback: (data) => this.analyticsEventCallback(data),
restartFloater: (data) => this.restartFloater(data),
onDownloadItemCallback: (data) => this.onDownloadItemCallback(data),
};
constructor() {
// starts with corporate pod and
// will be updated with the global config url
const currentWindow = remote.getCurrentWindow();
// @ts-ignore
this.origin = currentWindow.origin || '';
// this.origin = '*'; // DEMO-APP: Comment this line back in only to test demo-app - DO NOT COMMIT
if (ssInstance && typeof ssInstance.setBroadcastMessage === 'function') {
ssInstance.setBroadcastMessage(this.broadcastMessage);
}
window.addEventListener('message', this.callbackHandlers.onMessage);
}
/**
* Switch case that validates and handle
* incoming messages from postMessage
*
* @param event
*/
private async handleMessage(event): Promise<void> {
if (!AppBridge.isValidEvent(event)) {
return;
}
const { method, data } = event.data;
switch (method) {
case apiCmds.getVersionInfo:
const versionInfo = await ssf.getVersionInfo();
this.broadcastMessage('get-version-info-callback', {
requestId: data.requestId,
response: versionInfo,
});
break;
case apiCmds.activate:
ssf.activate(data as string);
break;
case apiCmds.bringToFront:
const { windowName, reason } = data;
ssf.bringToFront(windowName as string, reason as string);
break;
case apiCmds.setBadgeCount:
if (typeof data === 'number') {
ssf.setBadgeCount(data as number);
}
return event.source && event.source === window;
}
public origin: string;
private readonly callbackHandlers = {
onMessage: (event) => this.handleMessage(event),
onActivityCallback: (idleTime: number) => this.activityCallback(idleTime),
onScreenSnippetCallback: (arg: IScreenSnippet) => this.screenSnippetCallback(arg),
onRegisterBoundsChangeCallback: (arg: IBoundsChange) => this.registerBoundsChangeCallback(arg),
onRegisterLoggerCallback: (msg: ILogMsg, logLevel: LogLevel, showInConsole: boolean) =>
this.registerLoggerCallback(msg, logLevel, showInConsole),
onRegisterProtocolHandlerCallback: (uri: string) => this.protocolHandlerCallback(uri),
onCollectLogsCallback: () => this.collectLogsCallback(),
onScreenSharingIndicatorCallback: (arg: IScreenSharingIndicator) => this.screenSharingIndicatorCallback(arg),
onMediaSourceCallback: (
error: IScreenSourceError | null,
source: ICustomDesktopCapturerSource | undefined,
): void => this.gotMediaSource(error, source),
onNotificationCallback: (event, data) => this.notificationCallback(event, data),
onAnalyticsEventCallback: (data) => this.analyticsEventCallback(data),
restartFloater: (data) => this.restartFloater(data),
onDownloadItemCallback: (data) => this.onDownloadItemCallback(data),
};
constructor() {
// starts with corporate pod and
// will be updated with the global config url
const currentWindow = remote.getCurrentWindow();
// @ts-ignore
this.origin = currentWindow.origin || '';
// this.origin = '*'; // DEMO-APP: Comment this line back in only to test demo-app - DO NOT COMMIT
if (ssInstance && typeof ssInstance.setBroadcastMessage === 'function') {
ssInstance.setBroadcastMessage(this.broadcastMessage);
break;
case apiCmds.openDownloadedItem:
if (typeof data === 'string') {
ssf.openDownloadedItem(data as string);
}
window.addEventListener('message', this.callbackHandlers.onMessage);
}
/**
* Switch case that validates and handle
* incoming messages from postMessage
*
* @param event
*/
private async handleMessage(event): Promise<void> {
if (!AppBridge.isValidEvent(event)) {
return;
break;
case apiCmds.showDownloadedItem:
if (typeof data === 'string') {
ssf.showDownloadedItem(data as string);
}
const { method, data } = event.data;
switch (method) {
case apiCmds.getVersionInfo:
const versionInfo = await ssf.getVersionInfo();
this.broadcastMessage('get-version-info-callback', {
requestId: data.requestId,
response: versionInfo,
});
break;
case apiCmds.activate:
ssf.activate(data as string);
break;
case apiCmds.bringToFront:
const { windowName, reason } = data;
ssf.bringToFront(windowName as string, reason as string);
break;
case apiCmds.setBadgeCount:
if (typeof data === 'number') {
ssf.setBadgeCount(data as number);
}
break;
case apiCmds.openDownloadedItem:
if (typeof data === 'string') {
ssf.openDownloadedItem(data as string);
}
break;
case apiCmds.showDownloadedItem:
if (typeof data === 'string') {
ssf.showDownloadedItem(data as string);
}
break;
case apiCmds.clearDownloadedItems:
ssf.clearDownloadedItems();
break;
case apiCmds.restartApp:
ssf.restartApp();
break;
case apiCmds.setLocale:
if (typeof data === 'string') {
ssf.setLocale(data as string);
}
break;
case apiCmds.registerActivityDetection:
ssf.registerActivityDetection(data as number, this.callbackHandlers.onActivityCallback);
break;
case apiCmds.registerDownloadHandler:
ssf.registerDownloadHandler(this.callbackHandlers.onDownloadItemCallback);
break;
case apiCmds.openScreenSnippet:
ssf.openScreenSnippet(this.callbackHandlers.onScreenSnippetCallback);
break;
case apiCmds.closeScreenSnippet:
ssf.closeScreenSnippet();
break;
case apiCmds.registerBoundsChange:
ssf.registerBoundsChange(this.callbackHandlers.onRegisterBoundsChangeCallback);
break;
case apiCmds.registerLogger:
ssf.registerLogger(this.callbackHandlers.onRegisterLoggerCallback);
break;
case apiCmds.registerProtocolHandler:
ssf.registerProtocolHandler(this.callbackHandlers.onRegisterProtocolHandlerCallback);
break;
case apiCmds.registerLogRetriever:
ssf.registerLogRetriever(this.callbackHandlers.onCollectLogsCallback, data);
break;
case apiCmds.sendLogs:
ssf.sendLogs(data.logName, data.logFiles);
break;
case apiCmds.openScreenSharingIndicator:
ssf.openScreenSharingIndicator(data as IScreenSharingIndicatorOptions, this.callbackHandlers.onScreenSharingIndicatorCallback);
break;
case apiCmds.closeScreenSharingIndicator:
ssf.closeScreenSharingIndicator(data.streamId as string);
break;
case apiCmds.getMediaSource:
await ssf.getMediaSource(data as ICustomSourcesOptions, this.callbackHandlers.onMediaSourceCallback);
break;
case apiCmds.notification:
notification.showNotification(data as INotificationData, this.callbackHandlers.onNotificationCallback);
break;
case apiCmds.closeNotification:
await notification.hideNotification(data as number);
break;
case apiCmds.showNotificationSettings:
ssf.showNotificationSettings(data);
break;
case apiCmds.setIsInMeeting:
if (typeof data === 'boolean') {
ssf.setIsInMeeting(data as boolean);
}
break;
case apiCmds.registerAnalyticsHandler:
ssf.registerAnalyticsEvent(this.callbackHandlers.onAnalyticsEventCallback);
break;
case apiCmds.registerRestartFloater:
ssf.registerRestartFloater(this.callbackHandlers.restartFloater);
break;
case apiCmds.setCloudConfig:
ssf.setCloudConfig(data as object);
break;
case apiCmds.swiftSearch:
if (ssInstance) {
ssInstance.handleMessageEvents(data);
}
break;
case apiCmds.getCPUUsage:
const cpuUsage = await ssf.getCPUUsage();
this.broadcastMessage('get-cpu-usage-callback', {
requestId: data.requestId,
response: cpuUsage,
});
break;
case apiCmds.checkMediaPermission:
const mediaPermission = await ssf.checkMediaPermission();
this.broadcastMessage('check-media-permission-callback', {
requestId: data.requestId,
response: mediaPermission,
});
break;
break;
case apiCmds.clearDownloadedItems:
ssf.clearDownloadedItems();
break;
case apiCmds.restartApp:
ssf.restartApp();
break;
case apiCmds.setLocale:
if (typeof data === 'string') {
ssf.setLocale(data as string);
}
}
/**
* Broadcast user activity
* @param idleTime {number} - system idle tick
*/
private activityCallback = (idleTime: number): void => this.broadcastMessage('activity-callback', idleTime);
/**
* Broadcast snippet data
* @param arg {IScreenSnippet}
*/
private screenSnippetCallback = (arg: IScreenSnippet): void => this.broadcastMessage('screen-snippet-callback', arg);
/**
* Broadcast bound changes
* @param arg {IBoundsChange}
*/
private registerBoundsChangeCallback = (arg: IBoundsChange): void => this.broadcastMessage('bound-changes-callback', arg);
/**
* Broadcast logs
* @param msg {ILogMsg}
* @param logLevel {LogLevel}
* @param showInConsole {boolean}
*/
private registerLoggerCallback(msg: ILogMsg, logLevel: LogLevel, showInConsole: boolean): void {
this.broadcastMessage('logger-callback', { msg, logLevel, showInConsole });
}
/**
* Broadcast protocol uri
* @param uri {string}
*/
private protocolHandlerCallback = (uri: string): void => this.broadcastMessage('protocol-callback', uri);
private collectLogsCallback = (): void => this.broadcastMessage('collect-logs', undefined);
/**
* Broadcast event that stops screen sharing
* @param arg {IScreenSharingIndicator}
*/
private screenSharingIndicatorCallback(arg: IScreenSharingIndicator): void {
this.broadcastMessage('screen-sharing-indicator-callback', arg);
}
/**
* Broadcast analytics events data
* @param arg {IAnalyticsData}
*/
private analyticsEventCallback(arg: IAnalyticsData): void {
this.broadcastMessage('analytics-event-callback', arg);
}
/**
* Broadcast download item event
* @param arg {object}
*/
private onDownloadItemCallback(arg: object): void {
this.broadcastMessage('download-handler-callback', arg);
}
/**
* Broadcast to restart floater event with data
* @param arg {IAnalyticsData}
*/
private restartFloater(arg: IRestartFloaterData): void {
this.broadcastMessage('restart-floater-callback', arg);
}
/**
* Broadcast the user selected source
* @param sourceError {IScreenSourceError}
* @param selectedSource {ICustomDesktopCapturerSource}
*/
private gotMediaSource(sourceError: IScreenSourceError | null, selectedSource: ICustomDesktopCapturerSource | undefined): void {
if (sourceError) {
const { requestId, ...error } = sourceError;
this.broadcastMessage('media-source-callback', { requestId, error });
this.broadcastMessage('media-source-callback-v1', { requestId, error });
return;
break;
case apiCmds.registerActivityDetection:
ssf.registerActivityDetection(
data as number,
this.callbackHandlers.onActivityCallback,
);
break;
case apiCmds.registerDownloadHandler:
ssf.registerDownloadHandler(
this.callbackHandlers.onDownloadItemCallback,
);
break;
case apiCmds.openScreenSnippet:
ssf.openScreenSnippet(this.callbackHandlers.onScreenSnippetCallback);
break;
case apiCmds.closeScreenSnippet:
ssf.closeScreenSnippet();
break;
case apiCmds.registerBoundsChange:
ssf.registerBoundsChange(
this.callbackHandlers.onRegisterBoundsChangeCallback,
);
break;
case apiCmds.registerLogger:
ssf.registerLogger(this.callbackHandlers.onRegisterLoggerCallback);
break;
case apiCmds.registerProtocolHandler:
ssf.registerProtocolHandler(
this.callbackHandlers.onRegisterProtocolHandlerCallback,
);
break;
case apiCmds.registerLogRetriever:
ssf.registerLogRetriever(
this.callbackHandlers.onCollectLogsCallback,
data,
);
break;
case apiCmds.sendLogs:
ssf.sendLogs(data.logName, data.logFiles);
break;
case apiCmds.openScreenSharingIndicator:
ssf.openScreenSharingIndicator(
data as IScreenSharingIndicatorOptions,
this.callbackHandlers.onScreenSharingIndicatorCallback,
);
break;
case apiCmds.closeScreenSharingIndicator:
ssf.closeScreenSharingIndicator(data.streamId as string);
break;
case apiCmds.getMediaSource:
await ssf.getMediaSource(
data as ICustomSourcesOptions,
this.callbackHandlers.onMediaSourceCallback,
);
break;
case apiCmds.notification:
notification.showNotification(
data as INotificationData,
this.callbackHandlers.onNotificationCallback,
);
break;
case apiCmds.closeNotification:
await notification.hideNotification(data as number);
break;
case apiCmds.showNotificationSettings:
ssf.showNotificationSettings(data);
break;
case apiCmds.setIsInMeeting:
if (typeof data === 'boolean') {
ssf.setIsInMeeting(data as boolean);
}
if (selectedSource && selectedSource.requestId) {
const { requestId, ...source } = selectedSource;
this.broadcastMessage('media-source-callback', { requestId, source, error: sourceError });
this.broadcastMessage('media-source-callback-v1', { requestId, response: { source, error: sourceError } });
break;
case apiCmds.registerAnalyticsHandler:
ssf.registerAnalyticsEvent(
this.callbackHandlers.onAnalyticsEventCallback,
);
break;
case apiCmds.registerRestartFloater:
ssf.registerRestartFloater(this.callbackHandlers.restartFloater);
break;
case apiCmds.setCloudConfig:
ssf.setCloudConfig(data as object);
break;
case apiCmds.swiftSearch:
if (ssInstance) {
ssInstance.handleMessageEvents(data);
}
break;
case apiCmds.getCPUUsage:
const cpuUsage = await ssf.getCPUUsage();
this.broadcastMessage('get-cpu-usage-callback', {
requestId: data.requestId,
response: cpuUsage,
});
break;
case apiCmds.checkMediaPermission:
const mediaPermission = await ssf.checkMediaPermission();
this.broadcastMessage('check-media-permission-callback', {
requestId: data.requestId,
response: mediaPermission,
});
break;
}
}
/**
* Broadcast user activity
* @param idleTime {number} - system idle tick
*/
private activityCallback = (idleTime: number): void =>
this.broadcastMessage('activity-callback', idleTime);
/**
* Broadcast snippet data
* @param arg {IScreenSnippet}
*/
private screenSnippetCallback = (arg: IScreenSnippet): void =>
this.broadcastMessage('screen-snippet-callback', arg);
/**
* Broadcast bound changes
* @param arg {IBoundsChange}
*/
private registerBoundsChangeCallback = (arg: IBoundsChange): void =>
this.broadcastMessage('bound-changes-callback', arg);
/**
* Broadcast logs
* @param msg {ILogMsg}
* @param logLevel {LogLevel}
* @param showInConsole {boolean}
*/
private registerLoggerCallback(
msg: ILogMsg,
logLevel: LogLevel,
showInConsole: boolean,
): void {
this.broadcastMessage('logger-callback', { msg, logLevel, showInConsole });
}
/**
* Broadcast protocol uri
* @param uri {string}
*/
private protocolHandlerCallback = (uri: string): void =>
this.broadcastMessage('protocol-callback', uri);
private collectLogsCallback = (): void =>
this.broadcastMessage('collect-logs', undefined);
/**
* Broadcast event that stops screen sharing
* @param arg {IScreenSharingIndicator}
*/
private screenSharingIndicatorCallback(arg: IScreenSharingIndicator): void {
this.broadcastMessage('screen-sharing-indicator-callback', arg);
}
/**
* Broadcast analytics events data
* @param arg {IAnalyticsData}
*/
private analyticsEventCallback(arg: IAnalyticsData): void {
this.broadcastMessage('analytics-event-callback', arg);
}
/**
* Broadcast download item event
* @param arg {object}
*/
private onDownloadItemCallback(arg: object): void {
this.broadcastMessage('download-handler-callback', arg);
}
/**
* Broadcast to restart floater event with data
* @param arg {IAnalyticsData}
*/
private restartFloater(arg: IRestartFloaterData): void {
this.broadcastMessage('restart-floater-callback', arg);
}
/**
* Broadcast the user selected source
* @param sourceError {IScreenSourceError}
* @param selectedSource {ICustomDesktopCapturerSource}
*/
private gotMediaSource(
sourceError: IScreenSourceError | null,
selectedSource: ICustomDesktopCapturerSource | undefined,
): void {
if (sourceError) {
const { requestId, ...error } = sourceError;
this.broadcastMessage('media-source-callback', { requestId, error });
this.broadcastMessage('media-source-callback-v1', { requestId, error });
return;
}
/**
* Broadcast notification events
*
* @param event {string}
* @param data {Object}
*/
private notificationCallback(event, data) {
this.broadcastMessage(event, data);
if (selectedSource && selectedSource.requestId) {
const { requestId, ...source } = selectedSource;
this.broadcastMessage('media-source-callback', {
requestId,
source,
error: sourceError,
});
this.broadcastMessage('media-source-callback-v1', {
requestId,
response: { source, error: sourceError },
});
}
}
/**
* Method that broadcast messages to a specific origin via postMessage
*
* @param method {string}
* @param data {any}
*/
private broadcastMessage(method: string, data: any): void {
window.postMessage({ method, data }, this.origin);
}
/**
* Broadcast notification events
*
* @param event {string}
* @param data {Object}
*/
private notificationCallback(event, data) {
this.broadcastMessage(event, data);
}
/**
* Method that broadcast messages to a specific origin via postMessage
*
* @param method {string}
* @param data {any}
*/
private broadcastMessage(method: string, data: any): void {
window.postMessage({ method, data }, this.origin);
}
}
const appBridge = new AppBridge();

View File

@ -2,32 +2,32 @@ import { ipcRenderer, remote } from 'electron';
import * as React from 'react';
import { i18n } from '../../common/i18n-preload';
interface IState {
userConfig: object;
globalConfig: object;
cloudConfig: object;
finalConfig: object;
appName: string;
copyWrite?: string;
clientVersion: string;
buildNumber: string;
hostname: string;
sfeVersion: string;
sfeClientType: string;
versionLocalised?: string;
sdaVersion?: string;
sdaBuildNumber?: string;
electronVersion?: string;
chromeVersion?: string;
v8Version?: string;
nodeVersion?: string;
openSslVersion?: string;
zlibVersion?: string;
uvVersion?: string;
aresVersion?: string;
httpParserVersion?: string;
swiftSearchVersion?: string;
swiftSearchSupportedVersion?: string;
client?: string;
userConfig: object;
globalConfig: object;
cloudConfig: object;
finalConfig: object;
appName: string;
copyWrite?: string;
clientVersion: string;
buildNumber: string;
hostname: string;
sfeVersion: string;
sfeClientType: string;
versionLocalised?: string;
sdaVersion?: string;
sdaBuildNumber?: string;
electronVersion?: string;
chromeVersion?: string;
v8Version?: string;
nodeVersion?: string;
openSslVersion?: string;
zlibVersion?: string;
uvVersion?: string;
aresVersion?: string;
httpParserVersion?: string;
swiftSearchVersion?: string;
swiftSearchSupportedVersion?: string;
client?: string;
}
const ABOUT_SYMPHONY_NAMESPACE = 'AboutSymphony';
@ -36,121 +36,149 @@ const ABOUT_SYMPHONY_NAMESPACE = 'AboutSymphony';
* Window that display app version and copyright info
*/
export default class AboutApp extends React.Component<{}, IState> {
private readonly eventHandlers = {
onCopy: () => this.copy(),
};
private readonly eventHandlers = {
onCopy: () => this.copy(),
constructor(props) {
super(props);
this.state = {
userConfig: {},
globalConfig: {},
cloudConfig: {},
finalConfig: {},
appName: 'Symphony',
versionLocalised: 'Version',
clientVersion: 'N/A',
buildNumber: 'N/A',
hostname: 'N/A',
sfeVersion: 'N/A',
sfeClientType: 'N/A',
sdaVersion: 'N/A',
sdaBuildNumber: 'N/A',
electronVersion: 'N/A',
chromeVersion: 'N/A',
v8Version: 'N/A',
nodeVersion: 'N/A',
openSslVersion: 'N/A',
zlibVersion: 'N/A',
uvVersion: 'N/A',
aresVersion: 'N/A',
httpParserVersion: 'N/A',
swiftSearchVersion: 'N/A',
swiftSearchSupportedVersion: 'N/A',
};
this.updateState = this.updateState.bind(this);
}
constructor(props) {
super(props);
this.state = {
userConfig: {},
globalConfig: {},
cloudConfig: {},
finalConfig: {},
appName: 'Symphony',
versionLocalised: 'Version',
clientVersion: 'N/A',
buildNumber: 'N/A',
hostname: 'N/A',
sfeVersion: 'N/A',
sfeClientType: 'N/A',
sdaVersion: 'N/A',
sdaBuildNumber: 'N/A',
electronVersion: 'N/A',
chromeVersion: 'N/A',
v8Version: 'N/A',
nodeVersion: 'N/A',
openSslVersion: 'N/A',
zlibVersion: 'N/A',
uvVersion: 'N/A',
aresVersion: 'N/A',
httpParserVersion: 'N/A',
swiftSearchVersion: 'N/A',
swiftSearchSupportedVersion: 'N/A',
};
this.updateState = this.updateState.bind(this);
/**
* Renders the component
*/
public render(): JSX.Element {
const {
clientVersion,
buildNumber,
hostname,
sfeVersion,
sfeClientType,
sdaVersion,
sdaBuildNumber,
client,
} = this.state;
const appName = remote.app.getName() || 'Symphony';
const copyright = `\xA9 ${new Date().getFullYear()} ${appName}`;
const podVersion = `${clientVersion} (${buildNumber})`;
const sdaVersionBuild = `${sdaVersion} (${sdaBuildNumber})`;
let sfeClientTypeName = 'SFE';
if (sfeClientType !== '1.5') {
sfeClientTypeName = 'SFE-Lite';
}
/**
* main render function
*/
public render(): JSX.Element {
const { clientVersion, buildNumber, hostname, sfeVersion,
sfeClientType, sdaVersion, sdaBuildNumber, client,
} = this.state;
return (
<div className='AboutApp' lang={i18n.getLocale()}>
<div className='AboutApp-header-container'>
<div className='AboutApp-image-container'>
<img
className='AboutApp-logo'
src='../renderer/assets/symphony-logo.png'
alt={i18n.t('Symphony Logo', ABOUT_SYMPHONY_NAMESPACE)()}
/>
</div>
<div className='AboutApp-header-content'>
<h1 className='AboutApp-name'>{appName}</h1>
<p className='AboutApp-copyrightText'>{copyright}</p>
</div>
</div>
<div className='AboutApp-main-container'>
<section>
<ul className='AboutApp-symphony-section'>
<li>
<b>POD:</b> {hostname || 'N/A'}
</li>
<li>
<b>SBE:</b> {podVersion}
</li>
<li>
<b>SDA:</b> {sdaVersionBuild}
</li>
<li>
<b>{sfeClientTypeName}:</b> {sfeVersion} {client}
</li>
</ul>
</section>
</div>
<div>
<button
className='AboutApp-copy-button'
onClick={this.eventHandlers.onCopy}
title={i18n.t(
'Copy all the version information in this page',
ABOUT_SYMPHONY_NAMESPACE,
)()}
>
{i18n.t('Copy', ABOUT_SYMPHONY_NAMESPACE)()}
</button>
</div>
</div>
);
}
const appName = remote.app.getName() || 'Symphony';
const copyright = `\xA9 ${new Date().getFullYear()} ${appName}`;
const podVersion = `${clientVersion} (${buildNumber})`;
const sdaVersionBuild = `${sdaVersion} (${sdaBuildNumber})`;
let sfeClientTypeName = 'SFE';
if (sfeClientType !== '1.5') {
sfeClientTypeName = 'SFE-Lite';
}
/**
* Callback to handle event when a component is mounted
*/
public componentDidMount(): void {
ipcRenderer.on('about-app-data', this.updateState);
}
return (
<div className='AboutApp' lang={i18n.getLocale()}>
<div className='AboutApp-header-container'>
<div className='AboutApp-image-container'>
<img
className='AboutApp-logo'
src='../renderer/assets/symphony-logo.png'
alt={i18n.t('Symphony Logo', ABOUT_SYMPHONY_NAMESPACE)()}
/>
</div>
<div className='AboutApp-header-content'>
<h1 className='AboutApp-name'>{appName}</h1>
<p className='AboutApp-copyrightText'>{copyright}</p>
</div>
</div>
<div className='AboutApp-main-container'>
<section>
<ul className='AboutApp-symphony-section'>
<li><b>POD:</b> {hostname || 'N/A'}</li>
<li><b>SBE:</b> {podVersion}</li>
<li><b>SDA:</b> {sdaVersionBuild}</li>
<li><b>{sfeClientTypeName}:</b> {sfeVersion} {client}</li>
</ul>
</section>
</div>
<div>
<button
className='AboutApp-copy-button'
onClick={this.eventHandlers.onCopy}
title={i18n.t('Copy all the version information in this page', ABOUT_SYMPHONY_NAMESPACE)()}
>{i18n.t('Copy', ABOUT_SYMPHONY_NAMESPACE)()}</button>
</div>
</div>
);
/**
* Callback to handle event when a component is unmounted
*/
public componentWillUnmount(): void {
ipcRenderer.removeListener('about-app-data', this.updateState);
}
/**
* Copies the version info on to the clipboard
*/
public copy(): void {
const { clientVersion, ...rest } = this.state;
const data = { ...{ sbeVersion: clientVersion }, ...rest };
if (data) {
remote.clipboard.write(
{ text: JSON.stringify(data, null, 4) },
'clipboard',
);
}
}
public componentDidMount(): void {
ipcRenderer.on('about-app-data', this.updateState);
}
public componentWillUnmount(): void {
ipcRenderer.removeListener('about-app-data', this.updateState);
}
/**
* Copies the version info on to the clipboard
*/
public copy(): void {
const { clientVersion, ...rest } = this.state;
const data = { ...{ sbeVersion: clientVersion }, ...rest };
if (data) {
remote.clipboard.write({ text: JSON.stringify(data, null, 4) }, 'clipboard');
}
}
/**
* Sets the component state
*
* @param _event
* @param data {Object} { buildNumber, clientVersion, version }
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
/**
* Sets the component state
*
* @param _event
* @param data {Object} { buildNumber, clientVersion, version }
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
}

View File

@ -1,7 +1,16 @@
import { LazyBrush } from 'lazy-brush';
import * as React from 'react';
import { AnalyticsElements, ScreenSnippetActionTypes } from './../../app/analytics-handler';
import { IDimensions, IPath, IPoint, sendAnalyticsToMain, Tool } from './snipping-tool';
import {
AnalyticsElements,
ScreenSnippetActionTypes,
} from './../../app/analytics-handler';
import {
IDimensions,
IPath,
IPoint,
sendAnalyticsToMain,
Tool,
} from './snipping-tool';
const { useState } = React;
@ -50,7 +59,10 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
updPaths.map((p) => {
if (p && p.key === key) {
p.shouldShow = false;
sendAnalyticsToMain(AnalyticsElements.SCREEN_CAPTURE_ANNOTATE, ScreenSnippetActionTypes.ANNOTATE_ERASED);
sendAnalyticsToMain(
AnalyticsElements.SCREEN_CAPTURE_ANNOTATE,
ScreenSnippetActionTypes.ANNOTATE_ERASED,
);
}
return p;
});
@ -229,10 +241,16 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
const handleMouseUp = () => {
stopDrawing();
if (chosenTool === Tool.pen) {
sendAnalyticsToMain(AnalyticsElements.SCREEN_CAPTURE_ANNOTATE, ScreenSnippetActionTypes.ANNOTATE_ADDED_PEN);
sendAnalyticsToMain(
AnalyticsElements.SCREEN_CAPTURE_ANNOTATE,
ScreenSnippetActionTypes.ANNOTATE_ADDED_PEN,
);
}
if (chosenTool === Tool.highlight) {
sendAnalyticsToMain(AnalyticsElements.SCREEN_CAPTURE_ANNOTATE, ScreenSnippetActionTypes.ANNOTATE_ADDED_HIGHLIGHT);
sendAnalyticsToMain(
AnalyticsElements.SCREEN_CAPTURE_ANNOTATE,
ScreenSnippetActionTypes.ANNOTATE_ADDED_HIGHLIGHT,
);
}
};
@ -248,9 +266,7 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
};
return (
<div
id='annotate-wrapper'
style={getAnnotateWrapperStyle()}>
<div id='annotate-wrapper' style={getAnnotateWrapperStyle()}>
<svg
data-testid='annotate-area'
style={{ cursor: 'crosshair' }}
@ -262,8 +278,7 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
onMouseMove={handleMouseMove}
onMouseLeave={stopDrawing}
>
{
backgroundImagePath &&
{backgroundImagePath && (
<image
x={0}
y={0}
@ -271,11 +286,11 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
xlinkHref={backgroundImagePath}
width={imageDimensions.width}
height={imageDimensions.height}
/>}
/>
)}
{renderPaths(getSvgPathsData(paths))}
</svg>
</div>
);
};

View File

@ -5,10 +5,10 @@ import * as React from 'react';
import { i18n } from '../../common/i18n-preload';
interface IState {
hostname: string;
isValidCredentials: boolean;
password?: string;
username?: string;
hostname: string;
isValidCredentials: boolean;
password?: string;
username?: string;
}
const BASIC_AUTH_NAMESPACE = 'BasicAuth';
@ -17,106 +17,150 @@ const BASIC_AUTH_NAMESPACE = 'BasicAuth';
* Window that display app version and copyright info
*/
export default class BasicAuth extends React.Component<{}, IState> {
private readonly eventHandlers = {
onChange: (event) => this.change(event),
onSubmit: () => this.submit(),
onClose: () => this.close(),
};
private readonly eventHandlers = {
onChange: (event) => this.change(event),
onSubmit: () => this.submit(),
onClose: () => this.close(),
constructor(props) {
super(props);
this.state = {
hostname: 'unknown',
isValidCredentials: true,
};
this.updateState = this.updateState.bind(this);
}
constructor(props) {
super(props);
this.state = {
hostname: 'unknown',
isValidCredentials: true,
};
this.updateState = this.updateState.bind(this);
}
/**
* Callback to handle event when a component is mounted
*/
public componentDidMount(): void {
ipcRenderer.on('basic-auth-data', this.updateState);
}
public componentDidMount(): void {
ipcRenderer.on('basic-auth-data', this.updateState);
}
/**
* Callback to handle event when a component is unmounted
*/
public componentWillUnmount(): void {
ipcRenderer.removeListener('basic-auth-data', this.updateState);
}
public componentWillUnmount(): void {
ipcRenderer.removeListener('basic-auth-data', this.updateState);
}
/**
* main render function
*/
public render(): JSX.Element {
const { hostname, isValidCredentials } = this.state;
const shouldShowError = classNames('credentials-error', { 'display-error': !isValidCredentials });
return (
<div className='container' lang={i18n.getLocale()}>
<span>{i18n.t('Please provide your login credentials for:', BASIC_AUTH_NAMESPACE)()}</span>
<span className='hostname'>{hostname}</span>
<span id='credentialsError' className={shouldShowError}>{i18n.t('Invalid user name/password', BASIC_AUTH_NAMESPACE)()}</span>
<form id='basicAuth' name='Basic Auth' action='Login' onSubmit={this.eventHandlers.onSubmit}>
<table className='form'>
<tbody>
<tr>
<td id='username-text'>{i18n.t('User name:', BASIC_AUTH_NAMESPACE)()}</td>
<td>
<input id='username' name='username' title='Username' onChange={this.eventHandlers.onChange} required />
</td>
</tr>
<tr>
<td id='password-text'>{i18n.t('Password:', BASIC_AUTH_NAMESPACE)()}</td>
<td>
<input name='password' id='password' type='password' title='Password' onChange={this.eventHandlers.onChange} required />
</td>
</tr>
</tbody>
</table>
<div className='footer'>
<div className='button-container'>
<button type='submit' id='login'>{i18n.t('Log In', BASIC_AUTH_NAMESPACE)()}</button>
</div>
<div className='button-container'>
<button type='button' id='cancel' onClick={this.eventHandlers.onClose}>{i18n.t('Cancel', BASIC_AUTH_NAMESPACE)()}</button>
</div>
</div>
</form>
/**
* Renders the component
*/
public render(): JSX.Element {
const { hostname, isValidCredentials } = this.state;
const shouldShowError = classNames('credentials-error', {
'display-error': !isValidCredentials,
});
return (
<div className='container' lang={i18n.getLocale()}>
<span>
{i18n.t(
'Please provide your login credentials for:',
BASIC_AUTH_NAMESPACE,
)()}
</span>
<span className='hostname'>{hostname}</span>
<span id='credentialsError' className={shouldShowError}>
{i18n.t('Invalid user name/password', BASIC_AUTH_NAMESPACE)()}
</span>
<form
id='basicAuth'
name='Basic Auth'
action='Login'
onSubmit={this.eventHandlers.onSubmit}
>
<table className='form'>
<tbody>
<tr>
<td id='username-text'>
{i18n.t('User name:', BASIC_AUTH_NAMESPACE)()}
</td>
<td>
<input
id='username'
name='username'
title='Username'
onChange={this.eventHandlers.onChange}
required
/>
</td>
</tr>
<tr>
<td id='password-text'>
{i18n.t('Password:', BASIC_AUTH_NAMESPACE)()}
</td>
<td>
<input
name='password'
id='password'
type='password'
title='Password'
onChange={this.eventHandlers.onChange}
required
/>
</td>
</tr>
</tbody>
</table>
<div className='footer'>
<div className='button-container'>
<button type='submit' id='login'>
{i18n.t('Log In', BASIC_AUTH_NAMESPACE)()}
</button>
</div>
);
}
<div className='button-container'>
<button
type='button'
id='cancel'
onClick={this.eventHandlers.onClose}
>
{i18n.t('Cancel', BASIC_AUTH_NAMESPACE)()}
</button>
</div>
</div>
</form>
</div>
);
}
/**
* Sets states on input changes
*
* @param event
*/
private change(event): void {
this.setState({
[(event.target as any).id]: (event.target as any).value,
} as IState);
}
/**
* Sets states on input changes
*
* @param event
*/
private change(event): void {
this.setState({
[(event.target as any).id]: (event.target as any).value,
} as IState);
}
/**
* Submits the form with provided username and password info
*/
private submit(): void {
const { username, password } = this.state;
if (username && password) {
ipcRenderer.send('basic-auth-login', { username, password });
}
/**
* Submits the form with provided username and password info
*/
private submit(): void {
const { username, password } = this.state;
if (username && password) {
ipcRenderer.send('basic-auth-login', { username, password });
}
}
/**
* closes the auth window
*/
private close(): void {
ipcRenderer.send('basic-auth-closed', false);
}
/**
* closes the auth window
*/
private close(): void {
ipcRenderer.send('basic-auth-closed', false);
}
/**
* Sets the component state
*
* @param _event
* @param data {Object} { hostname, isValidCredentials }
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
/**
* Sets the component state
*
* @param _event
* @param data {Object} { hostname, isValidCredentials }
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
}

View File

@ -5,89 +5,94 @@ import { i18n } from '../../common/i18n-preload';
const DOWNLOAD_MANAGER_NAMESPACE = 'DownloadManager';
interface IDownloadItem {
_id: string;
fileName: string;
savedPath: string;
total: string;
flashing: boolean;
count: number;
_id: string;
fileName: string;
savedPath: string;
total: string;
flashing: boolean;
count: number;
}
interface IManagerState {
items: IDownloadItem[];
showMainComponent: boolean;
items: IDownloadItem[];
showMainComponent: boolean;
}
export default class DownloadManager {
private readonly eventHandlers = {
onInjectItem: (_event, item: IDownloadItem) => this.injectItem(item),
};
private readonly itemsContainer: HTMLElement | null;
private readonly closeButton: HTMLElement | null;
private readonly eventHandlers = {
onInjectItem: (_event, item: IDownloadItem) => this.injectItem(item),
private domParser: DOMParser;
private state: IManagerState;
constructor() {
this.state = {
items: [],
showMainComponent: false,
};
private readonly itemsContainer: HTMLElement | null;
private readonly closeButton: HTMLElement | null;
this.domParser = new DOMParser();
const parsedDownloadBar = this.domParser.parseFromString(
this.render(),
'text/html',
);
this.itemsContainer = parsedDownloadBar.getElementById('download-main');
this.closeButton = parsedDownloadBar.getElementById('close-download-bar');
private domParser: DOMParser;
private state: IManagerState;
constructor() {
this.state = {
items: [],
showMainComponent: false,
};
this.domParser = new DOMParser();
const parsedDownloadBar = this.domParser.parseFromString(this.render(), 'text/html');
this.itemsContainer = parsedDownloadBar.getElementById('download-main');
this.closeButton = parsedDownloadBar.getElementById('close-download-bar');
if (this.closeButton) {
this.closeButton.addEventListener('click', () => this.close());
}
this.getFileDisplayName = this.getFileDisplayName.bind(this);
if (this.closeButton) {
this.closeButton.addEventListener('click', () => this.close());
}
/**
* initializes the event listeners
*/
public initDownloadManager(): void {
ipcRenderer.on('downloadCompleted', this.eventHandlers.onInjectItem);
}
this.getFileDisplayName = this.getFileDisplayName.bind(this);
}
/**
* Main react render component
*/
public render(): string {
return (`
/**
* initializes the event listeners
*/
public initDownloadManager(): void {
ipcRenderer.on('downloadCompleted', this.eventHandlers.onInjectItem);
}
/**
* Main react render component
*/
public render(): string {
return `
<div id='download-manager' class='download-bar'>
<ul id='download-main' />
<span
id='close-download-bar'
class='close-download-bar tempo-icon tempo-icon--close' />
</div>
`);
}
`;
}
/**
* Toggles footer visibility class based on download items
*/
private showOrHideDownloadBar(): void {
const mainFooter = document.getElementById('footer');
const { items } = this.state;
if (mainFooter) {
items && items.length ? mainFooter.classList.remove('hidden') : mainFooter.classList.add('hidden');
}
/**
* Toggles footer visibility class based on download items
*/
private showOrHideDownloadBar(): void {
const mainFooter = document.getElementById('footer');
const { items } = this.state;
if (mainFooter) {
items && items.length
? mainFooter.classList.remove('hidden')
: mainFooter.classList.add('hidden');
}
}
/**
* Loop through the items downloaded
*
* @param item {IDownloadItem}
*/
private renderItem(item: IDownloadItem): void {
const { _id, total, fileName }: IDownloadItem = item;
const fileDisplayName = this.getFileDisplayName(fileName, item);
const itemContainer = document.getElementById('download-main');
const parsedItem = this.domParser.parseFromString(`
/**
* Loop through the items downloaded
*
* @param item {IDownloadItem}
*/
private renderItem(item: IDownloadItem): void {
const { _id, total, fileName }: IDownloadItem = item;
const fileDisplayName = this.getFileDisplayName(fileName, item);
const itemContainer = document.getElementById('download-main');
const parsedItem = this.domParser.parseFromString(
`
<li id=${_id} class='download-element' title="${fileDisplayName}">
<div class='download-item' id='dl-item'>
<div class='file'>
@ -99,168 +104,191 @@ export default class DownloadManager {
<h1 class='text-cutoff'>
${fileDisplayName}
</h1>
<span id='per' title="${total} ${i18n.t('downloaded', DOWNLOAD_MANAGER_NAMESPACE)()}">
${total} ${i18n.t('downloaded', DOWNLOAD_MANAGER_NAMESPACE)()}
<span id='per' title="${total} ${i18n.t(
'downloaded',
DOWNLOAD_MANAGER_NAMESPACE,
)()}">
${total} ${i18n.t(
'downloaded',
DOWNLOAD_MANAGER_NAMESPACE,
)()}
</span>
</div>
</div>
<div id='menu' class='caret tempo-icon tempo-icon--dropdown'>
<div id='download-action-menu' class='download-action-menu' style="width: 200px">
<ul id={_id}>
<li id='download-open' title="${i18n.t('Open', DOWNLOAD_MANAGER_NAMESPACE)()}">
<li id='download-open' title="${i18n.t(
'Open',
DOWNLOAD_MANAGER_NAMESPACE,
)()}">
${i18n.t('Open', DOWNLOAD_MANAGER_NAMESPACE)()}
</li>
<li id='download-show-in-folder' title="${i18n.t('Show in Folder', DOWNLOAD_MANAGER_NAMESPACE)()}">
${i18n.t('Show in Folder', DOWNLOAD_MANAGER_NAMESPACE)()}
<li id='download-show-in-folder' title="${i18n.t(
'Show in Folder',
DOWNLOAD_MANAGER_NAMESPACE,
)()}">
${i18n.t(
'Show in Folder',
DOWNLOAD_MANAGER_NAMESPACE,
)()}
</li>
</ul>
</div>
</div>
</li>`,
'text/html');
const progress = parsedItem.getElementById('download-progress');
const domItem = parsedItem.getElementById(_id);
'text/html',
);
const progress = parsedItem.getElementById('download-progress');
const domItem = parsedItem.getElementById(_id);
// add event listeners
this.attachEventListener('dl-item', parsedItem, _id);
this.attachEventListener('download-open', parsedItem, _id);
this.attachEventListener('download-show-in-folder', parsedItem, _id);
// add event listeners
this.attachEventListener('dl-item', parsedItem, _id);
this.attachEventListener('download-open', parsedItem, _id);
this.attachEventListener('download-show-in-folder', parsedItem, _id);
if (itemContainer && domItem) {
itemContainer.prepend(domItem);
}
setTimeout(() => {
if (progress) {
progress.classList.remove('flash');
}
}, 4000);
if (itemContainer && domItem) {
itemContainer.prepend(domItem);
}
setTimeout(() => {
if (progress) {
progress.classList.remove('flash');
}
}, 4000);
}
/**
* Inject items to global var
*
* @param args {IDownloadItem}
*/
private injectItem(args: IDownloadItem): void {
const { items } = this.state;
let itemCount = 0;
for (const item of items) {
if (args.fileName === item.fileName) {
itemCount++;
}
}
args.count = itemCount;
const newItem = { ...args, ...{ flashing: true } };
const allItems = [...items, ...[newItem]];
this.state = { items: allItems, showMainComponent: true };
// inserts download bar once
const downloadBar = document.getElementById('download-manager-footer');
if (this.itemsContainer && this.closeButton) {
this.showOrHideDownloadBar();
if (downloadBar) {
downloadBar.appendChild(this.itemsContainer);
downloadBar.appendChild(this.closeButton);
}
}
/**
* Inject items to global var
*
* @param args {IDownloadItem}
*/
private injectItem(args: IDownloadItem): void {
const { items } = this.state;
let itemCount = 0;
for (const item of items) {
if (args.fileName === item.fileName) {
itemCount++;
}
}
args.count = itemCount;
const newItem = { ...args, ...{ flashing: true } };
const allItems = [ ...items, ...[ newItem ] ];
this.state = { items: allItems, showMainComponent: true };
// appends items to the download bar
this.renderItem(newItem);
}
// inserts download bar once
const downloadBar = document.getElementById('download-manager-footer');
if (this.itemsContainer && this.closeButton) {
this.showOrHideDownloadBar();
if (downloadBar) {
downloadBar.appendChild(this.itemsContainer);
downloadBar.appendChild(this.closeButton);
}
}
// appends items to the download bar
this.renderItem(newItem);
/**
* adds event listener for the give id
*
* @param id {String}
* @param item {Document}
* @param itemId {String}
*/
private attachEventListener(
id: string,
item: Document,
itemId: string,
): void {
if (!item) {
return;
}
/**
* adds event listener for the give id
*
* @param id {String}
* @param item {Document}
* @param itemId {String}
*/
private attachEventListener(id: string, item: Document, itemId: string): void {
if (!item) {
return;
}
const element = item.getElementById(id);
if (element) {
switch (id) {
case 'dl-item':
case 'download-open':
element.addEventListener('click', () => this.openFile(itemId));
break;
case 'download-show-in-folder':
element.addEventListener('click', () => this.showInFinder(itemId));
}
}
const element = item.getElementById(id);
if (element) {
switch (id) {
case 'dl-item':
case 'download-open':
element.addEventListener('click', () => this.openFile(itemId));
break;
case 'download-show-in-folder':
element.addEventListener('click', () => this.showInFinder(itemId));
}
}
}
/**
* Show or hide main footer which comes from the client
*/
private close(): void {
this.state = {
showMainComponent: !this.state.showMainComponent,
items: [],
};
if (this.itemsContainer) {
this.itemsContainer.innerHTML = '';
}
this.showOrHideDownloadBar();
/**
* Show or hide main footer which comes from the client
*/
private close(): void {
this.state = {
showMainComponent: !this.state.showMainComponent,
items: [],
};
if (this.itemsContainer) {
this.itemsContainer.innerHTML = '';
}
this.showOrHideDownloadBar();
}
/**
* Opens the downloaded file
*
* @param id {string}
*/
private openFile(id: string): void {
const { items } = this.state;
const fileIndex = items.findIndex((item) => {
return item._id === id;
});
/**
* Opens the downloaded file
*
* @param id {string}
*/
private openFile(id: string): void {
const { items } = this.state;
const fileIndex = items.findIndex((item) => {
return item._id === id;
});
if (fileIndex !== -1) {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.downloadManagerAction,
path: items[ fileIndex ].savedPath,
type: 'open',
});
}
if (fileIndex !== -1) {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.downloadManagerAction,
path: items[fileIndex].savedPath,
type: 'open',
});
}
}
/**
* Opens the downloaded file in finder/explorer
*
* @param id {string}
*/
private showInFinder(id: string): void {
const { items } = this.state;
const fileIndex = items.findIndex((item) => {
return item._id === id;
});
/**
* Opens the downloaded file in finder/explorer
*
* @param id {string}
*/
private showInFinder(id: string): void {
const { items } = this.state;
const fileIndex = items.findIndex((item) => {
return item._id === id;
});
if (fileIndex !== -1) {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.downloadManagerAction,
path: items[ fileIndex ].savedPath,
type: 'show',
});
}
if (fileIndex !== -1) {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.downloadManagerAction,
path: items[fileIndex].savedPath,
type: 'show',
});
}
}
/**
* Checks and constructs file name
*
* @param fileName {String}
* @param item {IDownloadItem}
*/
private getFileDisplayName(fileName: string, item: IDownloadItem): string {
/* If it exists, add a count to the name like how Chrome does */
if (item.count > 0) {
const extLastIndex = fileName.lastIndexOf('.');
const fileCount = ' (' + item.count + ')';
/**
* Checks and constructs file name
*
* @param fileName {String}
* @param item {IDownloadItem}
*/
private getFileDisplayName(fileName: string, item: IDownloadItem): string {
/* If it exists, add a count to the name like how Chrome does */
if (item.count > 0) {
const extLastIndex = fileName.lastIndexOf('.');
const fileCount = ' (' + item.count + ')';
fileName = fileName.slice(0, extLastIndex) + fileCount + fileName.slice(extLastIndex);
}
return fileName;
fileName =
fileName.slice(0, extLastIndex) +
fileCount +
fileName.slice(extLastIndex);
}
return fileName;
}
}

View File

@ -4,112 +4,156 @@ import * as React from 'react';
import { i18n } from '../../common/i18n-preload';
interface IState {
error: ERROR | string;
error: ERROR | string;
}
enum ERROR {
NETWORK_OFFLINE = 'NETWORK_OFFLINE',
NETWORK_OFFLINE = 'NETWORK_OFFLINE',
}
/**
* Window that display app version and copyright info
*/
export default class LoadingScreen extends React.Component<{}, IState> {
private readonly eventHandlers = {
onRetry: () => this.retry(),
onQuit: () => this.quit(),
};
private readonly eventHandlers = {
onRetry: () => this.retry(),
onQuit: () => this.quit(),
constructor(props) {
super(props);
this.state = {
error: '',
};
this.updateState = this.updateState.bind(this);
}
constructor(props) {
super(props);
this.state = {
error: '',
};
this.updateState = this.updateState.bind(this);
/**
* Callback to handle event when a component is mounted
*/
public componentDidMount(): void {
ipcRenderer.on('loading-screen-data', this.updateState);
}
/**
* Callback to handle event when a component is unmounted
*/
public componentWillUnmount(): void {
ipcRenderer.removeListener('loading-screen-data', this.updateState);
}
/**
* Renders the component
*/
public render(): JSX.Element {
const { error } = this.state;
const appName = remote.app.getName() || 'Symphony';
if (error) {
return this.renderErrorContent(error);
}
public componentDidMount(): void {
ipcRenderer.on('loading-screen-data', this.updateState);
return (
<div className='LoadingScreen'>
<img className='LoadingScreen-logo' src={this.getImage(error)} />
<span className='LoadingScreen-name'>{appName}</span>
<svg
width='100%'
height='100%'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 100 200'
preserveAspectRatio='xMidYMid'
>
<circle
cx='50'
cy='50'
fill='none'
ng-attr-stroke='{{config.color}}'
stroke='#ffffff'
strokeWidth='10'
r='35'
strokeDasharray='164.93361431346415 56.97787143782138'
transform='rotate(59.6808 50 50)'
>
<animateTransform
attributeName='transform'
type='rotate'
calcMode='linear'
values='0 50 50;360 50 50'
keyTimes='0;1'
dur='1s'
begin='0s'
repeatCount='indefinite'
/>
</circle>
</svg>
</div>
);
}
/**
* Returns the appropriate image path based on the error
*/
private getImage(error: string): string {
if (error) {
return '../renderer/assets/offline_logo.png';
}
public componentWillUnmount(): void {
ipcRenderer.removeListener('loading-screen-data', this.updateState);
}
return '../renderer/assets/symphony-logo.png';
}
/**
* main render function
*/
public render(): JSX.Element {
const { error } = this.state;
const appName = remote.app.getName() || 'Symphony';
/**
* Renders the error or lading icon
*/
private renderErrorContent(error: string): JSX.Element {
return (
<div className='LoadingScreen'>
<img className='LoadingScreen-logo' src={this.getImage(error)} />
<span className='LoadingScreen-name'>
{i18n.t('Problem connecting to Symphony')()}
</span>
<div id='error-code' className='LoadingScreen-error-code'>
{error}
</div>
<div>
<button
onClick={this.eventHandlers.onRetry}
className='LoadingScreen-button'
>
{i18n.t('Try now')()}
</button>
<button
onClick={this.eventHandlers.onQuit}
className='LoadingScreen-button'
>
{i18n.t('Quit Symphony')()}
</button>
</div>
</div>
);
}
if (error) {
return this.renderErrorContent(error);
}
/**
* reloads the application
*/
private retry(): void {
ipcRenderer.send('reload-symphony');
}
return (
<div className='LoadingScreen'>
<img className='LoadingScreen-logo' src={this.getImage(error)} />
<span className='LoadingScreen-name'>{appName}</span>
<svg width='100%' height='100%' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 200' preserveAspectRatio='xMidYMid'>
<circle cx='50' cy='50' fill='none' ng-attr-stroke='{{config.color}}' stroke='#ffffff' strokeWidth='10' r='35' strokeDasharray='164.93361431346415 56.97787143782138' transform='rotate(59.6808 50 50)'>
<animateTransform attributeName='transform' type='rotate' calcMode='linear' values='0 50 50;360 50 50' keyTimes='0;1' dur='1s' begin='0s' repeatCount='indefinite' />
</circle>
</svg>
</div>
);
}
/**
* quits the application
*/
private quit(): void {
ipcRenderer.send('quit-symphony');
}
/**
* Returns the appropriate image path based on the error
*/
private getImage(error: string): string {
if (error) {
return '../renderer/assets/offline_logo.png';
}
return '../renderer/assets/symphony-logo.png';
}
/**
* Renders the error or lading icon
*/
private renderErrorContent(error: string): JSX.Element {
return (
<div className='LoadingScreen'>
<img className='LoadingScreen-logo' src={this.getImage(error)} />
<span className='LoadingScreen-name'>{i18n.t('Problem connecting to Symphony')()}</span>
<div id='error-code' className='LoadingScreen-error-code'>{error}</div>
<div>
<button onClick={this.eventHandlers.onRetry} className='LoadingScreen-button'>{i18n.t('Try now')()}</button>
<button onClick={this.eventHandlers.onQuit} className='LoadingScreen-button'>{i18n.t('Quit Symphony')()}</button>
</div>
</div>
);
}
/**
* reloads the application
*/
private retry(): void {
ipcRenderer.send('reload-symphony');
}
/**
* quits the application
*/
private quit(): void {
ipcRenderer.send('quit-symphony');
}
/**
* Sets the component state
*
* @param _event
* @param data {Object}
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
/**
* Sets the component state
*
* @param _event
* @param data {Object}
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
}

View File

@ -8,138 +8,155 @@ const onlineStateInterval = 5 * 1000;
let onlineStateIntervalId;
export default class MessageBanner {
private readonly body: HTMLCollectionOf<Element> | undefined;
private banner: HTMLElement | null = null;
private closeButton: HTMLElement | null = null;
private retryButton: HTMLElement | null = null;
private domParser: DOMParser | undefined;
private url: string | undefined;
private readonly body: HTMLCollectionOf<Element> | undefined;
private banner: HTMLElement | null = null;
private closeButton: HTMLElement | null = null;
private retryButton: HTMLElement | null = null;
private domParser: DOMParser | undefined;
private url: string | undefined;
constructor() {
this.body = document.getElementsByTagName('body');
window.addEventListener('beforeunload', () => {
if (onlineStateIntervalId) {
clearInterval(onlineStateIntervalId);
onlineStateIntervalId = null;
}
}, false);
}
/**
* initializes red banner
*/
public initBanner(): void {
this.domParser = new DOMParser();
const banner = this.domParser.parseFromString(this.render(), 'text/html');
this.closeButton = banner.getElementById('banner-close');
if (this.closeButton) {
this.closeButton.addEventListener('click', this.removeBanner);
}
this.retryButton = banner.getElementById('banner-retry');
if (this.retryButton) {
this.retryButton.addEventListener('click', this.reload);
}
this.banner = banner.getElementById('sda-banner');
}
/**
* Injects SDA banner into DOM
*
* @param show {boolean}
* @param type {bannerTypes}
* @param url {string} - POD URL from global config file
*/
public showBanner(show: boolean, type: bannerTypes, url?: string): void {
this.url = url;
if (this.body && this.body.length > 0 && this.banner) {
this.body[ 0 ].appendChild(this.banner);
if (show) {
this.banner.classList.add('sda-banner-show');
this.monitorOnlineState();
}
switch (type) {
case 'error':
this.banner.classList.add('sda-banner-error');
break;
case 'warning':
this.banner.classList.add('sda-banner-warning');
default:
break;
}
}
}
/**
* removes the message banner
*/
public removeBanner(): void {
const banner = document.getElementById('sda-banner');
if (banner) {
banner.classList.remove('sda-banner-show');
}
constructor() {
this.body = document.getElementsByTagName('body');
window.addEventListener(
'beforeunload',
() => {
if (onlineStateIntervalId) {
clearInterval(onlineStateIntervalId);
onlineStateIntervalId = null;
}
},
false,
);
}
/**
* initializes red banner
*/
public initBanner(): void {
this.domParser = new DOMParser();
const banner = this.domParser.parseFromString(this.render(), 'text/html');
this.closeButton = banner.getElementById('banner-close');
if (this.closeButton) {
this.closeButton.addEventListener('click', this.removeBanner);
}
this.retryButton = banner.getElementById('banner-retry');
if (this.retryButton) {
this.retryButton.addEventListener('click', this.reload);
}
this.banner = banner.getElementById('sda-banner');
}
/**
* Injects SDA banner into DOM
*
* @param show {boolean}
* @param type {bannerTypes}
* @param url {string} - POD URL from global config file
*/
public showBanner(show: boolean, type: bannerTypes, url?: string): void {
this.url = url;
if (this.body && this.body.length > 0 && this.banner) {
this.body[0].appendChild(this.banner);
if (show) {
this.banner.classList.add('sda-banner-show');
this.monitorOnlineState();
}
switch (type) {
case 'error':
this.banner.classList.add('sda-banner-error');
break;
case 'warning':
this.banner.classList.add('sda-banner-warning');
default:
break;
}
}
}
/**
* removes the message banner
*/
public removeBanner(): void {
const banner = document.getElementById('sda-banner');
if (banner) {
banner.classList.remove('sda-banner-show');
}
if (onlineStateIntervalId) {
clearInterval(onlineStateIntervalId);
onlineStateIntervalId = null;
}
}
/**
* Monitors network online status and updates the banner
*/
public monitorOnlineState(): void {
if (onlineStateIntervalId) {
return;
}
onlineStateIntervalId = setInterval(async () => {
try {
const response = await window.fetch(this.url || window.location.href, {
cache: 'no-cache',
keepalive: false,
});
if (
window.navigator.onLine &&
(response.status === 200 || response.ok)
) {
if (this.banner) {
this.banner.classList.remove('sda-banner-show');
}
if (onlineStateIntervalId) {
clearInterval(onlineStateIntervalId);
onlineStateIntervalId = null;
}
this.reload();
}
// tslint:disable-next-line:no-empty
} catch (e) {}
}, onlineStateInterval);
}
/**
* reloads the web page
*/
public reload(): void {
if (document.location) {
document.location.reload();
}
}
/**
* Monitors network online status and updates the banner
*/
public monitorOnlineState(): void {
if (onlineStateIntervalId) {
return;
}
onlineStateIntervalId = setInterval(async () => {
try {
const response = await window.fetch(this.url || window.location.href, { cache: 'no-cache', keepalive: false });
if (window.navigator.onLine && (response.status === 200 || response.ok)) {
if (this.banner) {
this.banner.classList.remove('sda-banner-show');
}
if (onlineStateIntervalId) {
clearInterval(onlineStateIntervalId);
onlineStateIntervalId = null;
}
this.reload();
}
// tslint:disable-next-line:no-empty
} catch (e) {}
}, onlineStateInterval);
}
/**
* reloads the web page
*/
public reload(): void {
if (document.location) {
document.location.reload();
}
}
/**
* Renders a message banner
*/
public render(): string {
return `
/**
* Renders a message banner
*/
public render(): string {
return `
<div id='sda-banner' class='sda-banner'>
<span class='sda-banner-icon'></span>
<span class='sda-banner-message'>
${i18n.t('Connection lost. This message will disappear once the connection is restored.', BANNER_NAME_SPACE)()}
${i18n.t(
'Connection lost. This message will disappear once the connection is restored.',
BANNER_NAME_SPACE,
)()}
</span>
<span id='banner-retry' class='sda-banner-retry-button' title='${i18n.t('Retry Now', BANNER_NAME_SPACE)()}'>
<span id='banner-retry' class='sda-banner-retry-button' title='${i18n.t(
'Retry Now',
BANNER_NAME_SPACE,
)()}'>
${i18n.t('Retry Now', BANNER_NAME_SPACE)()}
</span>
<span id='banner-close' class='sda-banner-close-icon' title='${i18n.t('Close')()}'></span>
<span id='banner-close' class='sda-banner-close-icon' title='${i18n.t(
'Close',
)()}'></span>
</div>
`;
}
}
}

View File

@ -4,11 +4,11 @@ import * as React from 'react';
import { i18n } from '../../common/i18n-preload';
interface IProps {
error: ERROR | string;
error: ERROR | string;
}
enum ERROR {
NETWORK_OFFLINE = 'NETWORK_OFFLINE',
NETWORK_OFFLINE = 'NETWORK_OFFLINE',
}
const NETWORK_ERROR_NAMESPACE = 'NetworkError';
@ -17,69 +17,78 @@ const NETWORK_ERROR_NAMESPACE = 'NetworkError';
* Window that display app version and copyright info
*/
export default class NetworkError extends React.Component<IProps, {}> {
private readonly eventHandlers = {
onRetry: () => this.retry(),
};
private readonly eventHandlers = {
onRetry: () => this.retry(),
};
constructor(props) {
super(props);
}
constructor(props) {
super(props);
}
/**
* main render function
*/
public render(): JSX.Element {
const { error } = this.props;
return (
<div id='main-content'>
<div className='NetworkError-icon'>
<svg width='60' viewBox='0 0 50 60' fill='none'>
<path
d='M40 20.111V9.653c0-2.142-1.1-4.153-2.883-5.255C34.458 2.754 28.7 0 20 0 11.3 0 5.542 2.754 2.883 4.407 1.1 5.5 0 7.511 0 9.653v15.705l31.667 9.618v6.995c0 .945-.567 1.61-1.534 2.108L20 49.404 9.808 44.052c-.908-.472-1.475-1.136-1.475-2.08v-5.247L0 34.102v7.87c0 4.319 2.358 7.991 6.108 9.906L20 59.46l13.833-7.546C37.642 49.963 40 46.291 40 41.971V28.855L8.333 19.237v-7.983C10.6 10.108 14.45 8.744 20 8.744s9.4 1.364 11.667 2.51v6.234L40 20.111z'
fill='#0098FF'
/>
<path
d='M40 20.111V9.653c0-2.142-1.1-4.153-2.883-5.255C34.458 2.754 28.7 0 20 0 11.3 0 5.542 2.754 2.883 4.407 1.1 5.5 0 7.511 0 9.653v15.705l31.667 9.618v6.995c0 .945-.567 1.61-1.534 2.108L20 49.404 9.808 44.052c-.908-.472-1.475-1.136-1.475-2.08v-5.247L0 34.102v7.87c0 4.319 2.358 7.991 6.108 9.906L20 59.46l13.833-7.546C37.642 49.963 40 46.291 40 41.971V28.855L8.333 19.237v-7.983C10.6 10.108 14.45 8.744 20 8.744s9.4 1.364 11.667 2.51v6.234L40 20.111z'
fill='url(#prefix__paint0_radial)'
/>
<defs>
<radialGradient
id='prefix__paint0_radial'
cx={0}
cy={0}
r={1}
gradientUnits='userSpaceOnUse'
gradientTransform='matrix(0 40.259 -50.3704 0 20.07 0)'>
<stop stopColor='#fff' stopOpacity={0.4} />
<stop offset={1} stopColor='#fff' stopOpacity={0} />
</radialGradient>
</defs>
</svg>
</div>
<div className='main-message' lang={i18n.getLocale()}>
<p className='NetworkError-header'>
{i18n.t('Problem connecting to Symphony', NETWORK_ERROR_NAMESPACE)()}
</p>
<p id='NetworkError-reason'>
{i18n.t(`Looks like you are not connected to the Internet. We'll try to reconnect automatically.`, NETWORK_ERROR_NAMESPACE)()}
</p>
<div id='error-code' className='NetworkError-error-code'>
{error || ERROR.NETWORK_OFFLINE}
</div>
<button id='retry-button' onClick={this.eventHandlers.onRetry} className='NetworkError-button'>
{i18n.t('Retry', NETWORK_ERROR_NAMESPACE)()}
</button>
</div>
</div>
);
}
/**
* reloads the application
*/
private retry(): void {
ipcRenderer.send('reload-symphony');
}
/**
* main render function
*/
public render(): JSX.Element {
const { error } = this.props;
return (
<div id='main-content'>
<div className='NetworkError-icon'>
<svg width='60' viewBox='0 0 50 60' fill='none'>
<path
d='M40 20.111V9.653c0-2.142-1.1-4.153-2.883-5.255C34.458 2.754 28.7 0 20 0 11.3 0 5.542 2.754 2.883 4.407 1.1 5.5 0 7.511 0 9.653v15.705l31.667 9.618v6.995c0 .945-.567 1.61-1.534 2.108L20 49.404 9.808 44.052c-.908-.472-1.475-1.136-1.475-2.08v-5.247L0 34.102v7.87c0 4.319 2.358 7.991 6.108 9.906L20 59.46l13.833-7.546C37.642 49.963 40 46.291 40 41.971V28.855L8.333 19.237v-7.983C10.6 10.108 14.45 8.744 20 8.744s9.4 1.364 11.667 2.51v6.234L40 20.111z'
fill='#0098FF'
/>
<path
d='M40 20.111V9.653c0-2.142-1.1-4.153-2.883-5.255C34.458 2.754 28.7 0 20 0 11.3 0 5.542 2.754 2.883 4.407 1.1 5.5 0 7.511 0 9.653v15.705l31.667 9.618v6.995c0 .945-.567 1.61-1.534 2.108L20 49.404 9.808 44.052c-.908-.472-1.475-1.136-1.475-2.08v-5.247L0 34.102v7.87c0 4.319 2.358 7.991 6.108 9.906L20 59.46l13.833-7.546C37.642 49.963 40 46.291 40 41.971V28.855L8.333 19.237v-7.983C10.6 10.108 14.45 8.744 20 8.744s9.4 1.364 11.667 2.51v6.234L40 20.111z'
fill='url(#prefix__paint0_radial)'
/>
<defs>
<radialGradient
id='prefix__paint0_radial'
cx={0}
cy={0}
r={1}
gradientUnits='userSpaceOnUse'
gradientTransform='matrix(0 40.259 -50.3704 0 20.07 0)'
>
<stop stopColor='#fff' stopOpacity={0.4} />
<stop offset={1} stopColor='#fff' stopOpacity={0} />
</radialGradient>
</defs>
</svg>
</div>
<div className='main-message' lang={i18n.getLocale()}>
<p className='NetworkError-header'>
{i18n.t(
'Problem connecting to Symphony',
NETWORK_ERROR_NAMESPACE,
)()}
</p>
<p id='NetworkError-reason'>
{i18n.t(
`Looks like you are not connected to the Internet. We'll try to reconnect automatically.`,
NETWORK_ERROR_NAMESPACE,
)()}
</p>
<div id='error-code' className='NetworkError-error-code'>
{error || ERROR.NETWORK_OFFLINE}
</div>
<button
id='retry-button'
onClick={this.eventHandlers.onRetry}
className='NetworkError-button'
>
{i18n.t('Retry', NETWORK_ERROR_NAMESPACE)()}
</button>
</div>
</div>
);
}
/**
* reloads the application
*/
private retry(): void {
ipcRenderer.send('reload-symphony');
}
}

View File

@ -4,28 +4,40 @@ import * as React from 'react';
import { i18n } from '../../common/i18n-preload';
const whiteColorRegExp = new RegExp(/^(?:white|#fff(?:fff)?|rgba?\(\s*255\s*,\s*255\s*,\s*255\s*(?:,\s*1\s*)?\))$/i);
const darkTheme = [ '#e23030', '#b5616a', '#ab8ead', '#ebc875', '#a3be77', '#58c6ff', '#ebab58' ];
const whiteColorRegExp = new RegExp(
/^(?:white|#fff(?:fff)?|rgba?\(\s*255\s*,\s*255\s*,\s*255\s*(?:,\s*1\s*)?\))$/i,
);
const darkTheme = [
'#e23030',
'#b5616a',
'#ab8ead',
'#ebc875',
'#a3be77',
'#58c6ff',
'#ebab58',
];
type Theme = '' | 'light' | 'dark';
interface IState {
title: string;
company: string;
body: string;
image: string;
icon: string;
id: number;
color: string;
flash: boolean;
isExternal: boolean;
theme: Theme;
hasReply: boolean;
isInputHidden: boolean;
containerHeight: number;
canSendMessage: boolean;
title: string;
company: string;
body: string;
image: string;
icon: string;
id: number;
color: string;
flash: boolean;
isExternal: boolean;
theme: Theme;
hasReply: boolean;
isInputHidden: boolean;
containerHeight: number;
canSendMessage: boolean;
}
type mouseEventButton = React.MouseEvent<HTMLDivElement> | React.MouseEvent<HTMLButtonElement>;
type mouseEventButton =
| React.MouseEvent<HTMLDivElement>
| React.MouseEvent<HTMLButtonElement>;
type keyboardEvent = React.KeyboardEvent<HTMLInputElement>;
// Notification container height
@ -33,389 +45,432 @@ const CONTAINER_HEIGHT = 100;
const CONTAINER_HEIGHT_WITH_INPUT = 132;
export default class NotificationComp extends React.Component<{}, IState> {
private readonly eventHandlers = {
onClose: (winKey) => (_event: mouseEventButton) => this.close(winKey),
onClick: (data) => (_event: mouseEventButton) => this.click(data),
onContextMenu: (event) => this.contextMenu(event),
onMouseEnter: (winKey) => (_event: mouseEventButton) =>
this.onMouseEnter(winKey),
onMouseLeave: (winKey) => (_event: mouseEventButton) =>
this.onMouseLeave(winKey),
onOpenReply: (winKey) => (event: mouseEventButton) =>
this.onOpenReply(event, winKey),
onThumbsUp: () => (_event: mouseEventButton) => this.onThumbsUp(),
onReply: (winKey) => (_event: mouseEventButton) => this.onReply(winKey),
onKeyUp: (winKey) => (event: keyboardEvent) => this.onKeyUp(event, winKey),
};
private flashTimer: NodeJS.Timer | undefined;
private customInput: React.RefObject<HTMLSpanElement>;
private inputCaret: React.RefObject<HTMLDivElement>;
private input: React.RefObject<HTMLInputElement>;
private readonly eventHandlers = {
onClose: (winKey) => (_event: mouseEventButton) => this.close(winKey),
onClick: (data) => (_event: mouseEventButton) => this.click(data),
onContextMenu: (event) => this.contextMenu(event),
onMouseEnter: (winKey) => (_event: mouseEventButton) => this.onMouseEnter(winKey),
onMouseLeave: (winKey) => (_event: mouseEventButton) => this.onMouseLeave(winKey),
onOpenReply: (winKey) => (event: mouseEventButton) => this.onOpenReply(event, winKey),
onThumbsUp: () => (_event: mouseEventButton) => this.onThumbsUp(),
onReply: (winKey) => (_event: mouseEventButton) => this.onReply(winKey),
onKeyUp: (winKey) => (event: keyboardEvent) => this.onKeyUp(event, winKey),
constructor(props) {
super(props);
this.state = {
title: '',
company: 'Symphony',
body: '',
image: '',
icon: '',
id: 0,
color: '',
flash: false,
isExternal: false,
theme: '',
isInputHidden: true,
hasReply: false,
containerHeight: CONTAINER_HEIGHT,
canSendMessage: false,
};
private flashTimer: NodeJS.Timer | undefined;
private customInput: React.RefObject<HTMLSpanElement>;
private inputCaret: React.RefObject<HTMLDivElement>;
private input: React.RefObject<HTMLInputElement>;
this.updateState = this.updateState.bind(this);
this.setInputCaretPosition = this.setInputCaretPosition.bind(this);
this.resetNotificationData = this.resetNotificationData.bind(this);
this.getInputValue = this.getInputValue.bind(this);
constructor(props) {
super(props);
this.state = {
title: '',
company: 'Symphony',
body: '',
image: '',
icon: '',
id: 0,
color: '',
flash: false,
isExternal: false,
theme: '',
isInputHidden: true,
hasReply: false,
containerHeight: CONTAINER_HEIGHT,
canSendMessage: false,
};
this.updateState = this.updateState.bind(this);
this.setInputCaretPosition = this.setInputCaretPosition.bind(this);
this.resetNotificationData = this.resetNotificationData.bind(this);
this.getInputValue = this.getInputValue.bind(this);
this.customInput = React.createRef();
this.inputCaret = React.createRef();
this.input = React.createRef();
}
this.customInput = React.createRef();
this.inputCaret = React.createRef();
this.input = React.createRef();
/**
* Callback to handle event when a component is mounted
*/
public componentDidMount(): void {
ipcRenderer.on('notification-data', this.updateState);
}
/**
* Callback to handle event when a component is unmounted
*/
public componentWillUnmount(): void {
ipcRenderer.removeListener('notification-data', this.updateState);
this.clearFlashInterval();
}
/**
* Renders the component
*/
public render(): JSX.Element {
const {
title,
body,
id,
color,
isExternal,
theme,
isInputHidden,
containerHeight,
hasReply,
canSendMessage,
} = this.state;
let themeClassName;
if (theme) {
themeClassName = theme;
} else if (darkTheme.includes(color.toLowerCase())) {
themeClassName = 'blackText';
} else {
themeClassName =
color && color.match(whiteColorRegExp) ? 'light' : 'dark';
}
public componentDidMount(): void {
ipcRenderer.on('notification-data', this.updateState);
}
const bgColor = { backgroundColor: color || '#ffffff' };
const containerClass = classNames('container', {
'external-border': isExternal,
});
public componentWillUnmount(): void {
ipcRenderer.removeListener('notification-data', this.updateState);
this.clearFlashInterval();
}
/**
* Renders the custom title bar
*/
public render(): JSX.Element {
const { title, body, id, color, isExternal, theme, isInputHidden, containerHeight, hasReply, canSendMessage } = this.state;
let themeClassName;
if (theme) {
themeClassName = theme;
} else if (darkTheme.includes(color.toLowerCase())) {
themeClassName = 'blackText';
} else {
themeClassName = color && color.match(whiteColorRegExp) ? 'light' : 'dark';
}
const bgColor = { backgroundColor: color || '#ffffff' };
const containerClass = classNames('container', { 'external-border': isExternal });
return (
<div className={containerClass} style={{ height: containerHeight }} lang={i18n.getLocale()}>
<div
className='main-container'
role='alert'
style={bgColor}
onContextMenu={this.eventHandlers.onContextMenu}
onClick={this.eventHandlers.onClick(id)}
onMouseEnter={this.eventHandlers.onMouseEnter(id)}
onMouseLeave={this.eventHandlers.onMouseLeave(id)}
>
<div className='logo-container'>
<div className='logo'>
<img src='../renderer/assets/notification-symphony-logo.svg' alt='Symphony logo'/>
</div>
</div>
<div className='header'>
<div className='title-container'>
<span className={`title ${themeClassName}`}>{title}</span>
{this.renderExtBadge(isExternal)}
</div>
<span className={`message ${themeClassName}`}>{body}</span>
</div>
<div className='actions-container'>
<button
className={`action-button ${themeClassName}`}
title={i18n.t('Close')()}
onClick={this.eventHandlers.onClose(id)}
>
{i18n.t('Close')()}
</button>
<button
className={`action-button ${themeClassName}`}
style={{ visibility: hasReply ? 'visible' : 'hidden' }}
title={i18n.t('Reply')()}
onClick={this.eventHandlers.onOpenReply(id)}
>
{i18n.t('Reply')()}
</button>
</div>
</div>
<div style={{
...{ display: isInputHidden ? 'none' : 'flex' },
...bgColor,
}} className='rte-container'>
<div className='input-container'>
<div className='input-border'/>
<div className='input-caret-container'>
<span ref={this.customInput} className='custom-input'/>
</div>
<div ref={this.inputCaret} className='input-caret'/>
<input
style={bgColor}
className={themeClassName}
autoFocus={true}
onInput={this.setInputCaretPosition}
onKeyDown={this.setInputCaretPosition}
onKeyUp={this.eventHandlers.onKeyUp(id)}
onChange={this.setInputCaretPosition}
onClick={this.setInputCaretPosition}
onPaste={this.setInputCaretPosition}
onCut={this.setInputCaretPosition}
onCopy={this.setInputCaretPosition}
onMouseDown={this.setInputCaretPosition}
onMouseUp={this.setInputCaretPosition}
onFocus={() => this.animateCaret(true)}
onBlur={() => this.animateCaret(false)}
ref={this.input}/>
</div>
<div className='rte-button-container'>
<button
className={`rte-thumbsup-button ${themeClassName}`}
onClick={this.eventHandlers.onThumbsUp()}
>👍</button>
<button
className={`rte-send-button ${themeClassName}`}
onClick={this.eventHandlers.onReply(id)}
disabled={!canSendMessage}
title={i18n.t('Send')()}
/>
</div>
</div>
return (
<div
className={containerClass}
style={{ height: containerHeight }}
lang={i18n.getLocale()}
>
<div
className='main-container'
role='alert'
style={bgColor}
onContextMenu={this.eventHandlers.onContextMenu}
onClick={this.eventHandlers.onClick(id)}
onMouseEnter={this.eventHandlers.onMouseEnter(id)}
onMouseLeave={this.eventHandlers.onMouseLeave(id)}
>
<div className='logo-container'>
<div className='logo'>
<img
src='../renderer/assets/notification-symphony-logo.svg'
alt='Symphony logo'
/>
</div>
);
}
/**
* Renders external badge if the content is from external
* @param isExternal
*/
private renderExtBadge(isExternal: boolean): JSX.Element | undefined {
if (!isExternal) {
return;
}
return (
<div className='ext-badge-container'>
<img src='../renderer/assets/notification-ext-badge.svg' alt='ext-badge'/>
</div>
<div className='header'>
<div className='title-container'>
<span className={`title ${themeClassName}`}>{title}</span>
{this.renderExtBadge(isExternal)}
</div>
);
}
<span className={`message ${themeClassName}`}>{body}</span>
</div>
<div className='actions-container'>
<button
className={`action-button ${themeClassName}`}
title={i18n.t('Close')()}
onClick={this.eventHandlers.onClose(id)}
>
{i18n.t('Close')()}
</button>
<button
className={`action-button ${themeClassName}`}
style={{ visibility: hasReply ? 'visible' : 'hidden' }}
title={i18n.t('Reply')()}
onClick={this.eventHandlers.onOpenReply(id)}
>
{i18n.t('Reply')()}
</button>
</div>
</div>
<div
style={{
...{ display: isInputHidden ? 'none' : 'flex' },
...bgColor,
}}
className='rte-container'
>
<div className='input-container'>
<div className='input-border' />
<div className='input-caret-container'>
<span ref={this.customInput} className='custom-input' />
</div>
<div ref={this.inputCaret} className='input-caret' />
<input
style={bgColor}
className={themeClassName}
autoFocus={true}
onInput={this.setInputCaretPosition}
onKeyDown={this.setInputCaretPosition}
onKeyUp={this.eventHandlers.onKeyUp(id)}
onChange={this.setInputCaretPosition}
onClick={this.setInputCaretPosition}
onPaste={this.setInputCaretPosition}
onCut={this.setInputCaretPosition}
onCopy={this.setInputCaretPosition}
onMouseDown={this.setInputCaretPosition}
onMouseUp={this.setInputCaretPosition}
onFocus={() => this.animateCaret(true)}
onBlur={() => this.animateCaret(false)}
ref={this.input}
/>
</div>
<div className='rte-button-container'>
<button
className={`rte-thumbsup-button ${themeClassName}`}
onClick={this.eventHandlers.onThumbsUp()}
>
👍
</button>
<button
className={`rte-send-button ${themeClassName}`}
onClick={this.eventHandlers.onReply(id)}
disabled={!canSendMessage}
title={i18n.t('Send')()}
/>
</div>
</div>
</div>
);
}
/**
* Invoked when the notification window is clicked
*
* @param id {number}
*/
private click(id: number): void {
ipcRenderer.send('notification-clicked', id);
this.clearFlashInterval();
/**
* Renders external badge if the content is from external
* @param isExternal
*/
private renderExtBadge(isExternal: boolean): JSX.Element | undefined {
if (!isExternal) {
return;
}
return (
<div className='ext-badge-container'>
<img
src='../renderer/assets/notification-ext-badge.svg'
alt='ext-badge'
/>
</div>
);
}
/**
* Closes the notification
*
* @param id {number}
*/
private close(id: number): void {
ipcRenderer.send('close-notification', id);
this.clearFlashInterval();
/**
* Invoked when the notification window is clicked
*
* @param id {number}
*/
private click(id: number): void {
ipcRenderer.send('notification-clicked', id);
this.clearFlashInterval();
}
/**
* Closes the notification
*
* @param id {number}
*/
private close(id: number): void {
ipcRenderer.send('close-notification', id);
this.clearFlashInterval();
}
/**
* Disable context menu
*
* @param event
*/
private contextMenu(event): void {
event.preventDefault();
}
/**
* Handle mouse enter
*
* @param id {number}
*/
private onMouseEnter(id: number): void {
ipcRenderer.send('notification-mouseenter', id);
}
/**
* Handle mouse over
*
* @param id {number}
*/
private onMouseLeave(id: number): void {
const { isInputHidden } = this.state;
ipcRenderer.send('notification-mouseleave', id, isInputHidden);
}
/**
* Insets a thumbs up emoji
* @private
*/
private onThumbsUp(): void {
if (this.input.current) {
const input = this.input.current.value;
this.input.current.value = input + '👍';
this.setInputCaretPosition();
this.input.current.focus();
}
}
/**
* Disable context menu
*
* @param event
*/
private contextMenu(event): void {
event.preventDefault();
/**
* Handles reply action
* @param id
* @private
*/
private onReply(id: number): void {
let replyText = this.getInputValue();
if (replyText) {
// need to replace 👍 with :thumbsup: to make sure client displays the correct emoji
replyText = replyText.replace(/👍/g, ':thumbsup: ');
ipcRenderer.send('notification-on-reply', id, replyText);
}
}
/**
* Handle mouse enter
*
* @param id {number}
*/
private onMouseEnter(id: number): void {
ipcRenderer.send('notification-mouseenter', id);
/**
* Clears a active notification flash interval
*/
private clearFlashInterval(): void {
if (this.flashTimer) {
clearInterval(this.flashTimer);
}
}
/**
* Handle mouse over
*
* @param id {number}
*/
private onMouseLeave(id: number): void {
const { isInputHidden } = this.state;
ipcRenderer.send('notification-mouseleave', id, isInputHidden);
/**
* Displays an input on the notification
*
* @private
*/
private onOpenReply(event, id) {
event.stopPropagation();
ipcRenderer.send('show-reply', id);
this.setState(
{
isInputHidden: false,
hasReply: false,
containerHeight: CONTAINER_HEIGHT_WITH_INPUT,
},
() => {
this.input.current?.focus();
},
);
}
/**
* Trim and returns the input value
* @private
*/
private getInputValue(): string | undefined {
return this.input.current?.value.trim();
}
/**
* Handles key up event and enter keyCode
*
* @param event
* @param id
* @private
*/
private onKeyUp(event, id) {
this.setInputCaretPosition();
if (event.key === 'Enter' || event.keyCode === 13) {
this.onReply(id);
}
}
/**
* Insets a thumbs up emoji
* @private
*/
private onThumbsUp(): void {
if (this.input.current) {
const input = this.input.current.value;
this.input.current.value = input + '👍';
this.setInputCaretPosition();
this.input.current.focus();
}
}
/**
* Handles reply action
* @param id
* @private
*/
private onReply(id: number): void {
let replyText = this.getInputValue();
if (replyText) {
// need to replace 👍 with :thumbsup: to make sure client displays the correct emoji
replyText = replyText.replace(/👍/g, ':thumbsup: ');
ipcRenderer.send('notification-on-reply', id, replyText);
}
}
/**
* Clears a active notification flash interval
*/
private clearFlashInterval(): void {
if (this.flashTimer) {
clearInterval(this.flashTimer);
}
}
/**
* Displays an input on the notification
*
* @private
*/
private onOpenReply(event, id) {
event.stopPropagation();
ipcRenderer.send('show-reply', id);
/**
* Moves the custom input caret based on input text
* @private
*/
private setInputCaretPosition() {
if (this.customInput.current) {
if (this.input.current) {
const inputText = this.input.current.value || '';
const selectionStart = this.input.current.selectionStart || 0;
this.customInput.current.innerText = inputText
.substring(0, selectionStart)
.replace(/\n$/, '\n\u0001');
this.setState({
isInputHidden: false,
hasReply: false,
containerHeight: CONTAINER_HEIGHT_WITH_INPUT,
}, () => {
this.input.current?.focus();
canSendMessage: inputText.trim().length > 0,
});
}
const rects = this.customInput.current.getClientRects();
const lastRect = rects && rects[rects.length - 1];
const x = (lastRect && lastRect.width) || 0;
if (this.inputCaret.current) {
this.inputCaret.current.style.left = x + 'px';
}
}
}
/**
* Trim and returns the input value
* @private
*/
private getInputValue(): string | undefined {
return this.input.current?.value.trim();
/**
* Adds blinking animation to input caret
* @param hasFocus
* @private
*/
private animateCaret(hasFocus: boolean) {
if (hasFocus) {
this.inputCaret.current?.classList.add('input-caret-focus');
} else {
this.inputCaret.current?.classList.remove('input-caret-focus');
}
}
/**
* Handles key up event and enter keyCode
*
* @param event
* @param id
* @private
*/
private onKeyUp(event, id) {
this.setInputCaretPosition();
if (event.key === 'Enter' || event.keyCode === 13) {
this.onReply(id);
}
/**
* Sets the component state
*
* @param _event
* @param data {Object}
*/
private updateState(_event, data): void {
const { color, flash } = data;
data.color = color && !color.startsWith('#') ? '#' + color : color;
data.isInputHidden = true;
data.containerHeight = CONTAINER_HEIGHT;
data.color = this.isValidColor(data.color) ? data.color : '';
this.resetNotificationData();
this.setState(data as IState);
if (this.flashTimer) {
clearInterval(this.flashTimer);
}
/**
* Moves the custom input caret based on input text
* @private
*/
private setInputCaretPosition() {
if (this.customInput.current) {
if (this.input.current) {
const inputText = this.input.current.value || '';
const selectionStart = this.input.current.selectionStart || 0;
this.customInput.current.innerText = inputText.substring(0, selectionStart).replace(/\n$/, '\n\u0001');
this.setState({
canSendMessage: inputText.trim().length > 0,
});
}
const rects = this.customInput.current.getClientRects();
const lastRect = rects && rects[ rects.length - 1 ];
const x = lastRect && lastRect.width || 0;
if (this.inputCaret.current) {
this.inputCaret.current.style.left = x + 'px';
}
}
}
/**
* Adds blinking animation to input caret
* @param hasFocus
* @private
*/
private animateCaret(hasFocus: boolean) {
if (hasFocus) {
this.inputCaret.current?.classList.add('input-caret-focus');
if (flash) {
const origColor = data.color;
this.flashTimer = setInterval(() => {
const { color: bgColor } = this.state;
if (bgColor === 'red') {
this.setState({ color: origColor });
} else {
this.inputCaret.current?.classList.remove('input-caret-focus');
this.setState({ color: 'red' });
}
}, 1000);
}
}
/**
* Sets the component state
*
* @param _event
* @param data {Object}
*/
private updateState(_event, data): void {
const { color, flash } = data;
data.color = (color && !color.startsWith('#')) ? '#' + color : color;
data.isInputHidden = true;
data.containerHeight = CONTAINER_HEIGHT;
/**
* Validates the color
* @param color
* @private
*/
private isValidColor(color: string): boolean {
return /^#([A-Fa-f0-9]{6})/.test(color);
}
data.color = this.isValidColor(data.color) ? data.color : '';
this.resetNotificationData();
this.setState(data as IState);
if (this.flashTimer) {
clearInterval(this.flashTimer);
}
if (flash) {
const origColor = data.color;
this.flashTimer = setInterval(() => {
const { color: bgColor } = this.state;
if (bgColor === 'red') {
this.setState({ color: origColor });
} else {
this.setState({ color: 'red' });
}
}, 1000);
}
}
/**
* Validates the color
* @param color
* @private
*/
private isValidColor(color: string): boolean {
return /^#([A-Fa-f0-9]{6})/.test(color);
}
/**
* Reset data for new notification
* @private
*/
private resetNotificationData(): void {
if (this.input.current) {
this.input.current.value = '';
}
this.setInputCaretPosition();
/**
* Reset data for new notification
* @private
*/
private resetNotificationData(): void {
if (this.input.current) {
this.input.current.value = '';
}
this.setInputCaretPosition();
}
}

View File

@ -9,217 +9,275 @@ const NOTIFICATION_SETTINGS_NAMESPACE = 'NotificationSettings';
type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left';
interface IState {
position: startCorner;
screens: Electron.Display[];
display: number;
theme: Themes;
position: startCorner;
screens: Electron.Display[];
display: number;
theme: Themes;
}
export enum Themes {
LIGHT = 'light',
DARK = 'dark',
LIGHT = 'light',
DARK = 'dark',
}
/**
* Notification Window component
*/
export default class NotificationSettings extends React.Component<{}, IState> {
constructor(props) {
super(props);
this.state = {
position: 'upper-right',
screens: [],
display: 1,
theme: Themes.LIGHT,
};
this.updateState = this.updateState.bind(this);
}
constructor(props) {
super(props);
this.state = {
position: 'upper-right',
screens: [],
display: 1,
theme: Themes.LIGHT,
};
this.updateState = this.updateState.bind(this);
/**
* Renders the notification settings window
*/
public render(): JSX.Element {
if (this.state.theme === Themes.DARK) {
document.body.classList.add('dark-mode');
}
/**
* Renders the notification settings window
*/
public render(): JSX.Element {
if (this.state.theme === Themes.DARK) {
document.body.classList.add('dark-mode');
return (
<div
className='content'
style={
this.state.theme === Themes.DARK
? { backgroundColor: '#25272B' }
: undefined
}
return (
<div className='content' style={this.state.theme === Themes.DARK ? { backgroundColor: '#25272B' } : undefined} >
<header
className='header'
style={this.state.theme === Themes.DARK ? { color: 'white', borderBottom: '1px solid #525760' } : undefined}>
<span className='header-title'>
{i18n.t('Set Notification Position', NOTIFICATION_SETTINGS_NAMESPACE)()}
</span>
</header>
<div className='form'>
<label className='display-label' style={this.state.theme === Themes.DARK ? { color: 'white' } : undefined}>
{i18n.t('Show on display', NOTIFICATION_SETTINGS_NAMESPACE)()}
</label>
<div id='screens' className='display-container'>
<select
className='display-selector'
style={this.state.theme === Themes.DARK ? { border: '2px solid #767A81', backgroundColor: '#25272B', color: 'white' } : undefined}
id='screen-selector'
title='position'
value={this.state.display}
onChange={this.selectDisplay.bind(this)}
>
{this.renderScreens()}
</select>
</div>
<label className='position-label' style={this.state.theme === Themes.DARK ? { color: 'white' } : undefined}>
{i18n.t('Position', NOTIFICATION_SETTINGS_NAMESPACE)()}
</label>
<div className='position-container' style={this.state.theme === Themes.DARK ? { background: '#2E3136' } : undefined}>
<div className='button-set-left'>
{this.renderPositionButton('upper-left', 'Top Left')}
{this.renderPositionButton('lower-left', 'Bottom Left')}
</div>
<div className='button-set-right'>
{this.renderPositionButton('upper-right', 'Top Right')}
{this.renderPositionButton('lower-right', 'Bottom Right')}
</div>
</div>
</div>
<footer className='footer' style={this.state.theme === Themes.DARK ? { borderTop: '1px solid #525760' } : undefined}>
<div className='footer-button-container'>
<button
id='cancel'
className='footer-button footer-button-dismiss'
onClick={this.close.bind(this)}
style={this.state.theme === Themes.DARK ? { backgroundColor: '#25272B', color: 'white' } : undefined}>
{i18n.t('CANCEL', NOTIFICATION_SETTINGS_NAMESPACE)()}
</button>
<button
id='ok-button'
className='footer-button footer-button-ok'
onClick={this.submit.bind(this)}
style={this.state.theme === Themes.DARK ? { backgroundColor: '#25272B', color: 'white' } : undefined}>
{i18n.t('OK', NOTIFICATION_SETTINGS_NAMESPACE)()}
</button>
</div>
</footer>
>
<header
className='header'
style={
this.state.theme === Themes.DARK
? { color: 'white', borderBottom: '1px solid #525760' }
: undefined
}
>
<span className='header-title'>
{i18n.t(
'Set Notification Position',
NOTIFICATION_SETTINGS_NAMESPACE,
)()}
</span>
</header>
<div className='form'>
<label
className='display-label'
style={
this.state.theme === Themes.DARK ? { color: 'white' } : undefined
}
>
{i18n.t('Show on display', NOTIFICATION_SETTINGS_NAMESPACE)()}
</label>
<div id='screens' className='display-container'>
<select
className='display-selector'
style={
this.state.theme === Themes.DARK
? {
border: '2px solid #767A81',
backgroundColor: '#25272B',
color: 'white',
}
: undefined
}
id='screen-selector'
title='position'
value={this.state.display}
onChange={this.selectDisplay.bind(this)}
>
{this.renderScreens()}
</select>
</div>
<label
className='position-label'
style={
this.state.theme === Themes.DARK ? { color: 'white' } : undefined
}
>
{i18n.t('Position', NOTIFICATION_SETTINGS_NAMESPACE)()}
</label>
<div
className='position-container'
style={
this.state.theme === Themes.DARK
? { background: '#2E3136' }
: undefined
}
>
<div className='button-set-left'>
{this.renderPositionButton('upper-left', 'Top Left')}
{this.renderPositionButton('lower-left', 'Bottom Left')}
</div>
);
}
/**
* Handles event when the component is mounted
*/
public componentDidMount(): void {
ipcRenderer.on('notification-settings-data', this.updateState);
}
/**
* Handles event when the component is unmounted
*/
public componentWillUnmount(): void {
ipcRenderer.removeListener('notification-settings-data', this.updateState);
}
/**
* Updates the selected display state
*
* @param event
*/
public selectDisplay(event): void {
this.setState({ display: event.target.value });
}
/**
* Updates the selected notification position
*
* @param event
*/
public togglePosition(event): void {
this.setState({
position: event.target.id,
});
}
/**
* Submits the new settings to the main process
*/
public submit(): void {
const { position, display } = this.state;
ipcRenderer.send('notification-settings-update', { position, display });
}
/**
* Closes the notification settings window
*/
public close(): void {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeWindow,
windowType: 'notification-settings',
});
}
/**
* Renders the position buttons
*
* @param id
* @param content
*/
private renderPositionButton(id: startCorner, content: string): JSX.Element {
const style = this.getPositionButtonStyle(id);
return (
<div className='position-button-container'>
<button
onClick={this.togglePosition.bind(this)}
className='position-button'
style={style}
id={id}
data-testid={id}
type='button'
name='position'
value={id}
>
{i18n.t(`${content}`, NOTIFICATION_SETTINGS_NAMESPACE)()}
</button>
<div className='button-set-right'>
{this.renderPositionButton('upper-right', 'Top Right')}
{this.renderPositionButton('lower-right', 'Bottom Right')}
</div>
);
}
</div>
</div>
<footer
className='footer'
style={
this.state.theme === Themes.DARK
? { borderTop: '1px solid #525760' }
: undefined
}
>
<div className='footer-button-container'>
<button
id='cancel'
className='footer-button footer-button-dismiss'
onClick={this.close.bind(this)}
style={
this.state.theme === Themes.DARK
? { backgroundColor: '#25272B', color: 'white' }
: undefined
}
>
{i18n.t('CANCEL', NOTIFICATION_SETTINGS_NAMESPACE)()}
</button>
<button
id='ok-button'
className='footer-button footer-button-ok'
onClick={this.submit.bind(this)}
style={
this.state.theme === Themes.DARK
? { backgroundColor: '#25272B', color: 'white' }
: undefined
}
>
{i18n.t('OK', NOTIFICATION_SETTINGS_NAMESPACE)()}
</button>
</div>
</footer>
</div>
);
}
/**
* Gets the text color and background color of a position button
*
* @param id
*/
private getPositionButtonStyle(id: string): React.CSSProperties {
let style: React.CSSProperties;
if (this.state.position === id) {
style = { backgroundColor: '#008EFF', color: 'white' };
} else if (this.state.theme === Themes.DARK) {
style = { backgroundColor: '#25272B', color: 'white' };
} else {
style = { backgroundColor: '#F8F8F9', color: '#17181B' };
}
return style;
}
/**
* Handles event when the component is mounted
*/
public componentDidMount(): void {
ipcRenderer.on('notification-settings-data', this.updateState);
}
/**
* Renders the drop down list of available screens
*/
private renderScreens(): JSX.Element[] {
const { screens } = this.state;
return screens.map((screen, index) => {
const screenId = screen.id;
return (
<option id={String(screenId)} key={screenId} value={screenId}>{index + 1}/{screens.length}</option>
);
});
}
/**
* Handles event when the component is unmounted
*/
public componentWillUnmount(): void {
ipcRenderer.removeListener('notification-settings-data', this.updateState);
}
/**
* Sets the component state
*
* @param _event
* @param data {Object} { buildNumber, clientVersion, version }
*/
private updateState(_event, data): void {
this.setState(data as IState);
/**
* Updates the selected display state
*
* @param event
*/
public selectDisplay(event): void {
this.setState({ display: event.target.value });
}
/**
* Updates the selected notification position
*
* @param event
*/
public togglePosition(event): void {
this.setState({
position: event.target.id,
});
}
/**
* Submits the new settings to the main process
*/
public submit(): void {
const { position, display } = this.state;
ipcRenderer.send('notification-settings-update', { position, display });
}
/**
* Closes the notification settings window
*/
public close(): void {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeWindow,
windowType: 'notification-settings',
});
}
/**
* Renders the position buttons
*
* @param id
* @param content
*/
private renderPositionButton(id: startCorner, content: string): JSX.Element {
const style = this.getPositionButtonStyle(id);
return (
<div className='position-button-container'>
<button
onClick={this.togglePosition.bind(this)}
className='position-button'
style={style}
id={id}
data-testid={id}
type='button'
name='position'
value={id}
>
{i18n.t(`${content}`, NOTIFICATION_SETTINGS_NAMESPACE)()}
</button>
</div>
);
}
/**
* Gets the text color and background color of a position button
*
* @param id
*/
private getPositionButtonStyle(id: string): React.CSSProperties {
let style: React.CSSProperties;
if (this.state.position === id) {
style = { backgroundColor: '#008EFF', color: 'white' };
} else if (this.state.theme === Themes.DARK) {
style = { backgroundColor: '#25272B', color: 'white' };
} else {
style = { backgroundColor: '#F8F8F9', color: '#17181B' };
}
return style;
}
/**
* Renders the drop down list of available screens
*/
private renderScreens(): JSX.Element[] {
const { screens } = this.state;
return screens.map((screen, index) => {
const screenId = screen.id;
return (
<option id={String(screenId)} key={screenId} value={screenId}>
{index + 1}/{screens.length}
</option>
);
});
}
/**
* Sets the component state
*
* @param _event
* @param data {Object} { buildNumber, clientVersion, version }
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
}

View File

@ -6,389 +6,438 @@ import { apiCmds, apiName } from '../../common/api-interface';
import { isLinux, isMac, isWindowsOS } from '../../common/env';
import { i18n } from '../../common/i18n-preload';
const screenRegExp = new RegExp(/^screen \d+$/gmi);
const screenRegExp = new RegExp(/^screen \d+$/gim);
const SCREEN_PICKER_NAMESPACE = 'ScreenPicker';
const ENTIRE_SCREEN = 'entire screen';
interface IState {
sources: ICustomDesktopCapturerSource[];
selectedSource: ICustomDesktopCapturerSource | undefined;
selectedTab: tabs;
sources: ICustomDesktopCapturerSource[];
selectedSource: ICustomDesktopCapturerSource | undefined;
selectedTab: tabs;
}
interface ICustomDesktopCapturerSource extends Electron.DesktopCapturerSource {
fileName: string | null;
fileName: string | null;
}
type tabs = 'screens' | 'applications';
const enum keyCode {
pageDown = 34,
rightArrow = 39,
pageUp = 33,
leftArrow = 37,
homeKey = 36,
upArrow = 38,
endKey = 35,
arrowDown = 40,
enterKey = 13,
escapeKey = 27,
pageDown = 34,
rightArrow = 39,
pageUp = 33,
leftArrow = 37,
homeKey = 36,
upArrow = 38,
endKey = 35,
arrowDown = 40,
enterKey = 13,
escapeKey = 27,
}
type inputChangeEvent = React.ChangeEvent<HTMLInputElement>;
export default class ScreenPicker extends React.Component<{}, IState> {
private isScreensAvailable: boolean;
private isApplicationsAvailable: boolean;
private readonly eventHandlers = {
onSelect: (src: ICustomDesktopCapturerSource) => this.select(src),
onToggle: (tab: tabs) => (_event: inputChangeEvent) => this.toggle(tab),
onClose: () => this.close(),
onSubmit: () => this.submit(),
};
private currentIndex: number;
private isScreensAvailable: boolean;
private isApplicationsAvailable: boolean;
private readonly eventHandlers = {
onSelect: (src: ICustomDesktopCapturerSource) => this.select(src),
onToggle: (tab: tabs) => (_event: inputChangeEvent) => this.toggle(tab),
onClose: () => this.close(),
onSubmit: () => this.submit(),
constructor(props) {
super(props);
this.state = {
sources: [],
selectedSource: undefined,
selectedTab: 'screens',
};
private currentIndex: number;
this.currentIndex = 0;
this.isScreensAvailable = false;
this.isApplicationsAvailable = false;
this.updateState = this.updateState.bind(this);
this.handleKeyUpPress = this.handleKeyUpPress.bind(this);
this.renderTabTitles = this.renderTabTitles.bind(this);
}
constructor(props) {
super(props);
this.state = {
sources: [],
selectedSource: undefined,
selectedTab: 'screens',
};
this.currentIndex = 0;
this.isScreensAvailable = false;
this.isApplicationsAvailable = false;
this.updateState = this.updateState.bind(this);
this.handleKeyUpPress = this.handleKeyUpPress.bind(this);
this.renderTabTitles = this.renderTabTitles.bind(this);
/**
* Callback to handle event when a component is mounted
*/
public componentDidMount(): void {
ipcRenderer.on('screen-picker-data', this.updateState);
document.addEventListener('keyup', this.handleKeyUpPress, true);
if (isWindowsOS) {
document.body.classList.add('ScreenPicker-window-border');
}
}
public componentDidMount(): void {
ipcRenderer.on('screen-picker-data', this.updateState);
document.addEventListener('keyup', this.handleKeyUpPress, true);
if (isWindowsOS) {
document.body.classList.add('ScreenPicker-window-border');
}
}
/**
* Callback to handle event when a component is unmounted
*/
public componentWillUnmount(): void {
ipcRenderer.removeListener('screen-picker-data', this.updateState);
document.removeEventListener('keyup', this.handleKeyUpPress, true);
}
public componentWillUnmount(): void {
ipcRenderer.removeListener('screen-picker-data', this.updateState);
document.removeEventListener('keyup', this.handleKeyUpPress, true);
}
/**
* main render function
*/
public render(): JSX.Element {
const { sources, selectedSource } = this.state;
return (
<div className='ScreenPicker ScreenPicker-content'>
<div className='ScreenPicker-title'>
<span>{i18n.t(`Choose what you'd like to share`, SCREEN_PICKER_NAMESPACE)()}</span>
<div className='ScreenPicker-x-button' onClick={this.eventHandlers.onClose}>
<div className='content-button'>
<i>
<svg viewBox='0 0 48 48' fill='grey'>
<path
d='M39.4,33.8L31,25.4c-0.4-0.4-0.9-0.9-1.4-1.4c0.5-0.5,1-1,1.4-1.4l8.4-8.4c0.8-0.8,0.8-2,0-2.8l-2.8-2.8 c-0.8-0.8-2-0.8-2.8,0L25.4,17c-0.4,0.4-0.9,0.9-1.4,1.4c-0.5-0.5-1-1-1.4-1.4l-8.4-8.4c-0.8-0.8-2-0.8-2.8,0l-2.8,2.8 c-0.8,0.8-0.8,2,0,2.8l8.4,8.4c0.4,0.4,0.9,0.9,1.4,1.4c-0.5,0.5-1,1-1.4,1.4l-8.4,8.4c-0.8,0.8-0.8,2,0,2.8l2.8,2.8 c0.8,0.8,2,0.8,2.8,0l8.4-8.4c0.4-0.4,0.9-0.9,1.4-1.4c0.5,0.5,1,1,1.4,1.4l8.4,8.4c0.8,0.8,2,0.8,2.8,0l2.8-2.8 C40.2,35.8,40.2,34.6,39.4,33.8z'
/>
</svg>
</i>
</div>
</div>
</div>
{this.renderSources(sources)}
<footer>
<button
className='ScreenPicker-cancel-button'
onClick={this.eventHandlers.onClose}
>
{i18n.t('Cancel', SCREEN_PICKER_NAMESPACE)()}
</button>
<button
className={classNames('ScreenPicker-share-button',
{ 'ScreenPicker-share-button-disable': !selectedSource })}
onClick={this.eventHandlers.onSubmit}
>
{selectedSource ? i18n.t('Share', SCREEN_PICKER_NAMESPACE)() : i18n.t('Select Screen', SCREEN_PICKER_NAMESPACE)()}
</button>
</footer>
/**
* Renders the component
*/
public render(): JSX.Element {
const { sources, selectedSource } = this.state;
return (
<div className='ScreenPicker ScreenPicker-content'>
<div className='ScreenPicker-title'>
<span>
{i18n.t(
`Choose what you'd like to share`,
SCREEN_PICKER_NAMESPACE,
)()}
</span>
<div
className='ScreenPicker-x-button'
onClick={this.eventHandlers.onClose}
>
<div className='content-button'>
<i>
<svg viewBox='0 0 48 48' fill='grey'>
<path d='M39.4,33.8L31,25.4c-0.4-0.4-0.9-0.9-1.4-1.4c0.5-0.5,1-1,1.4-1.4l8.4-8.4c0.8-0.8,0.8-2,0-2.8l-2.8-2.8 c-0.8-0.8-2-0.8-2.8,0L25.4,17c-0.4,0.4-0.9,0.9-1.4,1.4c-0.5-0.5-1-1-1.4-1.4l-8.4-8.4c-0.8-0.8-2-0.8-2.8,0l-2.8,2.8 c-0.8,0.8-0.8,2,0,2.8l8.4,8.4c0.4,0.4,0.9,0.9,1.4,1.4c-0.5,0.5-1,1-1.4,1.4l-8.4,8.4c-0.8,0.8-0.8,2,0,2.8l2.8,2.8 c0.8,0.8,2,0.8,2.8,0l8.4-8.4c0.4-0.4,0.9-0.9,1.4-1.4c0.5,0.5,1,1,1.4,1.4l8.4,8.4c0.8,0.8,2,0.8,2.8,0l2.8-2.8 C40.2,35.8,40.2,34.6,39.4,33.8z' />
</svg>
</i>
</div>
);
}
</div>
</div>
{this.renderSources(sources)}
<footer>
<button
className='ScreenPicker-cancel-button'
onClick={this.eventHandlers.onClose}
>
{i18n.t('Cancel', SCREEN_PICKER_NAMESPACE)()}
</button>
<button
className={classNames('ScreenPicker-share-button', {
'ScreenPicker-share-button-disable': !selectedSource,
})}
onClick={this.eventHandlers.onSubmit}
>
{selectedSource
? i18n.t('Share', SCREEN_PICKER_NAMESPACE)()
: i18n.t('Select Screen', SCREEN_PICKER_NAMESPACE)()}
</button>
</footer>
</div>
);
}
/**
* Renders the sources by separating screens and applications
*
* @param sources {DesktopCapturerSource}
*/
private renderSources(sources: ICustomDesktopCapturerSource[]): JSX.Element {
const screens: JSX.Element[] = [];
const applications: JSX.Element[] = [];
sources.map((source: ICustomDesktopCapturerSource) => {
screenRegExp.lastIndex = 0;
const shouldHighlight: string = classNames(
'ScreenPicker-item-container',
{ 'ScreenPicker-selected': this.shouldHighlight(source.id) },
);
const sourceName = source.name.toLocaleLowerCase();
if (((isMac || isLinux) && source.display_id !== '') || (isWindowsOS && (sourceName === ENTIRE_SCREEN || screenRegExp.exec(sourceName)))) {
source.fileName = 'fullscreen';
let screenName;
if (sourceName === ENTIRE_SCREEN) {
screenName = i18n.t('Entire screen', SCREEN_PICKER_NAMESPACE)();
} else {
const screenNumber = source.name.substr(7, source.name.length);
screenName = i18n.t('Screen {number}', SCREEN_PICKER_NAMESPACE)({ number: screenNumber });
}
screens.push(
<div
className={shouldHighlight}
id={source.id}
onClick={() => this.eventHandlers.onSelect(source)}>
<div className='ScreenPicker-screen-section-box'>
<img className='ScreenPicker-img-wrapper' src={source.thumbnail as any} alt='thumbnail image' />
</div>
<div className='ScreenPicker-screen-source-title'>{screenName}</div>
</div>,
);
} else {
source.fileName = null;
applications.push(
<div
className={shouldHighlight}
id={source.id}
onClick={() => this.eventHandlers.onSelect(source)}>
<div className='ScreenPicker-screen-section-box'>
<img className='ScreenPicker-img-wrapper' src={source.thumbnail as any} alt='thumbnail image' />
</div>
<div className='ScreenPicker-screen-source-title'>{source.name}</div>
</div>,
);
}
});
this.isScreensAvailable = screens.length > 0;
this.isApplicationsAvailable = applications.length > 0;
if (!this.isScreensAvailable && !this.isApplicationsAvailable) {
return (
<div className='ScreenPicker-error-content'>
<span className='error-message'>
{i18n.t('No screens or applications are currently available.', SCREEN_PICKER_NAMESPACE)()}
</span>
</div>
);
/**
* Renders the sources by separating screens and applications
*
* @param sources {DesktopCapturerSource}
*/
private renderSources(sources: ICustomDesktopCapturerSource[]): JSX.Element {
const screens: JSX.Element[] = [];
const applications: JSX.Element[] = [];
sources.map((source: ICustomDesktopCapturerSource) => {
screenRegExp.lastIndex = 0;
const shouldHighlight: string = classNames(
'ScreenPicker-item-container',
{ 'ScreenPicker-selected': this.shouldHighlight(source.id) },
);
const sourceName = source.name.toLocaleLowerCase();
if (
((isMac || isLinux) && source.display_id !== '') ||
(isWindowsOS &&
(sourceName === ENTIRE_SCREEN || screenRegExp.exec(sourceName)))
) {
source.fileName = 'fullscreen';
let screenName;
if (sourceName === ENTIRE_SCREEN) {
screenName = i18n.t('Entire screen', SCREEN_PICKER_NAMESPACE)();
} else {
const screenNumber = source.name.substr(7, source.name.length);
screenName = i18n.t(
'Screen {number}',
SCREEN_PICKER_NAMESPACE,
)({ number: screenNumber });
}
return (
<div className='ScreenPicker-main-content'>
{this.renderTabTitles()}
<section id='screen-contents'>{screens}</section>
<section id='application-contents'> {applications}</section>
screens.push(
<div
className={shouldHighlight}
id={source.id}
onClick={() => this.eventHandlers.onSelect(source)}
>
<div className='ScreenPicker-screen-section-box'>
<img
className='ScreenPicker-img-wrapper'
src={source.thumbnail as any}
alt='thumbnail image'
/>
</div>
<div className='ScreenPicker-screen-source-title'>{screenName}</div>
</div>,
);
} else {
source.fileName = null;
applications.push(
<div
className={shouldHighlight}
id={source.id}
onClick={() => this.eventHandlers.onSelect(source)}
>
<div className='ScreenPicker-screen-section-box'>
<img
className='ScreenPicker-img-wrapper'
src={source.thumbnail as any}
alt='thumbnail image'
/>
</div>
<div className='ScreenPicker-screen-source-title'>
{source.name}
</div>
</div>,
);
}
});
this.isScreensAvailable = screens.length > 0;
this.isApplicationsAvailable = applications.length > 0;
if (!this.isScreensAvailable && !this.isApplicationsAvailable) {
return (
<div className='ScreenPicker-error-content'>
<span className='error-message'>
{i18n.t(
'No screens or applications are currently available.',
SCREEN_PICKER_NAMESPACE,
)()}
</span>
</div>
);
}
/**
* Renders the screen and application tab section
*/
private renderTabTitles(): JSX.Element[] | undefined {
const { selectedTab } = this.state;
if (this.isScreensAvailable && this.isApplicationsAvailable) {
return [
<input
id='screen-tab'
className='ScreenPicker-screen-tab'
type='radio'
name='tabs'
checked={selectedTab === 'screens'}
onChange={this.eventHandlers.onToggle('screens')}
/>,
<label className={classNames('screens', { hidden: !this.isScreensAvailable })}
htmlFor='screen-tab'
>
{i18n.t('Screens', SCREEN_PICKER_NAMESPACE)()}
</label>,
<input
id='application-tab'
className='ScreenPicker-application-tab'
type='radio'
name='tabs'
checked={selectedTab === 'applications'}
onChange={this.eventHandlers.onToggle('applications')}
/>,
<label className={classNames('applications', { hidden: !this.isApplicationsAvailable })}
htmlFor='application-tab'
>
{i18n.t('Applications', SCREEN_PICKER_NAMESPACE)()}
</label>,
];
return (
<div className='ScreenPicker-main-content'>
{this.renderTabTitles()}
<section id='screen-contents'>{screens}</section>
<section id='application-contents'> {applications}</section>
</div>
);
}
/**
* Renders the screen and application tab section
*/
private renderTabTitles(): JSX.Element[] | undefined {
const { selectedTab } = this.state;
if (this.isScreensAvailable && this.isApplicationsAvailable) {
return [
<input
id='screen-tab'
className='ScreenPicker-screen-tab'
type='radio'
name='tabs'
checked={selectedTab === 'screens'}
onChange={this.eventHandlers.onToggle('screens')}
/>,
<label
className={classNames('screens', {
hidden: !this.isScreensAvailable,
})}
htmlFor='screen-tab'
>
{i18n.t('Screens', SCREEN_PICKER_NAMESPACE)()}
</label>,
<input
id='application-tab'
className='ScreenPicker-application-tab'
type='radio'
name='tabs'
checked={selectedTab === 'applications'}
onChange={this.eventHandlers.onToggle('applications')}
/>,
<label
className={classNames('applications', {
hidden: !this.isApplicationsAvailable,
})}
htmlFor='application-tab'
>
{i18n.t('Applications', SCREEN_PICKER_NAMESPACE)()}
</label>,
];
}
if (this.isScreensAvailable) {
return [
<input
id='screen-tab'
className='ScreenPicker-screen-tab'
type='radio'
name='tabs'
checked={true}
onChange={this.eventHandlers.onToggle('screens')}
/>,
<label
className={classNames('screens', {
hidden: !this.isScreensAvailable,
})}
htmlFor='screen-tab'
>
{i18n.t('Screens', SCREEN_PICKER_NAMESPACE)()}
</label>,
];
}
if (this.isApplicationsAvailable) {
return [
<input
id='application-tab'
className='ScreenPicker-application-tab'
type='radio'
name='tabs'
checked={true}
onChange={this.eventHandlers.onToggle('applications')}
/>,
<label
className={classNames('applications', {
hidden: !this.isApplicationsAvailable,
})}
htmlFor='application-tab'
>
{i18n.t('Applications', SCREEN_PICKER_NAMESPACE)()}
</label>,
];
}
return;
}
/**
* Updates the selected state
*
* @param id {string}
*/
private shouldHighlight(id: string): boolean {
const { selectedSource } = this.state;
return selectedSource ? id === selectedSource.id : false;
}
/**
* updates the state when the source is selected
*
* @param selectedSource {DesktopCapturerSource}
*/
private select(selectedSource: ICustomDesktopCapturerSource): void {
this.setState({ selectedSource });
if (selectedSource) {
ipcRenderer.send('screen-source-select', selectedSource);
}
}
/**
* Updates the screen picker tabs
*
* @param selectedTab
*/
private toggle(selectedTab: tabs): void {
this.setState({ selectedTab });
}
/**
* Closes the screen picker window
*/
private close(): void {
// setting null will clean up listeners
ipcRenderer.send('screen-source-selected', null);
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeWindow,
windowType: 'screen-picker',
});
}
/**
* Sends the selected source to the main process
* and closes the screen picker window
*/
private submit(): void {
const { selectedSource } = this.state;
if (selectedSource) {
ipcRenderer.send('screen-source-selected', selectedSource);
}
}
/**
* Method handles used key up event
* @param e
*/
private handleKeyUpPress(e): void {
const key = e.keyCode || e.which;
const { sources } = this.state;
switch (key) {
case keyCode.pageDown:
case keyCode.rightArrow:
this.updateSelectedSource(1);
break;
case keyCode.pageUp:
case keyCode.leftArrow:
this.updateSelectedSource(-1);
break;
case keyCode.homeKey:
if (this.currentIndex !== 0) {
this.updateSelectedSource(0);
}
if (this.isScreensAvailable) {
return [
<input
id='screen-tab'
className='ScreenPicker-screen-tab'
type='radio'
name='tabs'
checked={true}
onChange={this.eventHandlers.onToggle('screens')}
/>,
<label className={classNames('screens', { hidden: !this.isScreensAvailable })}
htmlFor='screen-tab'
>
{i18n.t('Screens', SCREEN_PICKER_NAMESPACE)()}
</label>,
];
break;
case keyCode.upArrow:
this.updateSelectedSource(-2);
break;
case keyCode.endKey:
if (this.currentIndex !== sources.length - 1) {
this.updateSelectedSource(sources.length - 1);
}
if (this.isApplicationsAvailable) {
return [
<input
id='application-tab'
className='ScreenPicker-application-tab'
type='radio'
name='tabs'
checked={true}
onChange={this.eventHandlers.onToggle('applications')}
/>,
<label className={classNames('applications', { hidden: !this.isApplicationsAvailable })}
htmlFor='application-tab'
>
{i18n.t('Applications', SCREEN_PICKER_NAMESPACE)()}
</label>,
];
}
return;
break;
case keyCode.arrowDown:
this.updateSelectedSource(2);
break;
case keyCode.enterKey:
this.eventHandlers.onSubmit();
break;
case keyCode.escapeKey:
this.eventHandlers.onClose();
break;
default:
break;
}
}
/**
* Updated the UI selected state based on
* the selected source state
*
* @param index
*/
private updateSelectedSource(index) {
const { sources, selectedSource } = this.state;
if (selectedSource) {
this.currentIndex = sources.findIndex((source) => {
return source.id === selectedSource.id;
});
}
/**
* Updates the selected state
*
* @param id {string}
*/
private shouldHighlight(id: string): boolean {
const { selectedSource } = this.state;
return selectedSource ? id === selectedSource.id : false;
// Find the next item to be selected
const nextIndex =
(this.currentIndex + index + sources.length) % sources.length;
if (sources[nextIndex] && sources[nextIndex].id) {
// Updates the selected source
this.setState({ selectedSource: sources[nextIndex] });
}
}
/**
* updates the state when the source is selected
*
* @param selectedSource {DesktopCapturerSource}
*/
private select(selectedSource: ICustomDesktopCapturerSource): void {
this.setState({ selectedSource });
if (selectedSource) {
ipcRenderer.send('screen-source-select', selectedSource);
}
}
/**
* Updates the screen picker tabs
*
* @param selectedTab
*/
private toggle(selectedTab: tabs): void {
this.setState({ selectedTab });
}
/**
* Closes the screen picker window
*/
private close(): void {
// setting null will clean up listeners
ipcRenderer.send('screen-source-selected', null);
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeWindow,
windowType: 'screen-picker',
});
}
/**
* Sends the selected source to the main process
* and closes the screen picker window
*/
private submit(): void {
const { selectedSource } = this.state;
if (selectedSource) {
ipcRenderer.send('screen-source-selected', selectedSource);
}
}
/**
* Method handles used key up event
* @param e
*/
private handleKeyUpPress(e): void {
const key = e.keyCode || e.which;
const { sources } = this.state;
switch (key) {
case keyCode.pageDown:
case keyCode.rightArrow:
this.updateSelectedSource(1);
break;
case keyCode.pageUp:
case keyCode.leftArrow:
this.updateSelectedSource(-1);
break;
case keyCode.homeKey:
if (this.currentIndex !== 0) {
this.updateSelectedSource(0);
}
break;
case keyCode.upArrow:
this.updateSelectedSource(-2);
break;
case keyCode.endKey:
if (this.currentIndex !== sources.length - 1) {
this.updateSelectedSource(sources.length - 1);
}
break;
case keyCode.arrowDown:
this.updateSelectedSource(2);
break;
case keyCode.enterKey:
this.eventHandlers.onSubmit();
break;
case keyCode.escapeKey:
this.eventHandlers.onClose();
break;
default:
break;
}
}
/**
* Updated the UI selected state based on
* the selected source state
*
* @param index
*/
private updateSelectedSource(index) {
const { sources, selectedSource } = this.state;
if (selectedSource) {
this.currentIndex = sources.findIndex((source) => {
return source.id === selectedSource.id;
});
}
// Find the next item to be selected
const nextIndex = (this.currentIndex + index + sources.length) % sources.length;
if (sources[nextIndex] && sources[nextIndex].id) {
// Updates the selected source
this.setState({ selectedSource: sources[nextIndex] });
}
}
/**
* Sets the component state
*
* @param _event
* @param data {Object}
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
/**
* Sets the component state
*
* @param _event
* @param data {Object}
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
}

View File

@ -5,15 +5,10 @@ import * as React from 'react';
* Window that display app version and copyright info
*/
export default class ScreenSharingFrame extends React.Component<{}> {
/**
* main render function
*/
public render(): JSX.Element {
return (
<div className={classNames('ScreenSharingFrame')}>
</div>
);
}
/**
* main render function
*/
public render(): JSX.Element {
return <div className={classNames('ScreenSharingFrame')}></div>;
}
}

View File

@ -7,87 +7,108 @@ import { isMac } from '../../common/env';
import { i18n } from '../../common/i18n-preload';
interface IState {
id: number;
streamId: string;
id: number;
streamId: string;
}
type mouseEventButton = React.MouseEvent<HTMLButtonElement>;
/**
* Window that display a banner when the users starting sharing screen
*/
export default class ScreenSharingIndicator extends React.Component<{}, IState> {
export default class ScreenSharingIndicator extends React.Component<
{},
IState
> {
private readonly eventHandlers = {
onStopScreenSharing: (id: number) => (_event: mouseEventButton) =>
this.stopScreenShare(id),
onClose: () => this.close(),
};
private readonly eventHandlers = {
onStopScreenSharing: (id: number) => (_event: mouseEventButton) => this.stopScreenShare(id),
onClose: () => this.close(),
constructor(props) {
super(props);
this.state = {
id: 0,
streamId: '',
};
this.updateState = this.updateState.bind(this);
}
constructor(props) {
super(props);
this.state = {
id: 0,
streamId: '',
};
this.updateState = this.updateState.bind(this);
}
/**
* Renders the component
*/
public render(): JSX.Element {
const { id } = this.state;
const namespace = 'ScreenSharingIndicator';
/**
* main render function
*/
public render(): JSX.Element {
const { id } = this.state;
const namespace = 'ScreenSharingIndicator';
return (
<div className={classNames('ScreenSharingIndicator', { mac: isMac })}>
<span className='text-label'>
{i18n
.t(
`You are sharing your screen on {appName}`,
namespace,
)({ appName: remote.app.getName() })
.replace(remote.app.getName(), '')}
<span className='text-label2'>&nbsp;{remote.app.getName()}</span>
</span>
<span className='buttons'>
<button
className='stop-sharing-button'
onClick={this.eventHandlers.onStopScreenSharing(id)}
>
{i18n.t('Stop sharing', namespace)()}
</button>
</span>
</div>
);
}
return (
<div className={classNames('ScreenSharingIndicator', { mac: isMac })}>
<span className='text-label'>{(i18n.t(`You are sharing your screen on {appName}`, namespace)({ appName: remote.app.getName() })).replace(remote.app.getName(), '')}
<span className='text-label2'>&nbsp;{remote.app.getName()}</span>
</span>
<span className='buttons'>
<button className='stop-sharing-button' onClick={this.eventHandlers.onStopScreenSharing(id)}>
{i18n.t('Stop sharing', namespace)()}
</button>
</span>
</div>
);
}
/**
* Callback to handle event when a component is mounted
*/
public componentDidMount(): void {
ipcRenderer.on('screen-sharing-indicator-data', this.updateState);
}
public componentDidMount(): void {
ipcRenderer.on('screen-sharing-indicator-data', this.updateState);
}
/**
* Callback to handle event when a component is unmounted
*/
public componentWillUnmount(): void {
ipcRenderer.removeListener(
'screen-sharing-indicator-data',
this.updateState,
);
}
public componentWillUnmount(): void {
ipcRenderer.removeListener('screen-sharing-indicator-data', this.updateState);
}
/**
* Stops sharing screen
*
* @param id
*/
private stopScreenShare(id: number): void {
ipcRenderer.send('stop-screen-sharing', id);
}
/**
* Stops sharing screen
*
* @param id
*/
private stopScreenShare(id: number): void {
ipcRenderer.send('stop-screen-sharing', id);
}
/**
* Closes the screen sharing indicator window
*/
private close(): void {
const { streamId } = this.state;
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeWindow,
windowType: 'screen-sharing-indicator',
winKey: streamId,
});
}
/**
* Closes the screen sharing indicator window
*/
private close(): void {
const { streamId } = this.state;
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeWindow,
windowType: 'screen-sharing-indicator',
winKey: streamId,
});
}
/**
* Sets the component state
*
* @param _event
* @param data {Object} { buildNumber, clientVersion, version }
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
/**
* Sets the component state
*
* @param _event
* @param data {Object} { buildNumber, clientVersion, version }
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
}

View File

@ -6,79 +6,89 @@ import { i18n } from '../../common/i18n-preload';
const SNACKBAR_NAMESPACE = 'SnackBar';
export default class SnackBar {
private readonly eventHandlers = {
onShowSnackBar: () => this.showSnackBar(),
onRemoveSnackBar: () => this.removeSnackBar(),
};
private readonly eventHandlers = {
onShowSnackBar: () => this.showSnackBar(),
onRemoveSnackBar: () => this.removeSnackBar(),
};
private snackBarTimer: Timer | undefined;
private domParser: DOMParser | undefined;
private body: HTMLCollectionOf<Element> | undefined;
private snackBar: HTMLElement | null = null;
private snackBarTimer: Timer | undefined;
private domParser: DOMParser | undefined;
private body: HTMLCollectionOf<Element> | undefined;
private snackBar: HTMLElement | null = null;
constructor() {
const browserWindow = remote.getCurrentWindow();
if (browserWindow && typeof browserWindow.isDestroyed === 'function' && !browserWindow.isDestroyed()) {
browserWindow.on('enter-full-screen', this.eventHandlers.onShowSnackBar);
browserWindow.on('leave-full-screen', this.eventHandlers.onRemoveSnackBar);
}
constructor() {
const browserWindow = remote.getCurrentWindow();
if (
browserWindow &&
typeof browserWindow.isDestroyed === 'function' &&
!browserWindow.isDestroyed()
) {
browserWindow.on('enter-full-screen', this.eventHandlers.onShowSnackBar);
browserWindow.on(
'leave-full-screen',
this.eventHandlers.onRemoveSnackBar,
);
}
}
/**
* initializes the event listeners
*/
public initSnackBar(): void {
this.body = document.getElementsByTagName('body');
/**
* initializes the event listeners
*/
public initSnackBar(): void {
this.body = document.getElementsByTagName('body');
this.domParser = new DOMParser();
const snackBar = this.domParser.parseFromString(this.render(), 'text/html');
this.snackBar = snackBar.getElementById('snack-bar');
}
this.domParser = new DOMParser();
const snackBar = this.domParser.parseFromString(this.render(), 'text/html');
this.snackBar = snackBar.getElementById('snack-bar');
}
/**
* Displays snackbar for 3sec
*/
public showSnackBar(): void {
setTimeout(() => {
if (this.body && this.body.length > 0 && this.snackBar) {
this.body[ 0 ].appendChild(this.snackBar);
this.snackBar.classList.add('SnackBar-show');
this.snackBarTimer = setTimeout(() => {
if (this.snackBar && this.body) {
if (document.getElementById('snack-bar')) {
this.body[ 0 ].removeChild(this.snackBar);
}
}
}, 3000);
}
}, 100);
}
/**
* Removes snackbar
*/
public removeSnackBar(): void {
if (this.body && this.body.length > 0 && this.snackBar) {
/**
* Displays snackbar for 3sec
*/
public showSnackBar(): void {
setTimeout(() => {
if (this.body && this.body.length > 0 && this.snackBar) {
this.body[0].appendChild(this.snackBar);
this.snackBar.classList.add('SnackBar-show');
this.snackBarTimer = setTimeout(() => {
if (this.snackBar && this.body) {
if (document.getElementById('snack-bar')) {
this.body[ 0 ].removeChild(this.snackBar);
if (this.snackBarTimer) {
clearTimeout(this.snackBarTimer);
}
this.body[0].removeChild(this.snackBar);
}
}
}
}
}, 3000);
}
}, 100);
}
/**
* Renders the custom title bar
*/
public render(): string {
return (
`<div id='snack-bar' class='SnackBar'>
<span >${i18n.t('Press ', SNACKBAR_NAMESPACE)()}</span>
<span class='SnackBar-esc'>${i18n.t('esc', SNACKBAR_NAMESPACE)()}</span>
<span >${i18n.t(' to exit full screen', SNACKBAR_NAMESPACE)()}</span>
</div>`
);
/**
* Removes snackbar
*/
public removeSnackBar(): void {
if (this.body && this.body.length > 0 && this.snackBar) {
if (document.getElementById('snack-bar')) {
this.body[0].removeChild(this.snackBar);
if (this.snackBarTimer) {
clearTimeout(this.snackBarTimer);
}
}
}
}
/**
* Renders the custom title bar
*/
public render(): string {
return `<div id='snack-bar' class='SnackBar'>
<span >${i18n.t('Press ', SNACKBAR_NAMESPACE)()}</span>
<span class='SnackBar-esc'>${i18n.t(
'esc',
SNACKBAR_NAMESPACE,
)()}</span>
<span >${i18n.t(
' to exit full screen',
SNACKBAR_NAMESPACE,
)()}</span>
</div>`;
}
}

View File

@ -2,7 +2,10 @@ import { ipcRenderer } from 'electron';
import * as React from 'react';
import { svgAsPngUri } from 'save-svg-as-png';
import { i18n } from '../../common/i18n-preload';
import { AnalyticsElements, ScreenSnippetActionTypes } from './../../app/analytics-handler';
import {
AnalyticsElements,
ScreenSnippetActionTypes,
} from './../../app/analytics-handler';
import AnnotateArea from './annotate-area';
import ColorPickerPill, { IColor } from './color-picker-pill';
@ -51,11 +54,16 @@ const availableHighlightColors: IColor[] = [
];
const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet';
export const sendAnalyticsToMain = (element: AnalyticsElements, type: ScreenSnippetActionTypes): void => {
export const sendAnalyticsToMain = (
element: AnalyticsElements,
type: ScreenSnippetActionTypes,
): void => {
ipcRenderer.send('snippet-analytics-data', { element, type });
};
const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPaths }) => {
const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({
existingPaths,
}) => {
// State preparation functions
const [screenSnippetPath, setScreenSnippetPath] = useState('');
@ -69,10 +77,12 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
});
const [paths, setPaths] = useState<IPath[]>(existingPaths || []);
const [chosenTool, setChosenTool] = useState(Tool.pen);
const [penColor, setPenColor] = useState<IColor>({ rgbaColor: 'rgba(0, 142, 255, 1)' });
const [highlightColor, setHighlightColor] = useState<IColor>(
{ rgbaColor: 'rgba(0, 142, 255, 0.64)' },
);
const [penColor, setPenColor] = useState<IColor>({
rgbaColor: 'rgba(0, 142, 255, 1)',
});
const [highlightColor, setHighlightColor] = useState<IColor>({
rgbaColor: 'rgba(0, 142, 255, 0.64)',
});
const [
shouldRenderHighlightColorPicker,
setShouldRenderHighlightColorPicker,
@ -81,24 +91,36 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
false,
);
const getSnipImageData = ({ }, {
snipImage,
annotateAreaHeight,
annotateAreaWidth,
snippetImageHeight,
snippetImageWidth,
}) => {
const getSnipImageData = (
{},
{
snipImage,
annotateAreaHeight,
annotateAreaWidth,
snippetImageHeight,
snippetImageWidth,
},
) => {
setScreenSnippetPath(snipImage);
setImageDimensions({ height: snippetImageHeight, width: snippetImageWidth });
setAnnotateAreaDimensions({ height: annotateAreaHeight, width: annotateAreaWidth });
setImageDimensions({
height: snippetImageHeight,
width: snippetImageWidth,
});
setAnnotateAreaDimensions({
height: annotateAreaHeight,
width: annotateAreaWidth,
});
};
useLayoutEffect(() => {
ipcRenderer.once('snipping-tool-data', getSnipImageData);
sendAnalyticsToMain(AnalyticsElements.SCREEN_CAPTURE_ANNOTATE, ScreenSnippetActionTypes.SCREENSHOT_TAKEN);
return () => {
ipcRenderer.removeListener('snipping-tool-data', getSnipImageData);
};
ipcRenderer.once('snipping-tool-data', getSnipImageData);
sendAnalyticsToMain(
AnalyticsElements.SCREEN_CAPTURE_ANNOTATE,
ScreenSnippetActionTypes.SCREENSHOT_TAKEN,
);
return () => {
ipcRenderer.removeListener('snipping-tool-data', getSnipImageData);
};
}, []);
// Hook that alerts clicks outside of the passed refs
@ -166,7 +188,10 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
return p;
});
setPaths(updPaths);
sendAnalyticsToMain(AnalyticsElements.SCREEN_CAPTURE_ANNOTATE, ScreenSnippetActionTypes.ANNOTATE_CLEARED);
sendAnalyticsToMain(
AnalyticsElements.SCREEN_CAPTURE_ANNOTATE,
ScreenSnippetActionTypes.ANNOTATE_CLEARED,
);
};
// Utility functions
@ -189,7 +214,9 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
const color = penColor.outline ? penColor.outline : penColor.rgbaColor;
return { border: '2px solid ' + color };
} else if (chosenTool === Tool.highlight) {
const color = highlightColor.outline ? highlightColor.outline : highlightColor.rgbaColor;
const color = highlightColor.outline
? highlightColor.outline
: highlightColor.rgbaColor;
return { border: '2px solid ' + color };
} else if (chosenTool === Tool.eraser) {
return { border: '2px solid #008EFF' };
@ -199,8 +226,13 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
const done = async () => {
const svg = document.getElementById('annotate-area');
const mergedImageData = svg ? await svgAsPngUri(document.getElementById('annotate-area'), {}) : 'MERGE_FAIL';
sendAnalyticsToMain(AnalyticsElements.SCREEN_CAPTURE_ANNOTATE, ScreenSnippetActionTypes.ANNOTATE_DONE );
const mergedImageData = svg
? await svgAsPngUri(document.getElementById('annotate-area'), {})
: 'MERGE_FAIL';
sendAnalyticsToMain(
AnalyticsElements.SCREEN_CAPTURE_ANNOTATE,
ScreenSnippetActionTypes.ANNOTATE_DONE,
);
ipcRenderer.send('upload-snippet', { screenSnippetPath, mergedImageData });
};
@ -247,35 +279,40 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
</div>
</header>
{
shouldRenderPenColorPicker && (
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
<div style={{ position: 'relative', left: '-50%' }}>
<ColorPickerPill
data-testid='pen-colorpicker'
availableColors={markChosenColor(availablePenColors, penColor.rgbaColor)}
onChange={penColorChosen}
/>
</div>
{shouldRenderPenColorPicker && (
<div
style={{ marginTop: '64px', position: 'absolute', left: '50%' }}
ref={colorPickerRef}
>
<div style={{ position: 'relative', left: '-50%' }}>
<ColorPickerPill
data-testid='pen-colorpicker'
availableColors={markChosenColor(
availablePenColors,
penColor.rgbaColor,
)}
onChange={penColorChosen}
/>
</div>
)
}
{
shouldRenderHighlightColorPicker && (
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
<div style={{ position: 'relative', left: '-50%' }}>
<ColorPickerPill
data-testid='highlight-colorpicker'
availableColors={markChosenColor(
availableHighlightColors,
highlightColor.rgbaColor,
)}
onChange={highlightColorChosen}
/>
</div>
</div>
)}
{shouldRenderHighlightColorPicker && (
<div
style={{ marginTop: '64px', position: 'absolute', left: '50%' }}
ref={colorPickerRef}
>
<div style={{ position: 'relative', left: '-50%' }}>
<ColorPickerPill
data-testid='highlight-colorpicker'
availableColors={markChosenColor(
availableHighlightColors,
highlightColor.rgbaColor,
)}
onChange={highlightColorChosen}
/>
</div>
)
}
</div>
)}
<main>
<div className='imageContainer'>
@ -293,14 +330,11 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPat
</div>
</main>
<footer>
<button
data-testid='done-button'
className='DoneButton'
onClick={done}>
<button data-testid='done-button' className='DoneButton' onClick={done}>
{i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
</button>
</footer>
</div >
</div>
);
};

View File

@ -3,139 +3,161 @@ import * as React from 'react';
import { i18n } from '../../common/i18n-preload';
interface IState {
url: string;
message: string;
urlValid: boolean;
sso: boolean;
url: string;
message: string;
urlValid: boolean;
sso: boolean;
}
const WELCOME_NAMESPACE = 'Welcome';
export default class Welcome extends React.Component<{}, IState> {
private readonly eventHandlers = {
onSetPodUrl: () => this.setPodUrl(),
};
private readonly eventHandlers = {
onSetPodUrl: () => this.setPodUrl(),
constructor(props) {
super(props);
this.state = {
url: 'https://[POD].symphony.com',
message: '',
urlValid: false,
sso: false,
};
this.updateState = this.updateState.bind(this);
}
constructor(props) {
super(props);
this.state = {
url: 'https://[POD].symphony.com',
message: '',
urlValid: false,
sso: false,
};
this.updateState = this.updateState.bind(this);
}
/**
* Render the component
*/
public render(): JSX.Element {
const { url, message, urlValid, sso } = this.state;
return (
<div className='Welcome' lang={i18n.getLocale()}>
<div className='Welcome-image-container'>
<img
src='../renderer/assets/symphony-logo-plain.png'
alt={i18n.t('Symphony Logo', WELCOME_NAMESPACE)()}
/>
</div>
<div className='Welcome-main-container'>
<h3 className='Welcome-name'>{i18n.t('Pod URL', WELCOME_NAMESPACE)()}</h3>
<div className='Welcome-main-container-input-div'>
<div className='Welcome-main-container-input-selection'>
<input className='Welcome-main-container-podurl-box'
type='url' value={url}
onChange={this.updatePodUrl.bind(this)}>
</input>
</div>
<div className='Welcome-main-container-sso-box'
title={i18n.t('Enable Single Sign On', WELCOME_NAMESPACE)()}>
<label>
<input type='checkbox' checked={sso} onChange={this.updateSsoCheckbox.bind(this)}/>
{i18n.t('SSO', WELCOME_NAMESPACE)()}
</label>
</div>
</div>
<label className='Welcome-message-label'>{message}</label>
<button className={!urlValid ? 'Welcome-continue-button-disabled' : 'Welcome-continue-button'}
disabled={!urlValid}
onClick={this.eventHandlers.onSetPodUrl}>
{i18n.t('Continue', WELCOME_NAMESPACE)()}
</button>
</div>
/**
* Render the component
*/
public render(): JSX.Element {
const { url, message, urlValid, sso } = this.state;
return (
<div className='Welcome' lang={i18n.getLocale()}>
<div className='Welcome-image-container'>
<img
src='../renderer/assets/symphony-logo-plain.png'
alt={i18n.t('Symphony Logo', WELCOME_NAMESPACE)()}
/>
</div>
<div className='Welcome-main-container'>
<h3 className='Welcome-name'>
{i18n.t('Pod URL', WELCOME_NAMESPACE)()}
</h3>
<div className='Welcome-main-container-input-div'>
<div className='Welcome-main-container-input-selection'>
<input
className='Welcome-main-container-podurl-box'
type='url'
value={url}
onChange={this.updatePodUrl.bind(this)}
></input>
</div>
);
}
<div
className='Welcome-main-container-sso-box'
title={i18n.t('Enable Single Sign On', WELCOME_NAMESPACE)()}
>
<label>
<input
type='checkbox'
checked={sso}
onChange={this.updateSsoCheckbox.bind(this)}
/>
{i18n.t('SSO', WELCOME_NAMESPACE)()}
</label>
</div>
</div>
<label className='Welcome-message-label'>{message}</label>
<button
className={
!urlValid
? 'Welcome-continue-button-disabled'
: 'Welcome-continue-button'
}
disabled={!urlValid}
onClick={this.eventHandlers.onSetPodUrl}
>
{i18n.t('Continue', WELCOME_NAMESPACE)()}
</button>
</div>
</div>
);
}
/**
* Perform actions on component being mounted
*/
public componentDidMount(): void {
ipcRenderer.on('welcome', this.updateState);
}
/**
* Perform actions on component being mounted
*/
public componentDidMount(): void {
ipcRenderer.on('welcome', this.updateState);
}
/**
* Perform actions on component being unmounted
*/
public componentWillUnmount(): void {
ipcRenderer.removeListener('welcome', this.updateState);
}
/**
* Perform actions on component being unmounted
*/
public componentWillUnmount(): void {
ipcRenderer.removeListener('welcome', this.updateState);
}
/**
* Set pod url and pass it to the main process
*/
public setPodUrl(): void {
const { url, sso } = this.state;
let ssoPath = '/login/sso/initsso';
if (url.endsWith('/')) {
ssoPath = 'login/sso/initsso';
}
ipcRenderer.send('set-pod-url', sso ? `${url}${ssoPath}` : url);
/**
* Set pod url and pass it to the main process
*/
public setPodUrl(): void {
const { url, sso } = this.state;
let ssoPath = '/login/sso/initsso';
if (url.endsWith('/')) {
ssoPath = 'login/sso/initsso';
}
ipcRenderer.send('set-pod-url', sso ? `${url}${ssoPath}` : url);
}
/**
* Update pod url from the text box
* @param _event
*/
public updatePodUrl(_event): void {
const url = _event.target.value.trim();
const match = url.match(/(https?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/g) != null;
if (url === 'https://[POD].symphony.com' || !match) {
this.updateState(_event, {
url,
message: i18n.t('Please enter a valid url', WELCOME_NAMESPACE)(),
urlValid: false,
sso: this.state.sso,
});
return;
}
this.updateState(_event, { url, message: '', urlValid: true, sso: this.state.sso });
/**
* Update pod url from the text box
* @param _event
*/
public updatePodUrl(_event): void {
const url = _event.target.value.trim();
const match =
url.match(
/(https?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/g,
) != null;
if (url === 'https://[POD].symphony.com' || !match) {
this.updateState(_event, {
url,
message: i18n.t('Please enter a valid url', WELCOME_NAMESPACE)(),
urlValid: false,
sso: this.state.sso,
});
return;
}
this.updateState(_event, {
url,
message: '',
urlValid: true,
sso: this.state.sso,
});
}
/**
* Update the SSO checkbox
* @param _event Event occurred upon action
* on the checkbox
*/
public updateSsoCheckbox(_event): void {
const ssoCheckBox = _event.target.checked;
this.updateState(_event, {
url: this.state.url,
message: this.state.message,
urlValid: this.state.urlValid,
sso: ssoCheckBox,
});
}
/**
* Update state
* @param _event
* @param data
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
/**
* Update the SSO checkbox
* @param _event Event occurred upon action
* on the checkbox
*/
public updateSsoCheckbox(_event): void {
const ssoCheckBox = _event.target.checked;
this.updateState(_event, {
url: this.state.url,
message: this.state.message,
urlValid: this.state.urlValid,
sso: ssoCheckBox,
});
}
/**
* Update state
* @param _event
* @param data
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
}

View File

@ -5,338 +5,390 @@ import { apiCmds, apiName } from '../../common/api-interface';
import { i18n } from '../../common/i18n-preload';
interface IState {
title: string;
isMaximized: boolean;
isFullScreen: boolean;
titleBarHeight: string;
title: string;
isMaximized: boolean;
isFullScreen: boolean;
titleBarHeight: string;
}
const TITLE_BAR_NAMESPACE = 'TitleBar';
export default class WindowsTitleBar extends React.Component<{}, IState> {
private readonly window: Electron.BrowserWindow;
private readonly eventHandlers = {
onClose: () => this.close(),
onMaximize: () => this.maximize(),
onMinimize: () => this.minimize(),
onShowMenu: () => this.showMenu(),
onUnmaximize: () => this.unmaximize(),
onDisableContextMenu: (event) => this.disableContextMenu(event),
private readonly window: Electron.BrowserWindow;
private readonly eventHandlers = {
onClose: () => this.close(),
onMaximize: () => this.maximize(),
onMinimize: () => this.minimize(),
onShowMenu: () => this.showMenu(),
onUnmaximize: () => this.unmaximize(),
onDisableContextMenu: (event) => this.disableContextMenu(event),
};
private observer: MutationObserver | undefined;
constructor(props) {
super(props);
this.window = remote.getCurrentWindow();
this.state = {
title: document.title || 'Symphony',
isFullScreen: this.window.isFullScreen(),
isMaximized: this.window.isMaximized(),
titleBarHeight: '32px',
};
private observer: MutationObserver | undefined;
// Adds borders to the window
this.addWindowBorders();
constructor(props) {
super(props);
this.window = remote.getCurrentWindow();
this.state = {
title: document.title || 'Symphony',
isFullScreen: this.window.isFullScreen(),
isMaximized: this.window.isMaximized(),
titleBarHeight: '32px',
};
// Adds borders to the window
this.addWindowBorders();
this.renderMaximizeButtons = this.renderMaximizeButtons.bind(this);
// Event to capture and update icons
this.window.on('maximize', () => this.updateState({ isMaximized: true }));
this.window.on('unmaximize', () =>
this.updateState({ isMaximized: false }),
);
this.window.on('enter-full-screen', () =>
this.updateState({ isFullScreen: true }),
);
this.window.on('leave-full-screen', () =>
this.updateState({ isFullScreen: false }),
);
}
this.renderMaximizeButtons = this.renderMaximizeButtons.bind(this);
// Event to capture and update icons
this.window.on('maximize', () => this.updateState({ isMaximized: true }));
this.window.on('unmaximize', () => this.updateState({ isMaximized: false }));
this.window.on('enter-full-screen', () => this.updateState({ isFullScreen: true }));
this.window.on('leave-full-screen', () => this.updateState({ isFullScreen: false }));
/**
* Callback to handle event when a component is mounted
*/
public componentDidMount(): void {
const target = document.querySelector('title');
this.observer = new MutationObserver((mutations) => {
const title: string = mutations[0].target.textContent
? mutations[0].target.textContent
: 'Symphony';
this.setState({ title });
});
if (target) {
this.observer.observe(target, {
attributes: true,
childList: true,
subtree: true,
characterData: true,
});
}
public componentDidMount(): void {
const target = document.querySelector('title');
this.observer = new MutationObserver((mutations) => {
const title: string = mutations[0].target.textContent ? mutations[0].target.textContent : 'Symphony';
this.setState({ title } );
});
if (target) {
this.observer.observe(target, { attributes: true, childList: true, subtree: true, characterData: true });
setTimeout(() => {
this.updateTitleBar();
}, 10000);
}
/**
* Callback to handle event when a component is unmounted
*/
public componentWillUnmount(): void {
if (this.observer) {
this.observer.disconnect();
}
}
/**
* Renders the component
*/
public render(): JSX.Element | null {
const { title, isFullScreen } = this.state;
const style = { display: isFullScreen ? 'none' : 'flex' };
this.updateTitleBar();
return (
<div
id='title-bar'
onDoubleClick={
this.state.isMaximized
? this.eventHandlers.onUnmaximize
: this.eventHandlers.onMaximize
}
setTimeout(() => {
this.updateTitleBar();
}, 10000);
}
public componentWillUnmount(): void {
if (this.observer) {
this.observer.disconnect();
}
}
/**
* Renders the custom title bar
*/
public render(): JSX.Element | null {
const { title, isFullScreen } = this.state;
const style = { display: isFullScreen ? 'none' : 'flex' };
this.updateTitleBar();
return (
<div id='title-bar'
onDoubleClick={this.state.isMaximized ? this.eventHandlers.onUnmaximize : this.eventHandlers.onMaximize}
style={style}
>
<div className='title-bar-button-container'>
<button
title={i18n.t('Menu', TITLE_BAR_NAMESPACE)()}
className='hamburger-menu-button'
onClick={this.eventHandlers.onShowMenu}
onContextMenu={this.eventHandlers.onDisableContextMenu}
onMouseDown={this.handleMouseDown}
>
<svg x='0px' y='0px' viewBox='0 0 15 10'>
<rect fill='rgba(255, 255, 255, 0.9)' width='15' height='1'/>
<rect fill='rgba(255, 255, 255, 0.9)' y='4' width='15' height='1'/>
<rect fill='rgba(255, 255, 255, 0.9)' y='8' width='152' height='1'/>
</svg>
</button>
</div>
<div className='title-container'>
{this.getSymphonyLogo()}
<p id='title-bar-title'>{title}</p>
</div>
<div className='title-bar-button-container'>
<button
className='title-bar-button'
title={i18n.t('Minimize', TITLE_BAR_NAMESPACE)()}
onClick={this.eventHandlers.onMinimize}
onContextMenu={this.eventHandlers.onDisableContextMenu}
onMouseDown={this.handleMouseDown}
>
<svg x='0px' y='0px' viewBox='0 0 14 1'>
<rect fill='rgba(255, 255, 255, 0.9)' width='14' height='0.6'/>
</svg>
</button>
</div>
<div className='title-bar-button-container'>
{this.renderMaximizeButtons()}
</div>
<div className='title-bar-button-container'>
<button
className='title-bar-button'
title={i18n.t('Close', TITLE_BAR_NAMESPACE)()}
onClick={this.eventHandlers.onClose}
onContextMenu={this.eventHandlers.onDisableContextMenu}
onMouseDown={this.handleMouseDown}
>
<svg x='0px' y='0px' viewBox='0 0 14 10.2'>
<polygon
fill='rgba(255, 255, 255, 0.9)'
points='10.2,0.7 9.5,0 5.1,4.4 0.7,0 0,0.7 4.4,5.1 0,9.5 0.7,10.2 5.1,5.8 9.5,10.2 10.2,9.5 5.8,5.1 '
/>
</svg>
</button>
</div>
<div className='branding-logo' />
</div>
);
}
/**
* Renders maximize or minimize buttons based on fullscreen state
*/
public renderMaximizeButtons(): JSX.Element {
const { isMaximized } = this.state;
if (isMaximized) {
return (
<button
className='title-bar-button'
title={i18n.t('Restore', TITLE_BAR_NAMESPACE)()}
onClick={this.eventHandlers.onUnmaximize}
onContextMenu={this.eventHandlers.onDisableContextMenu}
onMouseDown={this.handleMouseDown}
>
<svg x='0px' y='0px' viewBox='0 0 14 10.2'>
<path
fill='rgba(255, 255, 255, 0.9)'
d='M2.1,0v2H0v8.1h8.2v-2h2V0H2.1z M7.2,9.2H1.1V3h6.1V9.2z M9.2,7.1h-1V2H3.1V1h6.1V7.1z'
/>
</svg>
</button>
);
}
return (
<button
className='title-bar-button'
title={i18n.t('Maximize', TITLE_BAR_NAMESPACE)()}
onClick={this.eventHandlers.onMaximize}
onContextMenu={this.eventHandlers.onDisableContextMenu}
onMouseDown={this.handleMouseDown}
>
<svg x='0px' y='0px' viewBox='0 0 14 10.2'>
<path
fill='rgba(255, 255, 255, 0.9)'
d='M0,0v10.1h10.2V0H0z M9.2,9.2H1.1V1h8.1V9.2z'
/>
</svg>
</button>
);
}
/**
* Method that closes the browser window
*/
public close(): void {
if (this.isValidWindow()) {
this.window.close();
}
}
/**
* Method that minimizes the browser window
*/
public minimize(): void {
if (this.isValidWindow()) {
this.window.minimize();
}
}
/**
* Method that maximize the browser window
*/
public maximize(): void {
if (this.isValidWindow()) {
this.window.maximize();
this.setState({ isMaximized: true });
}
}
/**
* Method that unmaximize the browser window
*/
public unmaximize(): void {
if (this.isValidWindow()) {
this.window.isFullScreen() ? this.window.setFullScreen(false) : this.window.unmaximize();
}
}
/**
* Method that popup the application menu
*/
public showMenu(): void {
if (this.isValidWindow()) {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.popupMenu,
});
}
}
/**
* verifies if the this.window is valid and is not destroyed
*/
public isValidWindow(): boolean {
return (this.window && !this.window.isDestroyed());
}
/**
* Prevent default to make sure buttons don't take focus
* @param e
*/
private handleMouseDown(e) {
e.preventDefault();
}
/**
* Adds borders to the edges of the window chrome
*/
private addWindowBorders() {
const borderBottom = document.createElement('div');
borderBottom.className = 'bottom-window-border';
document.body.appendChild(borderBottom);
document.body.classList.add('window-border');
}
/**
* Modifies the client's DOM content
*/
private updateTitleBar(): void {
const { isFullScreen, titleBarHeight } = this.state;
const contentWrapper = document.getElementById('content-wrapper');
const appView = document.getElementsByClassName('jss1')[0] as HTMLElement;
const root = document.getElementById('root');
const railContainer = document.getElementsByClassName('ReactRail-container-2')[0] as HTMLElement;
const railList = document.getElementsByClassName('railList')[0] as HTMLElement;
if (railContainer) {
railContainer.style.height = isFullScreen ? '100vh' : `calc(100vh - ${titleBarHeight})`;
} else if (railList) {
railList.style.height = isFullScreen ? '100vh' : `calc(100vh - ${titleBarHeight})`;
}
if (!contentWrapper && !root) {
document.body.style.marginTop = isFullScreen ? '0px' : titleBarHeight;
return;
}
if (root) {
const rootChild = root.firstElementChild as HTMLElement;
if (rootChild && rootChild.style && rootChild.style.height === '100vh') {
rootChild.style.height = isFullScreen ? '100vh' : `calc(100vh - ${titleBarHeight})`;
}
root.style.height = isFullScreen ? '100vh' : `calc(100vh - ${titleBarHeight})`;
root.style.marginTop = isFullScreen ? '0px' : titleBarHeight;
} else if (contentWrapper) {
contentWrapper.style.marginTop = isFullScreen ? '0px' : titleBarHeight;
}
if (appView) {
appView.style.height = isFullScreen ? '100vh' : `calc(100vh - ${titleBarHeight})`;
}
if (isFullScreen) {
document.body.style.removeProperty('margin-top');
}
document.body.classList.add('sda-title-bar');
}
/**
* Returns the title bar logo
*/
private getSymphonyLogo(): JSX.Element {
return (
<svg width='20' viewBox='-10 0 60 60' fill='none'>
<path
d='M40 20.111V9.653c0-2.142-1.1-4.153-2.883-5.255C34.458 2.754 28.7 0 20 0 11.3 0 5.542 2.754 2.883 4.407 1.1 5.5 0 7.511 0 9.653v15.705l31.667 9.618v6.995c0 .945-.567 1.61-1.534 2.108L20 49.404 9.808 44.052c-.908-.472-1.475-1.136-1.475-2.08v-5.247L0 34.102v7.87c0 4.319 2.358 7.991 6.108 9.906L20 59.46l13.833-7.546C37.642 49.963 40 46.291 40 41.971V28.855L8.333 19.237v-7.983C10.6 10.108 14.45 8.744 20 8.744s9.4 1.364 11.667 2.51v6.234L40 20.111z'
fill='#0098FF'
/>
<path
d='M40 20.111V9.653c0-2.142-1.1-4.153-2.883-5.255C34.458 2.754 28.7 0 20 0 11.3 0 5.542 2.754 2.883 4.407 1.1 5.5 0 7.511 0 9.653v15.705l31.667 9.618v6.995c0 .945-.567 1.61-1.534 2.108L20 49.404 9.808 44.052c-.908-.472-1.475-1.136-1.475-2.08v-5.247L0 34.102v7.87c0 4.319 2.358 7.991 6.108 9.906L20 59.46l13.833-7.546C37.642 49.963 40 46.291 40 41.971V28.855L8.333 19.237v-7.983C10.6 10.108 14.45 8.744 20 8.744s9.4 1.364 11.667 2.51v6.234L40 20.111z'
fill='url(#prefix__paint0_radial)'
/>
<defs>
<radialGradient
id='prefix__paint0_radial'
cx={0}
cy={0}
r={1}
gradientUnits='userSpaceOnUse'
gradientTransform='matrix(0 40.259 -50.3704 0 20.07 0)'>
<stop stopColor='#fff' stopOpacity={0.4} />
<stop offset={1} stopColor='#fff' stopOpacity={0} />
</radialGradient>
</defs>
style={style}
>
<div className='title-bar-button-container'>
<button
title={i18n.t('Menu', TITLE_BAR_NAMESPACE)()}
className='hamburger-menu-button'
onClick={this.eventHandlers.onShowMenu}
onContextMenu={this.eventHandlers.onDisableContextMenu}
onMouseDown={this.handleMouseDown}
>
<svg x='0px' y='0px' viewBox='0 0 15 10'>
<rect fill='rgba(255, 255, 255, 0.9)' width='15' height='1' />
<rect
fill='rgba(255, 255, 255, 0.9)'
y='4'
width='15'
height='1'
/>
<rect
fill='rgba(255, 255, 255, 0.9)'
y='8'
width='152'
height='1'
/>
</svg>
);
</button>
</div>
<div className='title-container'>
{this.getSymphonyLogo()}
<p id='title-bar-title'>{title}</p>
</div>
<div className='title-bar-button-container'>
<button
className='title-bar-button'
title={i18n.t('Minimize', TITLE_BAR_NAMESPACE)()}
onClick={this.eventHandlers.onMinimize}
onContextMenu={this.eventHandlers.onDisableContextMenu}
onMouseDown={this.handleMouseDown}
>
<svg x='0px' y='0px' viewBox='0 0 14 1'>
<rect fill='rgba(255, 255, 255, 0.9)' width='14' height='0.6' />
</svg>
</button>
</div>
<div className='title-bar-button-container'>
{this.renderMaximizeButtons()}
</div>
<div className='title-bar-button-container'>
<button
className='title-bar-button'
title={i18n.t('Close', TITLE_BAR_NAMESPACE)()}
onClick={this.eventHandlers.onClose}
onContextMenu={this.eventHandlers.onDisableContextMenu}
onMouseDown={this.handleMouseDown}
>
<svg x='0px' y='0px' viewBox='0 0 14 10.2'>
<polygon
fill='rgba(255, 255, 255, 0.9)'
points='10.2,0.7 9.5,0 5.1,4.4 0.7,0 0,0.7 4.4,5.1 0,9.5 0.7,10.2 5.1,5.8 9.5,10.2 10.2,9.5 5.8,5.1 '
/>
</svg>
</button>
</div>
<div className='branding-logo' />
</div>
);
}
/**
* Renders maximize or minimize buttons based on fullscreen state
*/
public renderMaximizeButtons(): JSX.Element {
const { isMaximized } = this.state;
if (isMaximized) {
return (
<button
className='title-bar-button'
title={i18n.t('Restore', TITLE_BAR_NAMESPACE)()}
onClick={this.eventHandlers.onUnmaximize}
onContextMenu={this.eventHandlers.onDisableContextMenu}
onMouseDown={this.handleMouseDown}
>
<svg x='0px' y='0px' viewBox='0 0 14 10.2'>
<path
fill='rgba(255, 255, 255, 0.9)'
d='M2.1,0v2H0v8.1h8.2v-2h2V0H2.1z M7.2,9.2H1.1V3h6.1V9.2z M9.2,7.1h-1V2H3.1V1h6.1V7.1z'
/>
</svg>
</button>
);
}
return (
<button
className='title-bar-button'
title={i18n.t('Maximize', TITLE_BAR_NAMESPACE)()}
onClick={this.eventHandlers.onMaximize}
onContextMenu={this.eventHandlers.onDisableContextMenu}
onMouseDown={this.handleMouseDown}
>
<svg x='0px' y='0px' viewBox='0 0 14 10.2'>
<path
fill='rgba(255, 255, 255, 0.9)'
d='M0,0v10.1h10.2V0H0z M9.2,9.2H1.1V1h8.1V9.2z'
/>
</svg>
</button>
);
}
/**
* Method that closes the browser window
*/
public close(): void {
if (this.isValidWindow()) {
this.window.close();
}
}
/**
* Method that minimizes the browser window
*/
public minimize(): void {
if (this.isValidWindow()) {
this.window.minimize();
}
}
/**
* Method that maximize the browser window
*/
public maximize(): void {
if (this.isValidWindow()) {
this.window.maximize();
this.setState({ isMaximized: true });
}
}
/**
* Method that unmaximize the browser window
*/
public unmaximize(): void {
if (this.isValidWindow()) {
this.window.isFullScreen()
? this.window.setFullScreen(false)
: this.window.unmaximize();
}
}
/**
* Method that popup the application menu
*/
public showMenu(): void {
if (this.isValidWindow()) {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.popupMenu,
});
}
}
/**
* verifies if the this.window is valid and is not destroyed
*/
public isValidWindow(): boolean {
return this.window && !this.window.isDestroyed();
}
/**
* Prevent default to make sure buttons don't take focus
* @param e
*/
private handleMouseDown(e) {
e.preventDefault();
}
/**
* Adds borders to the edges of the window chrome
*/
private addWindowBorders() {
const borderBottom = document.createElement('div');
borderBottom.className = 'bottom-window-border';
document.body.appendChild(borderBottom);
document.body.classList.add('window-border');
}
/**
* Modifies the client's DOM content
*/
private updateTitleBar(): void {
const { isFullScreen, titleBarHeight } = this.state;
const contentWrapper = document.getElementById('content-wrapper');
const appView = document.getElementsByClassName('jss1')[0] as HTMLElement;
const root = document.getElementById('root');
const railContainer = document.getElementsByClassName(
'ReactRail-container-2',
)[0] as HTMLElement;
const railList = document.getElementsByClassName(
'railList',
)[0] as HTMLElement;
if (railContainer) {
railContainer.style.height = isFullScreen
? '100vh'
: `calc(100vh - ${titleBarHeight})`;
} else if (railList) {
railList.style.height = isFullScreen
? '100vh'
: `calc(100vh - ${titleBarHeight})`;
}
if (!contentWrapper && !root) {
document.body.style.marginTop = isFullScreen ? '0px' : titleBarHeight;
return;
}
/**
* Disables context menu for action buttons
*
* @param event
*/
private disableContextMenu(event): boolean {
event.preventDefault();
return false;
if (root) {
const rootChild = root.firstElementChild as HTMLElement;
if (rootChild && rootChild.style && rootChild.style.height === '100vh') {
rootChild.style.height = isFullScreen
? '100vh'
: `calc(100vh - ${titleBarHeight})`;
}
root.style.height = isFullScreen
? '100vh'
: `calc(100vh - ${titleBarHeight})`;
root.style.marginTop = isFullScreen ? '0px' : titleBarHeight;
} else if (contentWrapper) {
contentWrapper.style.marginTop = isFullScreen ? '0px' : titleBarHeight;
}
/**
* Updates the state with the give value
* @param state
*/
private updateState(state: Partial<IState>) {
this.setState((s) => Object.assign(s, state));
if (appView) {
appView.style.height = isFullScreen
? '100vh'
: `calc(100vh - ${titleBarHeight})`;
}
if (isFullScreen) {
document.body.style.removeProperty('margin-top');
}
document.body.classList.add('sda-title-bar');
}
/**
* Returns the title bar logo
*/
private getSymphonyLogo(): JSX.Element {
return (
<svg width='20' viewBox='-10 0 60 60' fill='none'>
<path
d='M40 20.111V9.653c0-2.142-1.1-4.153-2.883-5.255C34.458 2.754 28.7 0 20 0 11.3 0 5.542 2.754 2.883 4.407 1.1 5.5 0 7.511 0 9.653v15.705l31.667 9.618v6.995c0 .945-.567 1.61-1.534 2.108L20 49.404 9.808 44.052c-.908-.472-1.475-1.136-1.475-2.08v-5.247L0 34.102v7.87c0 4.319 2.358 7.991 6.108 9.906L20 59.46l13.833-7.546C37.642 49.963 40 46.291 40 41.971V28.855L8.333 19.237v-7.983C10.6 10.108 14.45 8.744 20 8.744s9.4 1.364 11.667 2.51v6.234L40 20.111z'
fill='#0098FF'
/>
<path
d='M40 20.111V9.653c0-2.142-1.1-4.153-2.883-5.255C34.458 2.754 28.7 0 20 0 11.3 0 5.542 2.754 2.883 4.407 1.1 5.5 0 7.511 0 9.653v15.705l31.667 9.618v6.995c0 .945-.567 1.61-1.534 2.108L20 49.404 9.808 44.052c-.908-.472-1.475-1.136-1.475-2.08v-5.247L0 34.102v7.87c0 4.319 2.358 7.991 6.108 9.906L20 59.46l13.833-7.546C37.642 49.963 40 46.291 40 41.971V28.855L8.333 19.237v-7.983C10.6 10.108 14.45 8.744 20 8.744s9.4 1.364 11.667 2.51v6.234L40 20.111z'
fill='url(#prefix__paint0_radial)'
/>
<defs>
<radialGradient
id='prefix__paint0_radial'
cx={0}
cy={0}
r={1}
gradientUnits='userSpaceOnUse'
gradientTransform='matrix(0 40.259 -50.3704 0 20.07 0)'
>
<stop stopColor='#fff' stopOpacity={0.4} />
<stop offset={1} stopColor='#fff' stopOpacity={0} />
</radialGradient>
</defs>
</svg>
);
}
/**
* Disables context menu for action buttons
*
* @param event
*/
private disableContextMenu(event): boolean {
event.preventDefault();
return false;
}
/**
* Updates the state with the give value
* @param state
*/
private updateState(state: Partial<IState>) {
this.setState((s) => {
return { ...s, ...state };
});
}
}

View File

@ -1,12 +1,16 @@
import {
desktopCapturer,
DesktopCapturerSource,
ipcRenderer,
remote,
SourcesOptions,
desktopCapturer,
DesktopCapturerSource,
ipcRenderer,
remote,
SourcesOptions,
} from 'electron';
import { apiCmds, apiName, NOTIFICATION_WINDOW_TITLE } from '../common/api-interface';
import {
apiCmds,
apiName,
NOTIFICATION_WINDOW_TITLE,
} from '../common/api-interface';
import { isWindowsOS } from '../common/env';
import { i18n } from '../common/i18n-preload';
@ -17,20 +21,23 @@ let isScreenShareEnabled = false;
let screenShareArgv: string;
export interface ICustomSourcesOptions extends SourcesOptions {
requestId?: number;
requestId?: number;
}
export interface ICustomDesktopCapturerSource extends DesktopCapturerSource {
requestId: number | undefined;
requestId: number | undefined;
}
export interface IScreenSourceError {
name: string;
message: string;
requestId: number | undefined;
name: string;
message: string;
requestId: number | undefined;
}
export type CallbackType = (error: IScreenSourceError | null, source?: ICustomDesktopCapturerSource) => void;
export type CallbackType = (
error: IScreenSourceError | null,
source?: ICustomDesktopCapturerSource,
) => void;
const getNextId = () => ++nextId;
/**
@ -39,7 +46,10 @@ const getNextId = () => ++nextId;
* @returns {boolean}
*/
const isValid = (options: ICustomSourcesOptions) => {
return ((options !== null ? options.types : undefined) !== null) && Array.isArray(options.types);
return (
(options !== null ? options.types : undefined) !== null &&
Array.isArray(options.types)
);
};
/**
@ -49,113 +59,138 @@ const isValid = (options: ICustomSourcesOptions) => {
* @param callback {CallbackType}
* @returns {*}
*/
export const getSource = async (options: ICustomSourcesOptions, callback: CallbackType) => {
let captureWindow;
let captureScreen;
let id;
const sourcesOpts: string[] = [];
const { requestId, ...updatedOptions } = options;
if (!isValid(options)) {
callback({ name: 'Invalid options', message: 'Invalid options', requestId });
return;
}
captureWindow = includes.call(options.types, 'window');
captureScreen = includes.call(options.types, 'screen');
if (!updatedOptions.thumbnailSize) {
updatedOptions.thumbnailSize = {
height: 150,
width: 150,
};
}
if (isWindowsOS && captureWindow) {
/**
* Sets the captureWindow to false if Desktop composition
* is disabled otherwise true
*
* Setting captureWindow to false returns only screen sources
* @type {boolean}
*/
captureWindow = remote.systemPreferences.isAeroGlassEnabled();
}
if (captureWindow) {
sourcesOpts.push('window');
}
if (captureScreen) {
sourcesOpts.push('screen');
}
// displays a dialog if media permissions are disable
if (!isScreenShareEnabled) {
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
if (focusedWindow && !focusedWindow.isDestroyed()) {
remote.dialog.showMessageBox(focusedWindow, {
message: `${i18n.t('Your administrator has disabled sharing your screen. Please contact your admin for help', 'Permissions')()}`,
title: `${i18n.t('Permission Denied')()}!`,
type: 'error',
});
callback({ name: 'Permission Denied', message: 'Permission Denied', requestId });
return;
}
}
id = getNextId();
const sources: DesktopCapturerSource[] = await desktopCapturer.getSources({ types: sourcesOpts, thumbnailSize: updatedOptions.thumbnailSize });
// Auto select screen source based on args for testing only
if (screenShareArgv) {
const title = screenShareArgv.substr(screenShareArgv.indexOf('=') + 1);
const filteredSource: DesktopCapturerSource[] = sources.filter((source) => source.name === title);
if (Array.isArray(filteredSource) && filteredSource.length > 0) {
const source = { ...filteredSource[ 0 ], requestId };
return callback(null, source);
}
if (sources.length > 0) {
const firstSource = { ...sources[ 0 ], requestId };
return callback(null, firstSource);
}
}
const updatedSources = sources
.filter((source) => source.name !== NOTIFICATION_WINDOW_TITLE)
.map((source) => {
return Object.assign({}, source, {
thumbnail: source.thumbnail.toDataURL(),
});
export const getSource = async (
options: ICustomSourcesOptions,
callback: CallbackType,
) => {
let captureWindow;
let captureScreen;
let id;
const sourcesOpts: string[] = [];
const { requestId, ...updatedOptions } = options;
if (!isValid(options)) {
callback({
name: 'Invalid options',
message: 'Invalid options',
requestId,
});
return;
}
captureWindow = includes.call(options.types, 'window');
captureScreen = includes.call(options.types, 'screen');
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.openScreenPickerWindow,
id,
sources: updatedSources,
});
const successCallback = (_e, source: DesktopCapturerSource) => {
// Cleaning up the event listener to prevent memory leaks
if (!source) {
ipcRenderer.removeListener('start-share' + id, successCallback);
return callback({ name: 'User Cancelled', message: 'User Cancelled', requestId });
}
return callback(null, { ...source, ...{ requestId } });
if (!updatedOptions.thumbnailSize) {
updatedOptions.thumbnailSize = {
height: 150,
width: 150,
};
ipcRenderer.once('start-share' + id, successCallback);
return null;
}
if (isWindowsOS && captureWindow) {
/**
* Sets the captureWindow to false if Desktop composition
* is disabled otherwise true
*
* Setting captureWindow to false returns only screen sources
* @type {boolean}
*/
captureWindow = remote.systemPreferences.isAeroGlassEnabled();
}
if (captureWindow) {
sourcesOpts.push('window');
}
if (captureScreen) {
sourcesOpts.push('screen');
}
// displays a dialog if media permissions are disable
if (!isScreenShareEnabled) {
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
if (focusedWindow && !focusedWindow.isDestroyed()) {
remote.dialog.showMessageBox(focusedWindow, {
message: `${i18n.t(
'Your administrator has disabled sharing your screen. Please contact your admin for help',
'Permissions',
)()}`,
title: `${i18n.t('Permission Denied')()}!`,
type: 'error',
});
callback({
name: 'Permission Denied',
message: 'Permission Denied',
requestId,
});
return;
}
}
id = getNextId();
const sources: DesktopCapturerSource[] = await desktopCapturer.getSources({
types: sourcesOpts,
thumbnailSize: updatedOptions.thumbnailSize,
});
// Auto select screen source based on args for testing only
if (screenShareArgv) {
const title = screenShareArgv.substr(screenShareArgv.indexOf('=') + 1);
const filteredSource: DesktopCapturerSource[] = sources.filter(
(source) => source.name === title,
);
if (Array.isArray(filteredSource) && filteredSource.length > 0) {
const source = { ...filteredSource[0], requestId };
return callback(null, source);
}
if (sources.length > 0) {
const firstSource = { ...sources[0], requestId };
return callback(null, firstSource);
}
}
const updatedSources = sources
.filter((source) => source.name !== NOTIFICATION_WINDOW_TITLE)
.map((source) => {
return {
...source,
...{
thumbnail: source.thumbnail.toDataURL(),
},
};
});
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.openScreenPickerWindow,
id,
sources: updatedSources,
});
const successCallback = (_e, source: DesktopCapturerSource) => {
// Cleaning up the event listener to prevent memory leaks
if (!source) {
ipcRenderer.removeListener('start-share' + id, successCallback);
return callback({
name: 'User Cancelled',
message: 'User Cancelled',
requestId,
});
}
return callback(null, { ...source, ...{ requestId } });
};
ipcRenderer.once('start-share' + id, successCallback);
return null;
};
// event that updates screen share argv
ipcRenderer.once('screen-share-argv', (_event, arg) => {
if (typeof arg === 'string') {
screenShareArgv = arg;
}
if (typeof arg === 'string') {
screenShareArgv = arg;
}
});
// event that updates screen share permission
ipcRenderer.on('is-screen-share-enabled', (_event, canShareScreen) => {
if (typeof canShareScreen === 'boolean' && canShareScreen) {
isScreenShareEnabled = canShareScreen;
}
if (typeof canShareScreen === 'boolean' && canShareScreen) {
isScreenShareEnabled = canShareScreen;
}
});

View File

@ -1,29 +1,28 @@
import * as asyncMap from 'async.map';
import { app } from 'electron';
import * as electron from 'electron';
import { app, screen } from 'electron';
import { windowExists } from '../app/window-utils';
import { isLinux, isMac } from '../common/env';
interface ISettings {
startCorner: startCorner;
displayId: string;
height: number;
width: number;
totalHeight: number;
totalWidth: number;
corner: ICorner;
firstPos: ICorner;
maxVisibleNotifications: number;
animationSteps: number;
animationStepMs: number;
spacing: number;
differentialHeight: number;
startCorner: startCorner;
displayId: string;
height: number;
width: number;
totalHeight: number;
totalWidth: number;
corner: ICorner;
firstPos: ICorner;
maxVisibleNotifications: number;
animationSteps: number;
animationStepMs: number;
spacing: number;
differentialHeight: number;
}
interface ICorner {
x: number;
y: number;
x: number;
y: number;
}
type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left';
@ -31,274 +30,299 @@ const NEXT_INSERT_POSITION = 108;
const NEXT_INSERT_POSITION_WITH_INPUT = 140;
export default class NotificationHandler {
public settings: ISettings;
public nextInsertPos: ICorner = { x: 0, y: 0 };
public settings: ISettings;
public nextInsertPos: ICorner = { x: 0, y: 0 };
private readonly eventHandlers = {
onSetup: () => this.setupNotificationPosition(),
private readonly eventHandlers = {
onSetup: () => this.setupNotificationPosition(),
};
private externalDisplay: Electron.Display | undefined;
constructor(opts) {
this.settings = opts as ISettings;
this.setupNotificationPosition();
app.once('ready', () => {
screen.on('display-added', this.eventHandlers.onSetup);
screen.on('display-removed', this.eventHandlers.onSetup);
screen.on('display-metrics-changed', this.eventHandlers.onSetup);
});
}
/**
* Sets the position of the notification window
*
* @param window {BrowserWindow}
* @param x {number}
* @param y {number}
*/
public setWindowPosition(
window: Electron.BrowserWindow,
x: number = 0,
y: number = 0,
) {
if (window && !window.isDestroyed()) {
window.setPosition(parseInt(String(x), 10), parseInt(String(y), 10));
}
}
/**
* Initializes / resets the notification positional values
*/
public setupNotificationPosition() {
// This feature only applies to windows & mac
if (!app.isReady()) {
return;
}
const screens = screen.getAllDisplays();
if (screens && screens.length >= 0) {
this.externalDisplay = screens.find((screen) => {
const screenId = screen.id.toString();
return screenId === this.settings.displayId;
});
}
const display = this.externalDisplay || screen.getPrimaryDisplay();
this.settings.corner.x = display.workArea.x;
this.settings.corner.y = display.workArea.y;
// update corner x/y based on corner of screen where notification should appear
const workAreaWidth = display.workAreaSize.width;
const workAreaHeight = display.workAreaSize.height;
const offSet = isMac || isLinux ? 20 : 10;
switch (this.settings.startCorner) {
case 'upper-right':
this.settings.corner.x += workAreaWidth - offSet;
this.settings.corner.y += offSet;
break;
case 'lower-right':
this.settings.corner.x += workAreaWidth - offSet;
this.settings.corner.y += workAreaHeight - offSet;
break;
case 'lower-left':
this.settings.corner.x += offSet;
this.settings.corner.y += workAreaHeight - offSet;
break;
case 'upper-left':
this.settings.corner.x += offSet;
this.settings.corner.y += offSet;
break;
default:
// no change needed
break;
}
this.calculateDimensions();
// Maximum amount of Notifications we can show:
this.settings.maxVisibleNotifications = Math.floor(
display.workAreaSize.height / this.settings.totalHeight,
);
}
/**
* Find next possible insert position (on top)
*/
public calcNextInsertPos(activeNotifications) {
let nextNotificationY: number = 0;
activeNotifications.forEach((notification) => {
if (notification && windowExists(notification)) {
const [, height] = notification.getSize();
nextNotificationY +=
height > this.settings.height
? NEXT_INSERT_POSITION_WITH_INPUT
: NEXT_INSERT_POSITION;
}
});
if (activeNotifications.length < this.settings.maxVisibleNotifications) {
switch (this.settings.startCorner) {
case 'upper-right':
case 'upper-left':
this.nextInsertPos.y = this.settings.corner.y + nextNotificationY;
break;
default:
case 'lower-right':
case 'lower-left':
this.nextInsertPos.y =
this.settings.corner.y - (nextNotificationY + NEXT_INSERT_POSITION);
break;
}
}
}
/**
* Moves the notification by one step
*
* @param startPos {number}
* @param activeNotifications {ICustomBrowserWindow[]}
* @param height {number} height of the closed notification
* @param isReset {boolean} whether to reset all notification position
*/
public moveNotificationDown(
startPos,
activeNotifications,
height: number = 0,
isReset: boolean = false,
) {
if (startPos >= activeNotifications || startPos === -1) {
return;
}
// Build array with index of affected notifications
const notificationPosArray: number[] = [];
for (let i = startPos; i < activeNotifications.length; i++) {
notificationPosArray.push(i);
}
asyncMap(notificationPosArray, (i, done) => {
// Get notification to move
const notificationWindow = activeNotifications[i];
if (!windowExists(notificationWindow)) {
return;
}
const [, y] = notificationWindow.getPosition();
// Calc new y position
let newY;
switch (this.settings.startCorner) {
case 'upper-right':
case 'upper-left':
newY = isReset
? this.settings.corner.y + this.settings.totalHeight * i
: y - height - this.settings.spacing;
break;
default:
case 'lower-right':
case 'lower-left':
newY = isReset
? this.settings.corner.y - this.settings.totalHeight * (i + 1)
: y + height + this.settings.spacing;
break;
}
this.animateNotificationPosition(notificationWindow, newY, done);
});
}
/**
* Moves the notification by one step
*
* @param startPos {number}
* @param activeNotifications {ICustomBrowserWindow[]}
*/
public moveNotificationUp(startPos, activeNotifications) {
if (startPos >= activeNotifications || startPos === -1) {
return;
}
if (
this.settings.startCorner === 'lower-right' ||
this.settings.startCorner === 'lower-left'
) {
startPos -= 1;
}
// Build array with index of affected notifications
const notificationPosArray: number[] = [];
for (let i = startPos; i < activeNotifications.length; i++) {
notificationPosArray.push(i);
}
asyncMap(notificationPosArray, (i, done) => {
// Get notification to move
const notificationWindow = activeNotifications[i];
if (!windowExists(notificationWindow)) {
return;
}
const [, y] = notificationWindow.getPosition();
// Calc new y position
let newY;
switch (this.settings.startCorner) {
case 'upper-right':
case 'upper-left':
newY = y + this.settings.differentialHeight;
break;
default:
case 'lower-right':
case 'lower-left':
newY = y - this.settings.differentialHeight;
break;
}
this.animateNotificationPosition(notificationWindow, newY, done);
});
}
/**
* Get startPos, calc step size and start animationInterval
* @param notificationWindow
* @param newY
* @param done
* @private
*/
private animateNotificationPosition(notificationWindow, newY, done) {
const startY = notificationWindow.getPosition()[1];
const step = (newY - startY) / this.settings.animationSteps;
let curStep = 1;
const animationInterval = setInterval(() => {
// Abort condition
if (curStep === this.settings.animationSteps) {
this.setWindowPosition(
notificationWindow,
this.settings.firstPos.x,
newY,
);
clearInterval(animationInterval);
done(null, 'done');
return;
}
// Move one step down
this.setWindowPosition(
notificationWindow,
this.settings.firstPos.x,
startY + curStep * step,
);
curStep++;
}, this.settings.animationStepMs);
}
/**
* Calculates the first and next notification insert position
*/
private calculateDimensions() {
const vertSpace = 8;
// Calc totalHeight & totalWidth
this.settings.totalHeight = this.settings.height + vertSpace;
this.settings.totalWidth = this.settings.width;
let firstPosX;
let firstPosY;
switch (this.settings.startCorner) {
case 'upper-right':
firstPosX = this.settings.corner.x - this.settings.totalWidth;
firstPosY = this.settings.corner.y;
break;
case 'lower-right':
firstPosX = this.settings.corner.x - this.settings.totalWidth;
firstPosY = this.settings.corner.y - this.settings.totalHeight;
break;
case 'lower-left':
firstPosX = this.settings.corner.x;
firstPosY = this.settings.corner.y - this.settings.totalHeight;
break;
case 'upper-left':
default:
firstPosX = this.settings.corner.x;
firstPosY = this.settings.corner.y;
break;
}
// Calc pos of first notification:
this.settings.firstPos = {
x: firstPosX,
y: firstPosY,
};
private externalDisplay: Electron.Display | undefined;
constructor(opts) {
this.settings = opts as ISettings;
this.setupNotificationPosition();
app.once('ready', () => {
electron.screen.on('display-added', this.eventHandlers.onSetup);
electron.screen.on('display-removed', this.eventHandlers.onSetup);
electron.screen.on('display-metrics-changed', this.eventHandlers.onSetup);
});
}
/**
* Sets the position of the notification window
*
* @param window {BrowserWindow}
* @param x {number}
* @param y {number}
*/
public setWindowPosition(window: Electron.BrowserWindow, x: number = 0, y: number = 0) {
if (window && !window.isDestroyed()) {
window.setPosition(parseInt(String(x), 10), parseInt(String(y), 10));
}
}
/**
* Initializes / resets the notification positional values
*/
public setupNotificationPosition() {
// This feature only applies to windows & mac
if (!app.isReady()) {
return;
}
const screens = electron.screen.getAllDisplays();
if (screens && screens.length >= 0) {
this.externalDisplay = screens.find((screen) => {
const screenId = screen.id.toString();
return screenId === this.settings.displayId;
});
}
const display = this.externalDisplay || electron.screen.getPrimaryDisplay();
this.settings.corner.x = display.workArea.x;
this.settings.corner.y = display.workArea.y;
// update corner x/y based on corner of screen where notification should appear
const workAreaWidth = display.workAreaSize.width;
const workAreaHeight = display.workAreaSize.height;
const offSet = (isMac || isLinux ? 20 : 10);
switch (this.settings.startCorner) {
case 'upper-right':
this.settings.corner.x += workAreaWidth - offSet;
this.settings.corner.y += offSet;
break;
case 'lower-right':
this.settings.corner.x += workAreaWidth - offSet;
this.settings.corner.y += workAreaHeight - offSet;
break;
case 'lower-left':
this.settings.corner.x += offSet;
this.settings.corner.y += workAreaHeight - offSet;
break;
case 'upper-left':
this.settings.corner.x += offSet;
this.settings.corner.y += offSet;
break;
default:
// no change needed
break;
}
this.calculateDimensions();
// Maximum amount of Notifications we can show:
this.settings.maxVisibleNotifications = Math.floor(display.workAreaSize.height / this.settings.totalHeight);
}
/**
* Find next possible insert position (on top)
*/
public calcNextInsertPos(activeNotifications) {
let nextNotificationY: number = 0;
activeNotifications.forEach((notification) => {
if (notification && windowExists(notification)) {
const [, height] = notification.getSize();
nextNotificationY += height > this.settings.height ? NEXT_INSERT_POSITION_WITH_INPUT : NEXT_INSERT_POSITION;
}
});
if (activeNotifications.length < this.settings.maxVisibleNotifications) {
switch (this.settings.startCorner) {
case 'upper-right':
case 'upper-left':
this.nextInsertPos.y = this.settings.corner.y + nextNotificationY;
break;
default:
case 'lower-right':
case 'lower-left':
this.nextInsertPos.y = this.settings.corner.y - (nextNotificationY + NEXT_INSERT_POSITION);
break;
}
}
}
/**
* Moves the notification by one step
*
* @param startPos {number}
* @param activeNotifications {ICustomBrowserWindow[]}
* @param height {number} height of the closed notification
* @param isReset {boolean} whether to reset all notification position
*/
public moveNotificationDown(startPos, activeNotifications, height: number = 0, isReset: boolean = false) {
if (startPos >= activeNotifications || startPos === -1) {
return;
}
// Build array with index of affected notifications
const notificationPosArray: number[] = [];
for (let i = startPos; i < activeNotifications.length; i++) {
notificationPosArray.push(i);
}
asyncMap(notificationPosArray, (i, done) => {
// Get notification to move
const notificationWindow = activeNotifications[i];
if (!windowExists(notificationWindow)) {
return;
}
const [, y] = notificationWindow.getPosition();
// Calc new y position
let newY;
switch (this.settings.startCorner) {
case 'upper-right':
case 'upper-left':
newY = isReset
? this.settings.corner.y + (this.settings.totalHeight * i)
: (y - height - this.settings.spacing);
break;
default:
case 'lower-right':
case 'lower-left':
newY = isReset
? this.settings.corner.y - (this.settings.totalHeight * (i + 1))
: (y + height + this.settings.spacing);
break;
}
this.animateNotificationPosition(notificationWindow, newY, done);
});
}
/**
* Moves the notification by one step
*
* @param startPos {number}
* @param activeNotifications {ICustomBrowserWindow[]}
*/
public moveNotificationUp(startPos, activeNotifications) {
if (startPos >= activeNotifications || startPos === -1) {
return;
}
if (this.settings.startCorner === 'lower-right' || this.settings.startCorner === 'lower-left') {
startPos -= 1;
}
// Build array with index of affected notifications
const notificationPosArray: number[] = [];
for (let i = startPos; i < activeNotifications.length; i++) {
notificationPosArray.push(i);
}
asyncMap(notificationPosArray, (i, done) => {
// Get notification to move
const notificationWindow = activeNotifications[i];
if (!windowExists(notificationWindow)) {
return;
}
const [, y] = notificationWindow.getPosition();
// Calc new y position
let newY;
switch (this.settings.startCorner) {
case 'upper-right':
case 'upper-left':
newY = y + this.settings.differentialHeight;
break;
default:
case 'lower-right':
case 'lower-left':
newY = y - this.settings.differentialHeight;
break;
}
this.animateNotificationPosition(notificationWindow, newY, done);
});
}
/**
* Get startPos, calc step size and start animationInterval
* @param notificationWindow
* @param newY
* @param done
* @private
*/
private animateNotificationPosition(notificationWindow, newY, done) {
const startY = notificationWindow.getPosition()[1];
const step = (newY - startY) / this.settings.animationSteps;
let curStep = 1;
const animationInterval = setInterval(() => {
// Abort condition
if (curStep === this.settings.animationSteps) {
this.setWindowPosition(notificationWindow, this.settings.firstPos.x, newY);
clearInterval(animationInterval);
done(null, 'done');
return;
}
// Move one step down
this.setWindowPosition(notificationWindow, this.settings.firstPos.x, startY + curStep * step);
curStep++;
}, this.settings.animationStepMs);
}
/**
* Calculates the first and next notification insert position
*/
private calculateDimensions() {
const vertSpace = 8;
// Calc totalHeight & totalWidth
this.settings.totalHeight = this.settings.height + vertSpace;
this.settings.totalWidth = this.settings.width;
let firstPosX;
let firstPosY;
switch (this.settings.startCorner) {
case 'upper-right':
firstPosX = this.settings.corner.x - this.settings.totalWidth;
firstPosY = this.settings.corner.y;
break;
case 'lower-right':
firstPosX = this.settings.corner.x - this.settings.totalWidth;
firstPosY = this.settings.corner.y - this.settings.totalHeight;
break;
case 'lower-left':
firstPosX = this.settings.corner.x;
firstPosY = this.settings.corner.y - this.settings.totalHeight;
break;
case 'upper-left':
default:
firstPosX = this.settings.corner.x;
firstPosY = this.settings.corner.y;
break;
}
// Calc pos of first notification:
this.settings.firstPos = {
x: firstPosX,
y: firstPosY,
};
// Set nextInsertPos
this.nextInsertPos.x = this.settings.firstPos.x;
this.nextInsertPos.y = this.settings.firstPos.y;
}
// Set nextInsertPos
this.nextInsertPos.x = this.settings.firstPos.x;
this.nextInsertPos.y = this.settings.firstPos.y;
}
}

View File

@ -1,6 +1,9 @@
import { remote } from 'electron';
import { INotificationData, NotificationActions } from '../common/api-interface';
import {
INotificationData,
NotificationActions,
} from '../common/api-interface';
const notification = remote.require('../renderer/notification').notification;
let latestID = 0;
@ -10,80 +13,89 @@ let latestID = 0;
* this class is to mock the Window.Notification interface
*/
export default class SSFNotificationHandler {
public _data: INotificationData;
public _data: INotificationData;
private readonly id: number;
private readonly eventHandlers = {
onClick: (event: NotificationActions, _data: INotificationData) =>
this.notificationClicked(event),
};
private notificationClickCallback: (({ target }) => {}) | undefined;
private notificationCloseCallback: (({ target }) => {}) | undefined;
private readonly id: number;
private readonly eventHandlers = {
onClick: (event: NotificationActions, _data: INotificationData) => this.notificationClicked(event),
};
private notificationClickCallback: (({ target }) => {}) | undefined;
private notificationCloseCallback: (({ target }) => {}) | undefined;
constructor(title, options) {
this.id = latestID;
latestID++;
notification.showNotification(
{ ...options, title, id: this.id },
this.eventHandlers.onClick,
);
this._data = options.data;
}
constructor(title, options) {
this.id = latestID;
latestID++;
notification.showNotification({ ...options, title, id: this.id }, this.eventHandlers.onClick);
this._data = options.data;
/**
* Closes notification
*/
public close(): void {
notification.hideNotification(this.id);
}
/**
* Always allow showing notifications.
* @return {string} 'granted'
*/
static get permission(): string {
return 'granted';
}
/**
* Returns data object passed in via constructor options
*/
get data(): INotificationData {
return this._data;
}
/**
* Adds event listeners for 'click', 'close', 'show', 'error' events
*
* @param {String} event event to listen for
* @param {func} cb callback invoked when event occurs
*/
public addEventListener(event: string, cb: () => {}): void {
if (event && typeof cb === 'function') {
switch (event) {
case 'click':
this.notificationClickCallback = cb;
break;
case 'close':
this.notificationCloseCallback = cb;
break;
}
}
}
/**
* Closes notification
*/
public close(): void {
notification.hideNotification(this.id);
}
/**
* Always allow showing notifications.
* @return {string} 'granted'
*/
static get permission(): string {
return 'granted';
}
/**
* Returns data object passed in via constructor options
*/
get data(): INotificationData {
return this._data;
}
/**
* Adds event listeners for 'click', 'close', 'show', 'error' events
*
* @param {String} event event to listen for
* @param {func} cb callback invoked when event occurs
*/
public addEventListener(event: string, cb: () => {}): void {
if (event && typeof cb === 'function') {
switch (event) {
case 'click':
this.notificationClickCallback = cb;
break;
case 'close':
this.notificationCloseCallback = cb;
break;
}
}
}
/**
* Handles the callback based on the event name
*
* @param event {NotificationActions}
*/
private notificationClicked(event: NotificationActions): void {
switch (event) {
case NotificationActions.notificationClicked:
if (this.notificationClickCallback && typeof this.notificationClickCallback === 'function') {
this.notificationClickCallback({ target: this });
}
break;
case NotificationActions.notificationClosed:
if (this.notificationCloseCallback && typeof this.notificationCloseCallback === 'function') {
this.notificationCloseCallback({ target: this });
}
/**
* Handles the callback based on the event name
*
* @param event {NotificationActions}
*/
private notificationClicked(event: NotificationActions): void {
switch (event) {
case NotificationActions.notificationClicked:
if (
this.notificationClickCallback &&
typeof this.notificationClickCallback === 'function'
) {
this.notificationClickCallback({ target: this });
}
break;
case NotificationActions.notificationClosed:
if (
this.notificationCloseCallback &&
typeof this.notificationCloseCallback === 'function'
) {
this.notificationCloseCallback({ target: this });
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -14,86 +14,89 @@ import SnippingTool from './components/snipping-tool';
import Welcome from './components/welcome';
const enum components {
aboutApp = 'about-app',
screenPicker = 'screen-picker',
screenSharingIndicator = 'screen-sharing-indicator',
screenSharingFrame = 'screen-sharing-frame',
basicAuth = 'basic-auth',
notification = 'notification-comp',
notificationSettings = 'notification-settings',
welcome = 'welcome',
snippingTool = 'snipping-tool',
aboutApp = 'about-app',
screenPicker = 'screen-picker',
screenSharingIndicator = 'screen-sharing-indicator',
screenSharingFrame = 'screen-sharing-frame',
basicAuth = 'basic-auth',
notification = 'notification-comp',
notificationSettings = 'notification-settings',
welcome = 'welcome',
snippingTool = 'snipping-tool',
}
const loadStyle = (style) => {
const styles = document.createElement('link');
styles.rel = 'stylesheet';
styles.type = 'text/css';
styles.href = `./styles/${style}.css`;
document.getElementsByTagName('head')[0].appendChild(styles);
const styles = document.createElement('link');
styles.rel = 'stylesheet';
styles.type = 'text/css';
styles.href = `./styles/${style}.css`;
document.getElementsByTagName('head')[0].appendChild(styles);
};
/**
* Loads the appropriate component
*/
const load = () => {
const query = new URL(window.location.href).searchParams;
const componentName = query.get('componentName');
const query = new URL(window.location.href).searchParams;
const componentName = query.get('componentName');
let component;
switch (componentName) {
case components.aboutApp:
loadStyle(components.aboutApp);
component = AboutBox;
document.title = i18n.t('About Symphony', 'AboutSymphony')();
break;
case components.screenPicker:
loadStyle(components.screenPicker);
document.title = i18n.t('Screen Picker - Symphony')();
component = ScreenPicker;
break;
case components.screenSharingIndicator:
loadStyle(components.screenSharingIndicator);
document.title = i18n.t('Screen Sharing Indicator - Symphony')();
component = ScreenSharingIndicator;
break;
case components.screenSharingFrame:
loadStyle(components.screenSharingFrame);
component = ScreenSharingFrame;
break;
case components.snippingTool:
loadStyle(components.snippingTool);
document.title = i18n.t('Symphony')();
component = SnippingTool;
break;
case components.basicAuth:
loadStyle(components.basicAuth);
document.title = i18n.t('Basic Authentication - Symphony')();
component = BasicAuth;
break;
case components.notification:
loadStyle(components.notification);
document.title = i18n.t('Notification - Symphony')();
component = NotificationComp;
break;
case components.notificationSettings:
document.title = i18n.t('Notification Settings - Symphony', 'NotificationSettings')();
loadStyle(components.notificationSettings);
component = NotificationSettings;
break;
case components.welcome:
document.title = i18n.t('WelcomeText', 'Welcome')();
loadStyle(components.welcome);
component = Welcome;
break;
}
const element = React.createElement(component);
ReactDOM.render(element, document.getElementById('Root'));
let component;
switch (componentName) {
case components.aboutApp:
loadStyle(components.aboutApp);
component = AboutBox;
document.title = i18n.t('About Symphony', 'AboutSymphony')();
break;
case components.screenPicker:
loadStyle(components.screenPicker);
document.title = i18n.t('Screen Picker - Symphony')();
component = ScreenPicker;
break;
case components.screenSharingIndicator:
loadStyle(components.screenSharingIndicator);
document.title = i18n.t('Screen Sharing Indicator - Symphony')();
component = ScreenSharingIndicator;
break;
case components.screenSharingFrame:
loadStyle(components.screenSharingFrame);
component = ScreenSharingFrame;
break;
case components.snippingTool:
loadStyle(components.snippingTool);
document.title = i18n.t('Symphony')();
component = SnippingTool;
break;
case components.basicAuth:
loadStyle(components.basicAuth);
document.title = i18n.t('Basic Authentication - Symphony')();
component = BasicAuth;
break;
case components.notification:
loadStyle(components.notification);
document.title = i18n.t('Notification - Symphony')();
component = NotificationComp;
break;
case components.notificationSettings:
document.title = i18n.t(
'Notification Settings - Symphony',
'NotificationSettings',
)();
loadStyle(components.notificationSettings);
component = NotificationSettings;
break;
case components.welcome:
document.title = i18n.t('WelcomeText', 'Welcome')();
loadStyle(components.welcome);
component = Welcome;
break;
}
const element = React.createElement(component);
ReactDOM.render(element, document.getElementById('Root'));
};
ipcRenderer.on('page-load', (_event, data) => {
const { locale, resource } = data;
i18n.setResource(locale, resource);
// Renders component as soon as the page is ready
load();
const { locale, resource } = data;
i18n.setResource(locale, resource);
// Renders component as soon as the page is ready
load();
});

View File

@ -14,7 +14,7 @@ import WindowsTitleBar from './components/windows-title-bar';
import { SSFApi } from './ssf-api';
interface ISSFWindow extends Window {
ssf?: SSFApi;
ssf?: SSFApi;
}
const ssfWindow: ISSFWindow = window;
@ -27,65 +27,65 @@ const banner = new MessageBanner();
* creates API exposed from electron.
*/
const createAPI = () => {
// iframes (and any other non-top level frames) get no api access
// http://stackoverflow.com/questions/326069/how-to-identify-if-a-webpage-is-being-loaded-inside-an-iframe-or-directly-into-t/326076
if (window.self !== window.top) {
return;
}
// iframes (and any other non-top level frames) get no api access
// http://stackoverflow.com/questions/326069/how-to-identify-if-a-webpage-is-being-loaded-inside-an-iframe-or-directly-into-t/326076
if (window.self !== window.top) {
return;
}
// note: window.open from main window (if in the same domain) will get
// api access. window.open in another domain will be opened in the default
// browser (see: handler for event 'new-window' in windowMgr.js)
// note: window.open from main window (if in the same domain) will get
// api access. window.open in another domain will be opened in the default
// browser (see: handler for event 'new-window' in windowMgr.js)
//
// API exposed to renderer process.
//
// @ts-ignore
ssfWindow.ssf = new SSFApi();
Object.freeze(ssfWindow.ssf);
//
// API exposed to renderer process.
//
// @ts-ignore
ssfWindow.ssf = new SSFApi();
Object.freeze(ssfWindow.ssf);
};
createAPI();
if (ssfWindow.ssf) {
// New context bridge api that exposes all the methods on to window object
contextBridge.exposeInMainWorld('manaSSF', {
setIsMana: ssfWindow.ssf.setIsMana,
CryptoLib: ssfWindow.ssf.CryptoLib,
Search: ssfWindow.ssf.Search,
Notification: ssfWindow.ssf.Notification,
getMediaSource: ssfWindow.ssf.getMediaSource,
activate: ssfWindow.ssf.activate,
bringToFront: ssfWindow.ssf.bringToFront,
getVersionInfo: ssfWindow.ssf.getVersionInfo,
registerActivityDetection: ssfWindow.ssf.registerActivityDetection,
registerDownloadHandler: ssfWindow.ssf.registerDownloadHandler,
openDownloadedItem: ssfWindow.ssf.openDownloadedItem,
showDownloadedItem: ssfWindow.ssf.showDownloadedItem,
clearDownloadedItems: ssfWindow.ssf.clearDownloadedItems,
registerBoundsChange: ssfWindow.ssf.registerBoundsChange,
registerLogger: ssfWindow.ssf.registerLogger,
registerProtocolHandler: ssfWindow.ssf.registerProtocolHandler,
registerLogRetriever: ssfWindow.ssf.registerLogRetriever,
sendLogs: ssfWindow.ssf.sendLogs,
registerAnalyticsEvent: ssfWindow.ssf.registerAnalyticsEvent,
ScreenSnippet: ssfWindow.ssf.ScreenSnippet,
openScreenSnippet: ssfWindow.ssf.openScreenSnippet,
closeScreenSnippet: ssfWindow.ssf.closeScreenSnippet,
setBadgeCount: ssfWindow.ssf.setBadgeCount,
setLocale: ssfWindow.ssf.setLocale,
setIsInMeeting: ssfWindow.ssf.setIsInMeeting,
showNotificationSettings: ssfWindow.ssf.showNotificationSettings,
showScreenSharingIndicator: ssfWindow.ssf.showScreenSharingIndicator,
openScreenSharingIndicator: ssfWindow.ssf.openScreenSharingIndicator,
closeScreenSharingIndicator: ssfWindow.ssf.closeScreenSharingIndicator,
registerRestartFloater: ssfWindow.ssf.registerRestartFloater,
setCloudConfig: ssfWindow.ssf.setCloudConfig,
checkMediaPermission: ssfWindow.ssf.checkMediaPermission,
showNotification: ssfWindow.ssf.showNotification,
closeNotification: ssfWindow.ssf.closeNotification,
restartApp: ssfWindow.ssf.restartApp,
});
// New context bridge api that exposes all the methods on to window object
contextBridge.exposeInMainWorld('manaSSF', {
setIsMana: ssfWindow.ssf.setIsMana,
CryptoLib: ssfWindow.ssf.CryptoLib,
Search: ssfWindow.ssf.Search,
Notification: ssfWindow.ssf.Notification,
getMediaSource: ssfWindow.ssf.getMediaSource,
activate: ssfWindow.ssf.activate,
bringToFront: ssfWindow.ssf.bringToFront,
getVersionInfo: ssfWindow.ssf.getVersionInfo,
registerActivityDetection: ssfWindow.ssf.registerActivityDetection,
registerDownloadHandler: ssfWindow.ssf.registerDownloadHandler,
openDownloadedItem: ssfWindow.ssf.openDownloadedItem,
showDownloadedItem: ssfWindow.ssf.showDownloadedItem,
clearDownloadedItems: ssfWindow.ssf.clearDownloadedItems,
registerBoundsChange: ssfWindow.ssf.registerBoundsChange,
registerLogger: ssfWindow.ssf.registerLogger,
registerProtocolHandler: ssfWindow.ssf.registerProtocolHandler,
registerLogRetriever: ssfWindow.ssf.registerLogRetriever,
sendLogs: ssfWindow.ssf.sendLogs,
registerAnalyticsEvent: ssfWindow.ssf.registerAnalyticsEvent,
ScreenSnippet: ssfWindow.ssf.ScreenSnippet,
openScreenSnippet: ssfWindow.ssf.openScreenSnippet,
closeScreenSnippet: ssfWindow.ssf.closeScreenSnippet,
setBadgeCount: ssfWindow.ssf.setBadgeCount,
setLocale: ssfWindow.ssf.setLocale,
setIsInMeeting: ssfWindow.ssf.setIsInMeeting,
showNotificationSettings: ssfWindow.ssf.showNotificationSettings,
showScreenSharingIndicator: ssfWindow.ssf.showScreenSharingIndicator,
openScreenSharingIndicator: ssfWindow.ssf.openScreenSharingIndicator,
closeScreenSharingIndicator: ssfWindow.ssf.closeScreenSharingIndicator,
registerRestartFloater: ssfWindow.ssf.registerRestartFloater,
setCloudConfig: ssfWindow.ssf.setCloudConfig,
checkMediaPermission: ssfWindow.ssf.checkMediaPermission,
showNotification: ssfWindow.ssf.showNotification,
closeNotification: ssfWindow.ssf.closeNotification,
restartApp: ssfWindow.ssf.restartApp,
});
}
/**
@ -97,9 +97,9 @@ if (ssfWindow.ssf) {
* @param max {number} - millisecond
*/
const getRandomTime = (min, max) => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
/**
@ -107,42 +107,45 @@ const getRandomTime = (min, max) => {
*
* @param time
*/
const monitorMemory = (time) => {
setTimeout(async () => {
const memoryInfo = await process.getProcessMemoryInfo();
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.memoryInfo,
memoryInfo,
});
monitorMemory(getRandomTime(minMemoryFetchInterval, maxMemoryFetchInterval));
}, time);
const monitorMemory = (time) => {
setTimeout(async () => {
const memoryInfo = await process.getProcessMemoryInfo();
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.memoryInfo,
memoryInfo,
});
monitorMemory(
getRandomTime(minMemoryFetchInterval, maxMemoryFetchInterval),
);
}, time);
};
// When the window is completely loaded
ipcRenderer.on('page-load', (_event, { locale, resources, enableCustomTitleBar }) => {
ipcRenderer.on(
'page-load',
(_event, { locale, resources, enableCustomTitleBar }) => {
i18n.setResource(locale, resources);
if (enableCustomTitleBar) {
// injects custom title bar
const element = React.createElement(WindowsTitleBar);
const div = document.createElement( 'div' );
document.body.appendChild(div);
ReactDOM.render(element, div);
// injects custom title bar
const element = React.createElement(WindowsTitleBar);
const div = document.createElement('div');
document.body.appendChild(div);
ReactDOM.render(element, div);
document.body.classList.add('sda-title-bar');
document.body.classList.add('sda-title-bar');
}
webFrame.setSpellCheckProvider('en-US', {
spellCheck(words, callback) {
const misspelled = words.filter((word) => {
return ipcRenderer.sendSync(apiName.symphonyApi, {
cmd: apiCmds.isMisspelled,
word,
});
});
callback(misspelled);
},
spellCheck(words, callback) {
const misspelled = words.filter((word) => {
return ipcRenderer.sendSync(apiName.symphonyApi, {
cmd: apiCmds.isMisspelled,
word,
});
});
callback(misspelled);
},
});
// injects snack bar
@ -155,50 +158,51 @@ ipcRenderer.on('page-load', (_event, { locale, resources, enableCustomTitleBar }
// initialize red banner
banner.initBanner();
banner.showBanner(false, 'error');
});
},
);
ipcRenderer.on('page-load-welcome', (_event, data) => {
const { locale, resource } = data;
i18n.setResource(locale, resource);
const { locale, resource } = data;
i18n.setResource(locale, resource);
document.title = 'Welcome';
const styles = document.createElement('link');
styles.rel = 'stylesheet';
styles.type = 'text/css';
styles.href = `./styles/welcome.css`;
document.getElementsByTagName('head')[0].appendChild(styles);
const element = React.createElement(Welcome);
ReactDOM.render(element, document.getElementById('Root'));
document.title = 'Welcome';
const styles = document.createElement('link');
styles.rel = 'stylesheet';
styles.type = 'text/css';
styles.href = `./styles/welcome.css`;
document.getElementsByTagName('head')[0].appendChild(styles);
const element = React.createElement(Welcome);
ReactDOM.render(element, document.getElementById('Root'));
});
// When the window fails to load
ipcRenderer.on('page-load-failed', (_event, { locale, resources }) => {
i18n.setResource(locale, resources);
i18n.setResource(locale, resources);
});
// Injects network error content into the DOM
ipcRenderer.on('network-error', (_event, { error }) => {
const networkErrorContainer = document.createElement( 'div' );
networkErrorContainer.id = 'main-frame';
networkErrorContainer.classList.add('content-wrapper');
document.body.append(networkErrorContainer);
const networkError = React.createElement(NetworkError, { error });
ReactDOM.render(networkError, networkErrorContainer);
const networkErrorContainer = document.createElement('div');
networkErrorContainer.id = 'main-frame';
networkErrorContainer.classList.add('content-wrapper');
document.body.append(networkErrorContainer);
const networkError = React.createElement(NetworkError, { error });
ReactDOM.render(networkError, networkErrorContainer);
});
ipcRenderer.on('show-banner', (_event, { show, bannerType, url }) => {
if (!!document.getElementsByClassName('sda-banner-show').length) {
return;
}
banner.showBanner(show, bannerType, url);
if (!!document.getElementsByClassName('sda-banner-show').length) {
return;
}
banner.showBanner(show, bannerType, url);
});
ipcRenderer.on('initialize-memory-refresh', () => {
monitorMemory(getRandomTime(minMemoryFetchInterval, maxMemoryFetchInterval));
monitorMemory(getRandomTime(minMemoryFetchInterval, maxMemoryFetchInterval));
});
ipcRenderer.on('exit-html-fullscreen', async () => {
if (document && typeof document.exitFullscreen === 'function') {
await document.exitFullscreen();
}
if (document && typeof document.exitFullscreen === 'function') {
await document.exitFullscreen();
}
});

View File

@ -5,33 +5,36 @@ import { apiCmds, apiName, IScreenSnippet } from '../common/api-interface';
* @deprecated user openScreenSnippet instead
*/
export class ScreenSnippetBcHandler {
/**
* capture method to support backward compatibility
*
* @deprecated user openScreenSnippet instead
*/
public capture(): Promise<IScreenSnippet> {
return new Promise((resolve, reject) => {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.openScreenSnippet,
});
ipcRenderer.on('screen-snippet-data', (_event: Event, arg: IScreenSnippet) => {
if (arg.type === 'ERROR') {
reject(arg);
return;
}
resolve(arg);
});
});
}
/**
* cancel capture method to support backward compatibility
*
* @deprecated user closeScreenSnippet instead
*/
public cancel() {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeScreenSnippet,
});
}
/**
* capture method to support backward compatibility
*
* @deprecated user openScreenSnippet instead
*/
public capture(): Promise<IScreenSnippet> {
return new Promise((resolve, reject) => {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.openScreenSnippet,
});
ipcRenderer.on(
'screen-snippet-data',
(_event: Event, arg: IScreenSnippet) => {
if (arg.type === 'ERROR') {
reject(arg);
return;
}
resolve(arg);
},
);
});
}
/**
* cancel capture method to support backward compatibility
*
* @deprecated user closeScreenSnippet instead
*/
public cancel() {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeScreenSnippet,
});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -23,14 +23,15 @@
],
"no-empty": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-implicit-dependencies": [true, "dev", "optional"],
"no-object-literal-type-assertion": false,
"no-var-requires": true,
"only-arrow-functions": true,
"object-literal-sort-keys": false,
"no-console": [true, "log", "error"],
"one-line": [true, "check-else", "check-whitespace", "check-open-brace"],
"quotemark": [true, "single", "avoid-escape"],
"semicolon": [true, "always"],
"semicolon": [true, "always", "ignore-bound-class-methods"],
"typedef-whitespace": [
true,
{