Skip to content

Commit f8b6510

Browse files
hasimk-dnaHasim KaramanTwitchBronBron
authored
Customize screenshot directory path (#677)
Co-authored-by: Hasim Karaman <Hasim.Karaman@Siriusxm.com> Co-authored-by: Bronley Plumb <bronley@gmail.com>
1 parent 152addf commit f8b6510

File tree

4 files changed

+260
-34
lines changed

4 files changed

+260
-34
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2860,6 +2860,11 @@
28602860
"deprecationMessage": "Use `fileLogging` option instead",
28612861
"description": "File where the 'BrightScript Extension' output panel (i.e. debug logs for the extension) will be appended. If omitted, no file logging will be done. ${workspaceFolder} is supported and will point to the first workspace found.",
28622862
"scope": "resource"
2863+
},
2864+
"brightscript.screenshotDir": {
2865+
"type": "string",
2866+
"description": "Directory where screenshots will be saved. If omitted, screenshots will be saved to the OS's temporary directory. Supports absolute paths (e.g., \"/Users/username/Desktop\" on macOS, \"C:\\\\Users\\\\username\\\\Desktop\" on Windows) and relative paths (e.g., \"debug/screenshots\" or \"debug\\\\screenshots\" on Windows) which are resolved relative to the workspace folder. ${workspaceFolder} is also supported and will point to the first workspace found.",
2867+
"scope": "resource"
28632868
}
28642869
}
28652870
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { vscode } from '../mockVscode.spec';
2+
import { createSandbox } from 'sinon';
3+
import { CaptureScreenshotCommand } from './CaptureScreenshotCommand';
4+
import { BrightScriptCommands } from '../BrightScriptCommands';
5+
import * as rokuDeploy from 'roku-deploy';
6+
import { expect } from 'chai';
7+
import URI from 'vscode-uri';
8+
import { standardizePath as s } from 'brighterscript';
9+
10+
const cwd = s`${process.cwd()}`;
11+
12+
const sinon = createSandbox();
13+
14+
describe('CaptureScreenshotCommand', () => {
15+
let brightScriptCommands: BrightScriptCommands;
16+
let command: CaptureScreenshotCommand;
17+
let context = vscode.context;
18+
let workspace = vscode.workspace;
19+
20+
beforeEach(() => {
21+
command = new CaptureScreenshotCommand();
22+
brightScriptCommands = new BrightScriptCommands({} as any, {} as any, {} as any, {} as any, {} as any, {} as any);
23+
command.register(context, brightScriptCommands);
24+
});
25+
26+
afterEach(() => {
27+
sinon.restore();
28+
workspace.workspaceFolders = [];
29+
});
30+
31+
it('gets remoteHost and remotePassword when hostParam is not provided', async () => {
32+
sinon.stub(brightScriptCommands, 'getRemoteHost').resolves('1.1.1.1');
33+
sinon.stub(brightScriptCommands, 'getRemotePassword').resolves('password');
34+
35+
const { host, password } = await command['getHostAndPassword']();
36+
37+
expect(host).to.eql('1.1.1.1');
38+
expect(password).to.eql('password');
39+
});
40+
41+
it('gets remotePassword when hostParam matches remoteHost', async () => {
42+
await context.workspaceState.update('remoteHost', '1.1.1.1');
43+
await context.workspaceState.update('remotePassword', 'password');
44+
45+
const { host, password } = await command['getHostAndPassword']('1.1.1.1');
46+
47+
expect(host).to.eql('1.1.1.1');
48+
expect(password).to.eql('password');
49+
});
50+
51+
it('prompts for password when hostParam does not match remoteHost', async () => {
52+
sinon.stub(vscode.window, 'showInputBox').resolves('password');
53+
54+
const { host, password } = await command['getHostAndPassword']('1.1.1.1');
55+
56+
expect(host).to.eql('1.1.1.1');
57+
expect(password).to.eql('password');
58+
});
59+
60+
it('shows error message when captureScreenshot fails', async () => {
61+
const stub = sinon.stub(command as any, 'getHostAndPassword').callsFake(() => Promise.resolve({ host: '1.1.1.1', password: 'password' }));
62+
sinon.stub(rokuDeploy, 'takeScreenshot').rejects(new Error('Screenshot failed'));
63+
const stubError = sinon.stub(vscode.window, 'showErrorMessage');
64+
65+
await command['captureScreenshot']('1.1.1.1');
66+
67+
expect(stub.getCall(0).args[0]).to.eql('1.1.1.1');
68+
expect(stubError.calledOnce).to.be.true;
69+
});
70+
71+
it('uses temp dir when screenshotDir is not defined', async () => {
72+
sinon.stub(command as any, 'getHostAndPassword').callsFake(() => Promise.resolve({ host: '1.1.1.1', password: 'password' }));
73+
const stub = sinon.stub(rokuDeploy, 'takeScreenshot').returns(Promise.resolve('screenshot.png'));
74+
75+
await command['captureScreenshot']();
76+
77+
expect(stub.getCall(0).args[0]).to.eql({ host: '1.1.1.1', password: 'password' });
78+
});
79+
80+
it('uses screenshotDir with single workspace', async () => {
81+
sinon.stub(command as any, 'getHostAndPassword').callsFake(() => Promise.resolve({ host: '1.1.1.1', password: 'password' }));
82+
const stub = sinon.stub(rokuDeploy, 'takeScreenshot').returns(Promise.resolve('screenshot.png'));
83+
workspace._configuration = {
84+
'brightscript.screenshotDir': '${workspaceFolder}/screenshots'
85+
};
86+
workspace.workspaceFolders = [
87+
{
88+
uri: URI.file(s`${cwd}/workspace`),
89+
name: 'test-workspace',
90+
index: 0
91+
}
92+
];
93+
94+
await command['captureScreenshot']();
95+
96+
expect(stub.getCall(0).args[0]).to.eql({ host: '1.1.1.1', password: 'password', outDir: s`${cwd}/workspace/screenshots` });
97+
});
98+
99+
it('uses relative screenshotDir with single workspace', async () => {
100+
sinon.stub(command as any, 'getHostAndPassword').callsFake(() => Promise.resolve({ host: '1.1.1.1', password: 'password' }));
101+
const stub = sinon.stub(rokuDeploy, 'takeScreenshot').returns(Promise.resolve('screenshot.png'));
102+
workspace._configuration = {
103+
'brightscript.screenshotDir': 'screenshots'
104+
};
105+
workspace.workspaceFolders = [
106+
{
107+
uri: URI.file(s`${cwd}/workspace`),
108+
name: 'test-workspace',
109+
index: 0
110+
}
111+
];
112+
113+
await command['captureScreenshot']();
114+
115+
expect(stub.getCall(0).args[0]).to.eql({ host: '1.1.1.1', password: 'password', outDir: s`${cwd}/workspace/screenshots` });
116+
});
117+
118+
it('uses screenshotDir with multiple workspace', async () => {
119+
sinon.stub(command as any, 'getHostAndPassword').callsFake(() => Promise.resolve({ host: '1.1.1.1', password: 'password' }));
120+
const stub = sinon.stub(rokuDeploy, 'takeScreenshot').returns(Promise.resolve('screenshot.png'));
121+
const workspaceFolders = [
122+
{
123+
uri: URI.file(s`${cwd}/workspace1`),
124+
name: 'test-workspace',
125+
index: 0
126+
},
127+
{
128+
uri: URI.file(s`${cwd}/workspace2`),
129+
name: 'test-workspace2',
130+
index: 1
131+
}
132+
];
133+
workspace.workspaceFolders = workspaceFolders;
134+
workspace._configuration = {
135+
'brightscript.screenshotDir': '${workspaceFolder}/screenshots'
136+
};
137+
sinon.stub(vscode.window, 'showWorkspaceFolderPick').resolves(workspaceFolders[1]);
138+
139+
await command['captureScreenshot']();
140+
141+
expect(stub.getCall(0).args[0]).to.eql({ host: '1.1.1.1', password: 'password', outDir: s`${cwd}/workspace2/screenshots` });
142+
});
143+
144+
it('uses relative screenshotDir with multiple workspace', async () => {
145+
sinon.stub(command as any, 'getHostAndPassword').callsFake(() => Promise.resolve({ host: '1.1.1.1', password: 'password' }));
146+
const stub = sinon.stub(rokuDeploy, 'takeScreenshot').returns(Promise.resolve('screenshot.png'));
147+
const workspaceFolders = [
148+
{
149+
uri: URI.file(s`${cwd}/workspace1`),
150+
name: 'test-workspace',
151+
index: 0
152+
},
153+
{
154+
uri: URI.file(s`${cwd}/workspace2`),
155+
name: 'test-workspace2',
156+
index: 1
157+
}
158+
];
159+
workspace.workspaceFolders = workspaceFolders;
160+
workspace._configuration = {
161+
'brightscript.screenshotDir': 'screenshots'
162+
};
163+
sinon.stub(vscode.window, 'showWorkspaceFolderPick').resolves(workspaceFolders[1]);
164+
165+
await command['captureScreenshot']();
166+
167+
expect(stub.getCall(0).args[0]).to.eql({ host: '1.1.1.1', password: 'password', outDir: s`${cwd}/workspace2/screenshots` });
168+
});
169+
});

