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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir .",
"reinstall:nautilus-extension": "NODE_ENV=development ts-node src/apps/nautilus-extension/reload.ts",
"lint": "cross-env NODE_ENV=development eslint . --ext .ts,.tsx --max-warnings=220",
"lint": "cross-env NODE_ENV=development eslint . --ext .ts,.tsx --max-warnings=210",
"lint:fix": "npm run lint --fix",
"format": "prettier src --check",
"format:fix": "prettier src --write",
Expand Down
3 changes: 3 additions & 0 deletions src/apps/main/interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,15 @@ export interface IElectronAPI {
removeInfectedFiles: (infectedFiles: string[]) => Promise<void>;
cancelScan: () => Promise<void>;
};
chooseSyncRootWithDialog(): Promise<string | null>;
getBackupErrorByFolder(folderId: number): Promise<BackupErrorRecord | undefined>;
getLastBackupHadIssues(): Promise<boolean>;
onBackupFatalErrorsChanged(fn: (backupErrors: Array<BackupErrorRecord>) => void): () => void;
getBackupFatalErrors(): Promise<Array<BackupErrorRecord>>;
onBackupProgress(func: (value: number) => void): () => void;
startRemoteSync(): Promise<void>;
getRemoteSyncStatus(): Promise<import('./remote-sync/helpers').RemoteSyncStatus>;
onRemoteSyncStatusChange(callback: (status: import('./remote-sync/helpers').RemoteSyncStatus) => void): () => void;

