Skip to content

Commit 3d6bce1

Browse files
committed
ADDED: new menu/combo for to toggle split view
ADDED: new menu.accelerators.spec file FIXED: Fs.generic API.join would cause a stack overflow
1 parent 14fae5b commit 3d6bce1

File tree

12 files changed

+320
-45
lines changed

12 files changed

+320
-45
lines changed

e2e/cypress/integration/keyboard.hotkeys.spec.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ describe("keyboard hotkeys", () => {
1010
function createStubs() {
1111
stubs.navHistory = [];
1212

13-
cy.window().then(win => {
13+
cy.log('hello');
14+
return cy.window().then(win => {
1415
const views = win.appState.winStates[0].views;
1516
for (let view of views) {
1617
for (let cache of view.caches) {
@@ -76,19 +77,6 @@ describe("keyboard hotkeys", () => {
7677
expect(stubs.navHistory[0]).to.be.calledWith(-1);
7778
});
7879
});
79-
80-
it("should not change view in single view mode", () => {
81-
cy.get("#view_0").should("have.class", "active");
82-
cy.get("#view_1").should("not.have.class", "active");
83-
84-
cy.get('.data-cy-toggle-splitview')
85-
.click();
86-
87-
cy.triggerHotkey(`{ctrl}{shift}{rightarrow}`).then(() => {
88-
cy.get("#view_1").should("not.have.class", "active");
89-
cy.get("#view_0").should("have.class", "active");
90-
});
91-
});
9280
// it("should open devtools", () => {
9381
// cy.triggerHotkey(`{alt}${MOD_KEY}i`).then(() => {
9482
// expect(ipcRenderer.send).to.be.calledOnce;
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/// <reference types="cypress"/>
2+
3+
import { Classes } from "@blueprintjs/core";
4+
5+
/**
6+
* NOTE: Combos are events that are supposed to be sent from the main process
7+
* after a menu (or its associated shortcut) has been selected.
8+
*
9+
* Since we are running inside the browser (Electron testing support is coming soon,
10+
* see: https://www.cypress.io/blog/2019/09/26/testing-electron-js-applications-using-cypress-alpha-release/),
11+
* we are sending fake combo events to simulate the user selecting a menu item or pressing the associated
12+
* accelerator combo.
13+
*/
14+
describe("combo hotkeys", () => {
15+
const stubs: any = {
16+
navHistory: []
17+
};
18+
19+
let caches:any;
20+
21+
function resetSelection() {
22+
cy.window().then(win => {
23+
win.appState.winStates[0].views.forEach((view: any) => {
24+
caches =
25+
view.caches.forEach((cache: any) => {
26+
cache.reset();
27+
});
28+
});
29+
});
30+
}
31+
32+
function createStubs() {
33+
stubs.navHistory = [];
34+
35+
cy.window().then(win => {
36+
const views = win.appState.winStates[0].views;
37+
for (let view of views) {
38+
for (let cache of view.caches) {
39+
stubs.navHistory.push(cy.spy(cache, "navHistory"));
40+
}
41+
}
42+
43+
// activate splitView mode
44+
const winState = win.appState.winStates[0];
45+
winState.splitView = true;
46+
47+
// stub copy/paste functions
48+
cy.stub(win.appState, 'copySelectedItemsPath').as('copySelectedItemsPath');
49+
// stub reload view
50+
cy.stub(win.appState, 'refreshActiveView').as('refreshActiveView');
51+
// stub first cache.openTerminal
52+
cy.stub(win.appState.winStates[0].views[0].caches[0], 'openTerminal').as('openTerminal');
53+
// stub first view.cycleTab
54+
cy.stub(win.appState.winStates[0].views[0], 'cycleTab').as('cycleTab');
55+
// stub first view.addCache
56+
cy.stub(win.appState.winStates[0].views[0], 'addCache').as('addCache');
57+
// stub first view.closeTab
58+
cy.stub(win.appState.winStates[0].views[0], 'closeTab').as('closeTab');
59+
// stub first win.toggleSplitView
60+
cy.spy(win.appState.winStates[0], 'toggleSplitViewMode').as('toggleSplitViewMode');
61+
});
62+
}
63+
64+
function getCaches() {
65+
cy.window().then(win => {
66+
caches = win.appState.winStates[0].views[0].caches;
67+
});
68+
}
69+
70+
before(() => {
71+
return cy.visit("http://127.0.0.1:8080");
72+
});
73+
74+
beforeEach(() => {
75+
createStubs();
76+
resetSelection();
77+
getCaches();
78+
// load files
79+
cy.CDAndList(0, "/");
80+
cy.get("#view_0 [data-cy-path]")
81+
.invoke("val", "/")
82+
.focus()
83+
.blur();
84+
});
85+
86+
it("should not show toast message on copy path if no file selected", () => {
87+
// no selection: triggering fake combo should not show toast message
88+
cy.triggerFakeCombo("CmdOrCtrl+Shift+C");
89+
90+
cy.get(`.${Classes.TOAST}`)
91+
.should('not.be.visible');
92+
93+
cy.get('@copySelectedItemsPath')
94+
.should('be.calledWith', caches[0], false);
95+
});
96+
97+
it("should copy file path to cb & show toast message if a file is selected", () => {
98+
// select first element
99+
cy.get("#view_0 [data-cy-file]:first")
100+
.click();
101+
102+
cy.triggerFakeCombo("CmdOrCtrl+Shift+C");
103+
104+
cy.get('@copySelectedItemsPath')
105+
.should('be.calledWith', caches[0], false);
106+
107+
cy.get(`.${Classes.TOAST}`)
108+
.should('be.visible')
109+
.find('button')
110+
.click();
111+
});
112+
113+
it("should not show toast message on copy filename if no file selected", () => {
114+
// no selection: triggering fake combo should not show toast message
115+
cy.triggerFakeCombo("CmdOrCtrl+Shift+N");
116+
117+
cy.get(`.${Classes.TOAST}`)
118+
.should('not.be.visible');
119+
120+
cy.get('@copySelectedItemsPath')
121+
.should('be.calledWith', caches[0], true);
122+
});
123+
124+
it("should copy file filename & show toast message if a file is selected", () => {
125+
// select first element
126+
cy.get("#view_0 [data-cy-file]:first")
127+
.click();
128+
129+
cy.triggerFakeCombo("CmdOrCtrl+Shift+N");
130+
131+
cy.get('@copySelectedItemsPath')
132+
.should('be.calledWith', caches[0], true);
133+
134+
cy.get(`.${Classes.TOAST}`)
135+
.should('be.visible')
136+
.find('button')
137+
.click();
138+
});
139+
140+
it("should open shortcuts dialog", () => {
141+
cy.triggerFakeCombo("CmdOrCtrl+S");
142+
143+
cy.get('.shortcutsDialog')
144+
.should('be.visible');
145+
146+
// close dialog
147+
cy.get(`.${Classes.DIALOG_FOOTER} .data-cy-close`)
148+
.click();
149+
150+
// wait for dialog to be closed otherwise
151+
// it could still be visible in next it()
152+
cy.get('.shortcutsDialog')
153+
.should('not.be.visible');
154+
});
155+
156+
it("should open prefs dialog", () => {
157+
cy.triggerFakeCombo("CmdOrCtrl+,");
158+
159+
cy.get('.data-cy-prefs-dialog')
160+
.should('be.visible');
161+
162+
// close dialog
163+
cy.get(`.${Classes.DIALOG_FOOTER} .data-cy-close`)
164+
.click();
165+
166+
// wait for dialog to be closed otherwise
167+
// it could still be visible in next it()
168+
cy.get('.shortcutsDialog')
169+
.should('not.be.visible');
170+
});
171+
172+
it("should reload file view", () => {
173+
cy.triggerFakeCombo("CmdOrCtrl+R");
174+
175+
cy.get('@refreshActiveView')
176+
.should('be.calledOnce');
177+
});
178+
179+
it("should open terminal", () => {
180+
cy.triggerFakeCombo("CmdOrCtrl+K");
181+
182+
cy.get('@openTerminal')
183+
.should('be.calledOnce');
184+
});
185+
186+
it("should activate next tab", () => {
187+
cy.triggerFakeCombo("Ctrl+Tab");
188+
189+
cy.get('@cycleTab')
190+
.should('be.calledOnce')
191+
.should('be.calledWith', 1);
192+
});
193+
194+
it("should activate previous tab", () => {
195+
cy.triggerFakeCombo("Ctrl+Shift+Tab");
196+
197+
cy.get('@cycleTab')
198+
.should('be.calledOnce')
199+
.should('be.calledWith', -1);
200+
});
201+
202+
it("should open a new tab", () => {
203+
cy.triggerFakeCombo("CmdOrCtrl+T");
204+
205+
cy.get('@addCache')
206+
.should('be.calledOnce');
207+
});
208+
209+
it("should close tab", () => {
210+
cy.triggerFakeCombo("CmdOrCtrl+W");
211+
212+
cy.get('@closeTab')
213+
.should('be.calledOnce');
214+
});
215+
216+
it("should toggle split view", () => {
217+
// initial state: split view active
218+
cy.get("#view_1")
219+
.should("not.have.class", "active")
220+
.and('be.visible');
221+
222+
cy.get("#view_0")
223+
.should("have.class", "active")
224+
.and('be.visible');
225+
226+
// de-activate split view
227+
cy.triggerFakeCombo("CmdOrCtrl+Shift+Alt+V");
228+
229+
// check status: we should have only one call
230+
cy.get('@toggleSplitViewMode')
231+
.should('be.calledOnce');
232+
233+
cy.get("#view_0")
234+
.should('be.visible')
235+
.and('have.class', 'active');
236+
237+
cy.get("#view_1")
238+
.should('not.be.visible');
239+
240+
// re-activate split view
241+
cy.triggerFakeCombo("CmdOrCtrl+Shift+Alt+V");
242+
243+
// check status: should have two calls now
244+
cy.get('@toggleSplitViewMode')
245+
.should('be.calledTwice');
246+
247+
cy.get("#view_0")
248+
.should('be.visible')
249+
.and('not.have.class', 'active');
250+
251+
cy.get("#view_1")
252+
.should('be.visible')
253+
.and('have.class', 'active');
254+
});
255+
});

e2e/cypress/support/commands.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,32 @@ declare global {
22
namespace Cypress {
33
interface Chainable {
44
/**
5-
* Yields "foo"
5+
* Yields "json object"
66
*
7-
* @returns {typeof foo}
7+
* @returns {typeof object}
88
* @memberof Chainable
99
* @example
10-
* cy.foo().then(f = ...) // f is "foo"
10+
* cy.CDAndList('/').then(json => ...)
1111
*/
1212
CDAndList: typeof CDList;
13+
/**
14+
* Yields document.body
15+
*
16+
* @returns {typeof Body}
17+
* @memberof Chainable
18+
* @example
19+
* cy.triggerHotkey('{meta}f').then(body => ...)
20+
*/
1321
triggerHotkey: typeof triggerHotkey;
22+
/**
23+
* Yields document
24+
*
25+
* @returns {typeof Document}
26+
* @memberof Chainable
27+
* @example
28+
* cy.triggerFakeCombo('CmdOrCtrl+Shift+C').then(doc => ...)
29+
*/
30+
triggerFakeCombo: typeof triggerFakeCombo
1431
}
1532
}
1633
}
@@ -29,9 +46,16 @@ export function CDList(viewId = 0, path: string, fixture = "files.json") {
2946
});
3047
}
3148

32-
export function triggerHotkey(hotkey: string) {
33-
return cy.get("body").type(hotkey);
49+
export function triggerHotkey(hotkey: string, options = {}) {
50+
return cy.get("body").type(hotkey, options);
51+
}
52+
53+
export function triggerFakeCombo(combo: string, data = { title: "hey!"}) {
54+
cy.log('triggering', { combo, data });
55+
return cy.document()
56+
.trigger('menu_accelerator', { combo, data } );
3457
}
3558

3659
Cypress.Commands.add("CDAndList", CDList);
3760
Cypress.Commands.add("triggerHotkey", triggerHotkey);
61+
Cypress.Commands.add("triggerFakeCombo", triggerFakeCombo);

e2e/webpack.config.e2e.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ const baseConfig = {
1414
},
1515
externals: {
1616
os: `{release: function() { return "${release}"}, tmpdir: function() { return "/tmpdir" }, homedir: function() { return "/homedir" }}`,
17-
process: `{process: "foo", platform: "${platform}"}`,
17+
process: `{process: "React-Explorer", platform: "${platform}"}`,
1818
electron:
19-
'{ipcRenderer: {send: function() {}, on: function() {}}, remote: { getCurrentWindow: () => {}, Menu: { buildFromTemplate: function() { return { popup: function() {}, closePopup: function() { } };}},app: { getLocale: function() { return "en"; }, getPath: function(str) { return "cy_" + str; } } } }',
19+
'{ipcRenderer: {send: function() {}, on: function(event, method) { document.addEventListener(event, function(e) { method(e, {data: e.data, combo: e.combo}); })}}, remote: { getCurrentWindow: () => {}, Menu: { buildFromTemplate: function() { return { popup: function() {}, closePopup: function() { } };}},app: { getLocale: function() { return "en"; }, getPath: function(str) { return "cy_" + str; } } } }',
2020
child_process: "{exec: function(str, cb) { cb(); }}",
2121
fs: "{}",
2222
path: "{}",

src/components/Nav.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ class NavComponent extends React.Component<WithNamespaces> {
5050
};
5151

5252
onToggleSplitView = () => {
53-
const winState = this.appState.winStates[0];
54-
winState.toggleSplitViewMode();
53+
if (this.appState.isExplorer) {
54+
const winState = this.appState.winStates[0];
55+
winState.toggleSplitViewMode();
56+
}
5557
};
5658

5759
onOpenPrefs = () => {

src/components/WithMenuAccelerators.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export function WithMenuAccelerators<
8787
e: MenuAcceleratorEvent,
8888
data: { combo: string; data: any }
8989
) => {
90+
console.log('******* onAccelerator !!', e, data);
9091
// check if combo is valid
9192
const callback = this.getCallback(data.combo);
9293
if (typeof callback === "function") {

src/components/dialogs/ShortcutsDialog.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ class ShortcutsDialogClass extends React.Component<IShortcutsProps>{
3030
{ combo: "mod + s", label: t('SHORTCUT.MAIN.KEYBOARD_SHORTCUTS') },
3131
{ combo: "mod + ,", label: t('SHORTCUT.MAIN.PREFERENCES') },
3232
{ combo: "alt + mod + i", label: t('SHORTCUT.OPEN_DEVTOOLS') },
33-
{ combo: "mod + q", label: t('SHORTCUT.MAIN.QUIT') }
33+
{ combo: "mod + q", label: t('SHORTCUT.MAIN.QUIT') },
34+
{ combo: "mod + alt + shift + v", label: t('NAV.SPLITVIEW') }
3435
],
3536
// group: t('SHORTCUT.GROUP.ACTIVE_VIEW')
3637
[
@@ -111,7 +112,7 @@ class ShortcutsDialogClass extends React.Component<IShortcutsProps>{
111112
</div>
112113
<div className={Classes.DIALOG_FOOTER}>
113114
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
114-
<Button onClick={this.onClose}>
115+
<Button onClick={this.onClose} className="data-cy-close">
115116
{t('COMMON.CLOSE')}
116117
</Button>
117118
</div>

0 commit comments

Comments
 (0)