src/commands/CaptureScreenshotCommand.ts

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,102 @@
11
import * as vscode from 'vscode';
2+
import * as path from 'path';
23
import * as rokuDeploy from 'roku-deploy';
34
import type { BrightScriptCommands } from '../BrightScriptCommands';
5+
import { util } from '../util';
46

57
export const FILE_SCHEME = 'bs-captureScreenshot';
68

79
export class CaptureScreenshotCommand {
10+
private context: vscode.ExtensionContext;
11+
private brightScriptCommands: BrightScriptCommands;
812

9-
public register(context: vscode.ExtensionContext, BrightScriptCommandsInstance: BrightScriptCommands) {
10-
context.subscriptions.push(vscode.commands.registerCommand('extension.brightscript.captureScreenshot', async (hostParam?: string) => {
11-
let host: string;
12-
let password: string;
13+
public register(context: vscode.ExtensionContext, brightScriptCommands: BrightScriptCommands) {
14+
this.context = context;
15+
this.brightScriptCommands = brightScriptCommands;
16+
context.subscriptions.push(vscode.commands.registerCommand('extension.brightscript.captureScreenshot', this.captureScreenshot.bind(this)));
17+
}
18+
19+
private async getHostAndPassword(hostParam?: string): Promise<{ host: string; password: string }> {
20+
let host: string;
21+
let password: string;
1322

14-
//if a hostParam was not provided, then go the normal flow for getting info
15-
if (!hostParam) {
16-
host = await BrightScriptCommandsInstance.getRemoteHost();
17-
password = await BrightScriptCommandsInstance.getRemotePassword();
23+
//if a hostParam was not provided, then go the normal flow for getting info
24+
if (!hostParam) {
25+
host = await this.brightScriptCommands.getRemoteHost();
26+
password = await this.brightScriptCommands.getRemotePassword();
1827

19-
//the host was provided, probably by clicking the "capture screenshot" link in the tree view. Do we have a password stored as well? If not, prompt for one
28+
//the host was provided, probably by clicking the "capture screenshot" link in the tree view. Do we have a password stored as well? If not, prompt for one
29+
} else {
30+
host = hostParam;
31+
let remoteHost = await this.context.workspaceState.get('remoteHost');
32+
if (host === remoteHost) {
33+
password = this.context.workspaceState.get('remotePassword');
2034
} else {
21-
host = hostParam;
22-
let remoteHost = await context.workspaceState.get('remoteHost');
23-
if (host === remoteHost) {
24-
password = context.workspaceState.get('remotePassword');
25-
} else {
26-
password = await vscode.window.showInputBox({
27-
placeHolder: `Please enter the developer password for host '${host}'`,
28-
value: ''
29-
});
35+
password = await vscode.window.showInputBox({
36+
placeHolder: `Please enter the developer password for host '${host}'`,
37+
value: ''
38+
});
39+
}
40+
}
41+
42+
return { host: host, password: password };
43+
}
44+
45+
private async getScreenshotDir() {
46+
let screenshotDir = vscode.workspace.getConfiguration('brightscript').get<string>('screenshotDir');
47+
if (screenshotDir) {
48+
let workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
49+
if (vscode.workspace.workspaceFolders?.length > 1) {
50+
const workspaceFolder = await vscode.window.showWorkspaceFolderPick();
51+
if (workspaceFolder) {
52+
workspacePath = workspaceFolder.uri.fsPath;
3053
}
3154
}
3255

33-
await vscode.window.withProgress({
56+
screenshotDir = screenshotDir.replace('${workspaceFolder}', workspacePath);
57+
screenshotDir = path.resolve(workspacePath ?? process.cwd(), screenshotDir);
58+
}
59+
return screenshotDir;
60+
}
61+
62+
private async captureScreenshot(hostParam?: string) {
63+
const { host, password } = await this.getHostAndPassword(hostParam);
64+
65+
let start = Date.now();
66+
const MIN_PROGRESS_TIME = 850; // Minimum time (in ms) that vscode will ensure the withProgress notification is shown.
67+
let ensureSleepMin = async () => {
68+
let elapsed = Date.now() - start;
69+
if (elapsed < MIN_PROGRESS_TIME) {
70+
await util.sleep(MIN_PROGRESS_TIME - elapsed);
71+
}
72+
};
73+
try {
74+
const screenshotPath = await vscode.window.withProgress({
3475
title: `Capturing screenshot from '${host}'`,
3576
location: vscode.ProgressLocation.Notification
36-
}, async () => {
37-
try {
38-
let screenshotPath = await rokuDeploy.takeScreenshot({
39-
host: host,
40-
password: password
41-
});
42-
if (screenshotPath) {
43-
void vscode.window.showInformationMessage(`Screenshot saved at: ` + screenshotPath);
44-
void vscode.commands.executeCommand('vscode.open', vscode.Uri.file(screenshotPath));
45-
}
46-
} catch (e) {
47-
void vscode.window.showErrorMessage('Could not capture screenshot');
48-
}
77+
}, async (options) => {
78+
const screenshotDir = await this.getScreenshotDir();
79+
80+
let screenshotPath = await rokuDeploy.takeScreenshot({
81+
host: host,
82+
password: password,
83+
...(screenshotDir && { outDir: screenshotDir })
84+
});
85+
86+
return screenshotPath;
4987
});
50-
}));
88+
89+
if (screenshotPath) {
90+
await ensureSleepMin();
91+
await Promise.all([
92+
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(screenshotPath)),
93+
vscode.window.showInformationMessage(`Screenshot saved at: ` + screenshotPath)
94+
]);
95+
}
96+
} catch (e) {
97+
await ensureSleepMin();
98+
void vscode.window.showErrorMessage('Could not capture screenshot');
99+
}
51100
}
52101
}
53102

src/mockVscode.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ afterEach(() => {
1818
delete vscode.workspace._configuration;
1919
vscode.workspace.workspaceFolders = [] as any;
2020
vscode.context.globalState['_data'] = {};
21+
vscode.context.workspaceState['_data'] = {};
2122
});
2223

2324
export let vscode = {
@@ -177,7 +178,9 @@ export let vscode = {
177178
withProgress: (options, action) => {
178179
return action();
179180
},
180-
showInputBox: () => { },
181+
showInputBox: () => {
182+
return Promise.resolve(undefined);
183+
},
181184
createStatusBarItem: () => {
182185
return {
183186
clear: () => { },

0 commit comments

Comments
 (0)