pathChanged(path: string): void;
isUserLoggedIn(): Promise<boolean>;
Expand Down
4 changes: 0 additions & 4 deletions src/apps/main/preload.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ declare interface Window {

getUser(): Promise<ReturnType<typeof import('./auth/service').getUser>>;

startSyncProcess(): void;

stopSyncProcess(): void;

openProcessIssuesWindow(): void;

openLogs(): void;
Expand Down
15 changes: 0 additions & 15 deletions src/apps/main/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,6 @@ contextBridge.exposeInMainWorld('electron', {
getUser() {
return ipcRenderer.invoke('get-user');
},
startSyncProcess() {
return ipcRenderer.send('start-sync-process');
},
stopSyncProcess() {
return ipcRenderer.send('stop-sync-process');
},
getSyncStatus() {
return ipcRenderer.invoke('get-sync-status');
},
onSyncStatusChanged(func) {
const eventName = 'sync-status-changed';
const callback = (_, v) => func(v);
ipcRenderer.on(eventName, callback);
return () => ipcRenderer.removeListener(eventName, callback);
},
onSyncStopped(func) {
const eventName = 'sync-stopped';
const callback = (_, v) => func(v);
Expand Down
25 changes: 14 additions & 11 deletions src/apps/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import './localize/i18n.service';
import { Suspense, useEffect, useRef } from 'react';
import { HashRouter as Router, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { TranslationProvider } from './context/LocalContext';
import { SyncProvider } from './context/SyncContext';
import Login from './pages/Login';
import Onboarding from './pages/Onboarding';
import IssuesPage from './pages/Issues/IssuesPage';
Expand Down Expand Up @@ -76,17 +77,19 @@ export default function App() {
<Suspense fallback={<Loader />}>
<TranslationProvider>
<UsageProvider>
<LocationWrapper>
<LoggedInWrapper>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/process-issues" element={<IssuesPage />} />
<Route path="/onboarding" element={<Onboarding />} />
<Route path="/settings" element={<Settings />} />
<Route path="/" element={<Widget />} />
</Routes>
</LoggedInWrapper>
</LocationWrapper>
<SyncProvider>
<LocationWrapper>
<LoggedInWrapper>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/process-issues" element={<IssuesPage />} />
<Route path="/onboarding" element={<Onboarding />} />
<Route path="/settings" element={<Settings />} />
<Route path="/" element={<Widget />} />
</Routes>
</LoggedInWrapper>
</LocationWrapper>
</SyncProvider>
</UsageProvider>
</TranslationProvider>
</Suspense>
Expand Down
113 changes: 113 additions & 0 deletions src/apps/renderer/context/SyncContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { SyncProvider, useSyncContext } from './SyncContext';
import { RemoteSyncStatus } from '../../main/remote-sync/helpers';
import { partialSpyOn } from '../../../../tests/vitest/utils.helper';

describe('SyncContext', () => {
void window.electron.getRemoteSyncStatus;
void window.electron.onRemoteSyncStatusChange;

const getRemoteSyncStatusMock = partialSpyOn(window.electron, 'getRemoteSyncStatus', false);
const onRemoteSyncStatusChangeMock = partialSpyOn(window.electron, 'onRemoteSyncStatusChange', false);
const unsubscribeMock = vi.fn();

beforeEach(() => {
onRemoteSyncStatusChangeMock.mockReturnValue(unsubscribeMock);
});

function renderSyncHook() {
return renderHook(() => useSyncContext(), {
wrapper: ({ children }) => <SyncProvider>{children}</SyncProvider>,
});
}

it('should have STANDBY as default status', () => {
getRemoteSyncStatusMock.mockReturnValue(new Promise(() => {}));

const { result } = renderSyncHook();

expect(result.current.syncStatus).toBe('STANDBY');
});

it('should fetch remote sync status on mount', () => {
getRemoteSyncStatusMock.mockReturnValue(new Promise(() => {}));

renderSyncHook();

expect(getRemoteSyncStatusMock).toHaveBeenCalledOnce();
});

it('should subscribe to remote sync status changes on mount', () => {
getRemoteSyncStatusMock.mockReturnValue(new Promise(() => {}));

renderSyncHook();

expect(onRemoteSyncStatusChangeMock).toBeCalledWith(expect.any(Function));
});

it('should unsubscribe from remote sync status changes on unmount', () => {
getRemoteSyncStatusMock.mockReturnValue(new Promise(() => {}));

const { unmount } = renderSyncHook();

unmount();

expect(unsubscribeMock).toHaveBeenCalledOnce();
});

it.each([
['SYNCING', 'RUNNING'],
['SYNC_FAILED', 'FAILED'],
] as [RemoteSyncStatus, string][])(
'should map remote status %s to %s on initial fetch',
async (remoteStatus, expectedStatus) => {
getRemoteSyncStatusMock.mockResolvedValue(remoteStatus);

const { result, waitForNextUpdate } = renderSyncHook();

await waitForNextUpdate();

expect(result.current.syncStatus).toBe(expectedStatus);
},
);

it.each([
['IDLE', 'STANDBY'],
['SYNCED', 'STANDBY'],
] as [RemoteSyncStatus, string][])(
'should keep STANDBY when remote status %s resolves (same as default)',
async (remoteStatus, expectedStatus) => {
getRemoteSyncStatusMock.mockResolvedValue(remoteStatus);

const { result } = renderSyncHook();

await vi.waitFor(() => {
expect(getRemoteSyncStatusMock).toHaveBeenCalledOnce();
});

expect(result.current.syncStatus).toBe(expectedStatus);
},
);

it.each([
['SYNCING', 'RUNNING'],
['IDLE', 'STANDBY'],
['SYNCED', 'STANDBY'],
['SYNC_FAILED', 'FAILED'],
] as [RemoteSyncStatus, string][])(
'should map remote status %s to %s on status change event',
async (remoteStatus, expectedStatus) => {
getRemoteSyncStatusMock.mockReturnValue(new Promise(() => {}));

const { result } = renderSyncHook();

const changeCallback = onRemoteSyncStatusChangeMock.mock.calls[0][0];

act(() => {
changeCallback(remoteStatus);
});

expect(result.current.syncStatus).toBe(expectedStatus);
},
);
});
38 changes: 38 additions & 0 deletions src/apps/renderer/context/SyncContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { SyncStatus } from '../../../context/desktop/sync/domain/SyncStatus';
import { RemoteSyncStatus } from '../../main/remote-sync/helpers';

const statusesMap: Record<RemoteSyncStatus, SyncStatus> = {
SYNCING: 'RUNNING',
IDLE: 'STANDBY',
SYNCED: 'STANDBY',
SYNC_FAILED: 'FAILED',
};

interface SyncContextValue {
syncStatus: SyncStatus;
}

const SyncContext = createContext<SyncContextValue>({ syncStatus: 'STANDBY' });

export function SyncProvider({ children }: { readonly children: ReactNode }) {
const [syncStatus, setSyncStatus] = useState<SyncStatus>('STANDBY');

const setSyncStatusFromRemote = useCallback((remote: RemoteSyncStatus): void => {
setSyncStatus(statusesMap[remote]);
}, []);

useEffect(() => {
window.electron.getRemoteSyncStatus().then(setSyncStatusFromRemote);

Check warning on line 26 in src/apps/renderer/context/SyncContext.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-desktop-linux&issues=AZz3-hSmuS6a3aTy2dgH&open=AZz3-hSmuS6a3aTy2dgH&pullRequest=274
const removeListener = window.electron.onRemoteSyncStatusChange(setSyncStatusFromRemote);

Check warning on line 27 in src/apps/renderer/context/SyncContext.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=internxt_drive-desktop-linux&issues=AZz3-hSmuS6a3aTy2dgI&open=AZz3-hSmuS6a3aTy2dgI&pullRequest=274
return removeListener;
}, [setSyncStatusFromRemote]);

const value = useMemo(() => ({ syncStatus }), [syncStatus]);

return <SyncContext.Provider value={value}>{children}</SyncContext.Provider>;
}

export function useSyncContext(): SyncContextValue {
return useContext(SyncContext);
}
88 changes: 88 additions & 0 deletions src/apps/renderer/hooks/useOnSyncRunning.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useOnSyncRunning } from './useOnSyncRunning';
import { SyncProvider } from '../context/SyncContext';
import { RemoteSyncStatus } from '../../main/remote-sync/helpers';
import { partialSpyOn } from '../../../../tests/vitest/utils.helper';

describe('useOnSyncRunning', () => {
void window.electron.getRemoteSyncStatus;
void window.electron.onRemoteSyncStatusChange;

const getRemoteSyncStatusMock = partialSpyOn(window.electron, 'getRemoteSyncStatus', false);
const onRemoteSyncStatusChangeMock = partialSpyOn(window.electron, 'onRemoteSyncStatusChange', false);

beforeEach(() => {
onRemoteSyncStatusChangeMock.mockReturnValue(vi.fn());
});

function renderOnSyncRunningHook(fn: () => void) {
return renderHook(() => useOnSyncRunning(fn), {
wrapper: ({ children }) => <SyncProvider>{children}</SyncProvider>,
});
}

it('should call callback when sync status changes to RUNNING', async () => {
getRemoteSyncStatusMock.mockResolvedValue('SYNCING' as RemoteSyncStatus);
const callback = vi.fn();

const { waitForNextUpdate } = renderOnSyncRunningHook(callback);

await waitForNextUpdate();

expect(callback).toHaveBeenCalledOnce();
});

it('should not call callback when sync status is STANDBY', async () => {
getRemoteSyncStatusMock.mockResolvedValue('SYNCED' as RemoteSyncStatus);
const callback = vi.fn();

renderOnSyncRunningHook(callback);

await vi.waitFor(() => {
expect(getRemoteSyncStatusMock).toHaveBeenCalledOnce();
});

expect(callback).not.toBeCalled();
});

it('should not call callback when sync status is FAILED', async () => {
getRemoteSyncStatusMock.mockResolvedValue('SYNC_FAILED' as RemoteSyncStatus);
const callback = vi.fn();

const { waitForNextUpdate } = renderOnSyncRunningHook(callback);

await waitForNextUpdate();

expect(callback).not.toBeCalled();
});

it('should call callback when status changes to RUNNING via event', () => {
getRemoteSyncStatusMock.mockReturnValue(new Promise(() => {}));
const callback = vi.fn();

renderOnSyncRunningHook(callback);

const changeCallback = onRemoteSyncStatusChangeMock.mock.calls[0][0];

act(() => {
changeCallback('SYNCING' as RemoteSyncStatus);
});

expect(callback).toHaveBeenCalledOnce();
});

it('should not call callback when status changes to non-RUNNING via event', () => {
getRemoteSyncStatusMock.mockReturnValue(new Promise(() => {}));
const callback = vi.fn();

renderOnSyncRunningHook(callback);

const changeCallback = onRemoteSyncStatusChangeMock.mock.calls[0][0];

act(() => {
changeCallback('SYNCED' as RemoteSyncStatus);
});

expect(callback).not.toBeCalled();
});
});
18 changes: 8 additions & 10 deletions src/apps/renderer/hooks/useOnSyncRunning.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { SyncStatus } from '../../../context/desktop/sync/domain/SyncStatus';
import useSyncStatus from './useSyncStatus';
import { useEffect } from 'react';
import { useSyncContext } from '../context/SyncContext';

export function useOnSyncRunning(fn: () => void) {
function isRunning(status: SyncStatus) {
return status === 'RUNNING';
}
const { syncStatus } = useSyncContext();

useSyncStatus((status) => {
if (!isRunning(status)) return;

fn();
});
useEffect(() => {
if (syncStatus === 'RUNNING') {
fn();
}
}, [syncStatus]);
}
32 changes: 0 additions & 32 deletions src/apps/renderer/hooks/useSyncStatus.tsx

This file was deleted.

Loading
Loading