Skip to content

Commit 61ec0cd

Browse files
committed
Improved the GitGraphView's message handling and logging (when sending messages from the back-end to the front-end).
1 parent 9b5a9ae commit 61ec0cd

4 files changed

Lines changed: 246 additions & 14 deletions

File tree

src/gitGraphView.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,8 @@ export class GitGraphView extends Disposable {
247247
case 'compareCommits':
248248
this.sendMessage({
249249
command: 'compareCommits',
250-
commitHash: msg.commitHash, compareWithHash: msg.compareWithHash,
250+
commitHash: msg.commitHash,
251+
compareWithHash: msg.compareWithHash,
251252
...await this.dataSource.getCommitComparison(msg.repo, msg.fromHash, msg.toHash),
252253
codeReview: msg.toHash !== UNCOMMITTED ? this.extensionState.getCodeReview(msg.repo, msg.fromHash + '-' + msg.toHash) : null,
253254
refresh: msg.refresh
@@ -371,6 +372,15 @@ export class GitGraphView extends Disposable {
371372
errors: errorInfos
372373
});
373374
break;
375+
case 'endCodeReview':
376+
this.extensionState.endCodeReview(msg.repo, msg.id);
377+
break;
378+
case 'exportRepoConfig':
379+
this.sendMessage({
380+
command: 'exportRepoConfig',
381+
error: await this.repoManager.exportRepoConfig(msg.repo)
382+
});
383+
break;
374384
case 'fetch':
375385
this.sendMessage({
376386
command: 'fetch',
@@ -386,15 +396,6 @@ export class GitGraphView extends Disposable {
386396
error: await this.dataSource.fetchIntoLocalBranch(msg.repo, msg.remote, msg.remoteBranch, msg.localBranch, msg.force)
387397
});
388398
break;
389-
case 'endCodeReview':
390-
this.extensionState.endCodeReview(msg.repo, msg.id);
391-
break;
392-
case 'exportRepoConfig':
393-
this.sendMessage({
394-
command: 'exportRepoConfig',
395-
error: await this.repoManager.exportRepoConfig(msg.repo)
396-
});
397-
break;
398399
case 'loadCommits':
399400
this.loadCommitsRefreshId = msg.refreshId;
400401
this.sendMessage({
@@ -439,7 +440,8 @@ export class GitGraphView extends Disposable {
439440
break;
440441
case 'merge':
441442
this.sendMessage({
442-
command: 'merge', actionOn: msg.actionOn,
443+
command: 'merge',
444+
actionOn: msg.actionOn,
443445
error: await this.dataSource.merge(msg.repo, msg.obj, msg.actionOn, msg.createNewCommit, msg.squash, msg.noCommit)
444446
});
445447
break;
@@ -512,7 +514,9 @@ export class GitGraphView extends Disposable {
512514
break;
513515
case 'rebase':
514516
this.sendMessage({
515-
command: 'rebase', actionOn: msg.actionOn, interactive: msg.interactive,
517+
command: 'rebase',
518+
actionOn: msg.actionOn,
519+
interactive: msg.interactive,
516520
error: await this.dataSource.rebase(msg.repo, msg.obj, msg.actionOn, msg.ignoreDate, msg.interactive)
517521
});
518522
break;
@@ -613,7 +617,20 @@ export class GitGraphView extends Disposable {
613617
* @param msg The message to be sent.
614618
*/
615619
private sendMessage(msg: ResponseMessage) {
616-
this.panel.webview.postMessage(msg);
620+
if (this.isDisposed()) {
621+
this.logger.log('The Git Graph View has already been disposed, ignored sending "' + msg.command + '" message.');
622+
} else {
623+
this.panel.webview.postMessage(msg).then(
624+
() => { },
625+
() => {
626+
if (this.isDisposed()) {
627+
this.logger.log('The Git Graph View was disposed while sending "' + msg.command + '" message.');
628+
} else {
629+
this.logger.logError('Unable to send "' + msg.command + '" message to the Git Graph View.');
630+
}
631+
}
632+
);
633+
}
617634
}
618635

619636
/**

src/utils/disposable.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ import * as vscode from 'vscode';
22

33
export class Disposable implements vscode.Disposable {
44
private disposables: vscode.Disposable[] = [];
5+
private disposed: boolean = false;
56

67
/**
78
* Disposes the resources used by the subclass.
89
*/
910
public dispose() {
10-
this.disposables.forEach((disposable) => disposable.dispose());
11+
this.disposed = true;
12+
this.disposables.forEach((disposable) => {
13+
try {
14+
disposable.dispose();
15+
} catch (_) { }
16+
});
1117
this.disposables = [];
1218
}
1319

@@ -24,6 +30,14 @@ export class Disposable implements vscode.Disposable {
2430
protected registerDisposables(...disposables: vscode.Disposable[]) {
2531
this.disposables.push(...disposables);
2632
}
33+
34+
/**
35+
* Is the Disposable disposed.
36+
* @returns `TRUE` => Disposable has been disposed, `FALSE` => Disposable hasn't been disposed.
37+
*/
38+
protected isDisposed() {
39+
return this.disposed;
40+
}
2741
}
2842

2943
export function toDisposable(fn: () => void): vscode.Disposable {

tests/disposable.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import * as vscode from 'vscode';
2+
import { Disposable, toDisposable } from '../src/utils/disposable';
3+
4+
class DisposableTest extends Disposable {
5+
constructor() {
6+
super();
7+
}
8+
9+
public registerDisposable(disposable: vscode.Disposable) {
10+
super.registerDisposable(disposable);
11+
}
12+
13+
public registerDisposables(...disposables: vscode.Disposable[]) {
14+
super.registerDisposables(...disposables);
15+
}
16+
17+
public isDisposed() {
18+
return super.isDisposed();
19+
}
20+
}
21+
22+
describe('Disposable', () => {
23+
it('Should register a disposable', () => {
24+
// Setup
25+
const disposableTest = new DisposableTest();
26+
const disposable = { dispose: jest.fn() };
27+
28+
// Run
29+
disposableTest.registerDisposable(disposable);
30+
31+
// Assert
32+
expect(disposableTest.isDisposed()).toBe(false);
33+
expect(disposableTest['disposables']).toStrictEqual([disposable]);
34+
});
35+
36+
it('Should register multiple disposables', () => {
37+
// Setup
38+
const disposableTest = new DisposableTest();
39+
const disposable1 = { dispose: jest.fn() };
40+
const disposable2 = { dispose: jest.fn() };
41+
42+
// Run
43+
disposableTest.registerDisposables(disposable1, disposable2);
44+
45+
// Assert
46+
expect(disposableTest.isDisposed()).toBe(false);
47+
expect(disposableTest['disposables']).toStrictEqual([disposable1, disposable2]);
48+
});
49+
50+
it('Should dispose all registered disposables', () => {
51+
// Setup
52+
const disposableTest = new DisposableTest();
53+
const disposable1 = { dispose: jest.fn() };
54+
const disposable2 = { dispose: jest.fn() };
55+
disposableTest.registerDisposables(disposable1, disposable2);
56+
57+
// Run
58+
disposableTest.dispose();
59+
60+
// Assert
61+
expect(disposableTest.isDisposed()).toBe(true);
62+
expect(disposableTest['disposables']).toStrictEqual([]);
63+
expect(disposable1.dispose).toHaveBeenCalled();
64+
expect(disposable2.dispose).toHaveBeenCalled();
65+
});
66+
67+
it('Should dispose all registered disposables independently, catching any exceptions', () => {
68+
// Setup
69+
const disposableTest = new DisposableTest();
70+
const disposable1 = { dispose: jest.fn() };
71+
const disposable2 = {
72+
dispose: jest.fn(() => {
73+
throw new Error();
74+
})
75+
};
76+
const disposable3 = { dispose: jest.fn() };
77+
disposableTest.registerDisposables(disposable1, disposable2, disposable3);
78+
79+
// Run
80+
disposableTest.dispose();
81+
82+
// Assert
83+
expect(disposableTest.isDisposed()).toBe(true);
84+
expect(disposableTest['disposables']).toStrictEqual([]);
85+
expect(disposable1.dispose).toHaveBeenCalled();
86+
expect(disposable2.dispose).toHaveBeenCalled();
87+
expect(disposable3.dispose).toHaveBeenCalled();
88+
});
89+
});
90+
91+
describe('toDisposable', () => {
92+
it('Should wrap a function with a disposable', () => {
93+
// Setup
94+
const fn = () => { };
95+
96+
// Run
97+
const result = toDisposable(fn);
98+
99+
// Assert
100+
expect(result).toStrictEqual({ dispose: fn });
101+
});
102+
});

tests/gitGraphView.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe('GitGraphView', () => {
3333
let repoManager: RepoManager;
3434

3535
let spyOnLog: jest.SpyInstance;
36+
let spyOnLogError: jest.SpyInstance;
3637
let spyOnGetRepos: jest.SpyInstance;
3738
let spyOnIsGitExecutableUnknown: jest.SpyInstance;
3839

@@ -49,6 +50,7 @@ describe('GitGraphView', () => {
4950
repoManager = new RepoManager(dataSource, extensionState, onDidChangeConfiguration.subscribe, logger);
5051

5152
spyOnLog = jest.spyOn(logger, 'log');
53+
spyOnLogError = jest.spyOn(logger, 'logError');
5254
spyOnGetRepos = jest.spyOn(repoManager, 'getRepos');
5355
spyOnIsGitExecutableUnknown = jest.spyOn(dataSource, 'isGitExecutableUnknown');
5456

@@ -3467,6 +3469,103 @@ describe('GitGraphView', () => {
34673469
});
34683470
});
34693471

3472+
describe('sendMessage', () => {
3473+
beforeEach(() => {
3474+
GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null);
3475+
spyOnLog.mockReset();
3476+
spyOnLogError.mockReset();
3477+
});
3478+
3479+
it('Should send a message to the Webview', async () => {
3480+
// Setup
3481+
const mockedWebviewPanel = vscode.getMockedWebviewPanel(0);
3482+
jest.spyOn(utils, 'viewScm').mockResolvedValueOnce(null);
3483+
jest.spyOn(mockedWebviewPanel.panel.webview, 'postMessage').mockResolvedValueOnce(true);
3484+
3485+
// Run
3486+
mockedWebviewPanel.mocks.panel.webview.onDidReceiveMessage({
3487+
command: 'viewScm'
3488+
});
3489+
3490+
// Assert
3491+
await waitForExpect(() => {
3492+
expect(mockedWebviewPanel.panel.webview.postMessage).toHaveBeenCalledWith({
3493+
command: 'viewScm',
3494+
error: null
3495+
});
3496+
expect(spyOnLog).not.toHaveBeenCalled();
3497+
expect(spyOnLogError).not.toHaveBeenCalled();
3498+
});
3499+
});
3500+
3501+
it('Should log an error message when Webview.postMessage rejects, and the GitGraphView hasn\'t been disposed', async () => {
3502+
// Setup
3503+
const mockedWebviewPanel = vscode.getMockedWebviewPanel(0);
3504+
jest.spyOn(utils, 'viewScm').mockResolvedValueOnce(null);
3505+
jest.spyOn(mockedWebviewPanel.panel.webview, 'postMessage').mockRejectedValueOnce(null);
3506+
3507+
// Run
3508+
mockedWebviewPanel.mocks.panel.webview.onDidReceiveMessage({
3509+
command: 'viewScm'
3510+
});
3511+
3512+
// Assert
3513+
await waitForExpect(() => {
3514+
expect(mockedWebviewPanel.panel.webview.postMessage).toHaveBeenCalledWith({
3515+
command: 'viewScm',
3516+
error: null
3517+
});
3518+
expect(spyOnLog).not.toHaveBeenCalled();
3519+
expect(spyOnLogError).toHaveBeenCalledWith('Unable to send "viewScm" message to the Git Graph View.');
3520+
});
3521+
});
3522+
3523+
it('Should log an information message when Webview.postMessage rejects, and the GitGraphView has been disposed', async () => {
3524+
// Setup
3525+
const mockedWebviewPanel = vscode.getMockedWebviewPanel(0);
3526+
jest.spyOn(utils, 'viewScm').mockResolvedValueOnce(null);
3527+
jest.spyOn(mockedWebviewPanel.panel.webview, 'postMessage').mockImplementationOnce(() => {
3528+
GitGraphView.currentPanel!.dispose();
3529+
return Promise.reject();
3530+
});
3531+
3532+
// Run
3533+
mockedWebviewPanel.mocks.panel.webview.onDidReceiveMessage({
3534+
command: 'viewScm'
3535+
});
3536+
3537+
// Assert
3538+
await waitForExpect(() => {
3539+
expect(mockedWebviewPanel.panel.webview.postMessage).toHaveBeenCalledWith({
3540+
command: 'viewScm',
3541+
error: null
3542+
});
3543+
expect(spyOnLog).toHaveBeenCalledWith('The Git Graph View was disposed while sending "viewScm" message.');
3544+
expect(spyOnLogError).not.toHaveBeenCalled();
3545+
});
3546+
});
3547+
3548+
it('Shouldn\'t send a message to the Webview if it has been disposed', async () => {
3549+
// Setup
3550+
const mockedWebviewPanel = vscode.getMockedWebviewPanel(0);
3551+
jest.spyOn(utils, 'viewScm').mockResolvedValueOnce(null);
3552+
jest.spyOn(mockedWebviewPanel.panel.webview, 'postMessage').mockResolvedValueOnce(true);
3553+
3554+
// Run
3555+
GitGraphView.currentPanel!.dispose();
3556+
mockedWebviewPanel.mocks.panel.webview.onDidReceiveMessage({
3557+
command: 'viewScm'
3558+
});
3559+
3560+
// Assert
3561+
await waitForExpect(() => {
3562+
expect(mockedWebviewPanel.panel.webview.postMessage).not.toHaveBeenCalled();
3563+
expect(spyOnLog).toHaveBeenCalledWith('The Git Graph View has already been disposed, ignored sending "viewScm" message.');
3564+
expect(spyOnLogError).not.toHaveBeenCalled();
3565+
});
3566+
});
3567+
});
3568+
34703569
describe('getHtmlForWebview', () => {
34713570
beforeEach(() => {
34723571
jest.spyOn(utils, 'getNonce').mockReturnValueOnce('1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d');

0 commit comments

Comments
 (0)