diff --git a/e2e/cypress/integration/keyboard.hotkeys.spec.ts b/e2e/cypress/integration/keyboard.hotkeys.spec.ts index b673c8fe..4b3d412e 100644 --- a/e2e/cypress/integration/keyboard.hotkeys.spec.ts +++ b/e2e/cypress/integration/keyboard.hotkeys.spec.ts @@ -10,7 +10,8 @@ describe("keyboard hotkeys", () => { function createStubs() { stubs.navHistory = []; - cy.window().then(win => { + cy.log('hello'); + return cy.window().then(win => { const views = win.appState.winStates[0].views; for (let view of views) { for (let cache of view.caches) { @@ -76,19 +77,6 @@ describe("keyboard hotkeys", () => { expect(stubs.navHistory[0]).to.be.calledWith(-1); }); }); - - it("should not change view in single view mode", () => { - cy.get("#view_0").should("have.class", "active"); - cy.get("#view_1").should("not.have.class", "active"); - - cy.get('.data-cy-toggle-splitview') - .click(); - - cy.triggerHotkey(`{ctrl}{shift}{rightarrow}`).then(() => { - cy.get("#view_1").should("not.have.class", "active"); - cy.get("#view_0").should("have.class", "active"); - }); - }); // it("should open devtools", () => { // cy.triggerHotkey(`{alt}${MOD_KEY}i`).then(() => { // expect(ipcRenderer.send).to.be.calledOnce; diff --git a/e2e/cypress/integration/menu.accelerators.spec.ts b/e2e/cypress/integration/menu.accelerators.spec.ts new file mode 100644 index 00000000..dbd13434 --- /dev/null +++ b/e2e/cypress/integration/menu.accelerators.spec.ts @@ -0,0 +1,255 @@ +/// + +import { Classes } from "@blueprintjs/core"; + +/** + * NOTE: Combos are events that are supposed to be sent from the main process + * after a menu (or its associated shortcut) has been selected. + * + * Since we are running inside the browser (Electron testing support is coming soon, + * see: https://www.cypress.io/blog/2019/09/26/testing-electron-js-applications-using-cypress-alpha-release/), + * we are sending fake combo events to simulate the user selecting a menu item or pressing the associated + * accelerator combo. + */ +describe("combo hotkeys", () => { + const stubs: any = { + navHistory: [] + }; + + let caches:any; + + function resetSelection() { + cy.window().then(win => { + win.appState.winStates[0].views.forEach((view: any) => { + caches = + view.caches.forEach((cache: any) => { + cache.reset(); + }); + }); + }); + } + + function createStubs() { + stubs.navHistory = []; + + cy.window().then(win => { + const views = win.appState.winStates[0].views; + for (let view of views) { + for (let cache of view.caches) { + stubs.navHistory.push(cy.spy(cache, "navHistory")); + } + } + + // activate splitView mode + const winState = win.appState.winStates[0]; + winState.splitView = true; + + // stub copy/paste functions + cy.stub(win.appState, 'copySelectedItemsPath').as('copySelectedItemsPath'); + // stub reload view + cy.stub(win.appState, 'refreshActiveView').as('refreshActiveView'); + // stub first cache.openTerminal + cy.stub(win.appState.winStates[0].views[0].caches[0], 'openTerminal').as('openTerminal'); + // stub first view.cycleTab + cy.stub(win.appState.winStates[0].views[0], 'cycleTab').as('cycleTab'); + // stub first view.addCache + cy.stub(win.appState.winStates[0].views[0], 'addCache').as('addCache'); + // stub first view.closeTab + cy.stub(win.appState.winStates[0].views[0], 'closeTab').as('closeTab'); + // stub first win.toggleSplitView + cy.spy(win.appState.winStates[0], 'toggleSplitViewMode').as('toggleSplitViewMode'); + }); + } + + function getCaches() { + cy.window().then(win => { + caches = win.appState.winStates[0].views[0].caches; + }); + } + + before(() => { + return cy.visit("http://127.0.0.1:8080"); + }); + + beforeEach(() => { + createStubs(); + resetSelection(); + getCaches(); + // load files + cy.CDAndList(0, "/"); + cy.get("#view_0 [data-cy-path]") + .invoke("val", "/") + .focus() + .blur(); + }); + + it("should not show toast message on copy path if no file selected", () => { + // no selection: triggering fake combo should not show toast message + cy.triggerFakeCombo("CmdOrCtrl+Shift+C"); + + cy.get(`.${Classes.TOAST}`) + .should('not.be.visible'); + + cy.get('@copySelectedItemsPath') + .should('be.calledWith', caches[0], false); + }); + + it("should copy file path to cb & show toast message if a file is selected", () => { + // select first element + cy.get("#view_0 [data-cy-file]:first") + .click(); + + cy.triggerFakeCombo("CmdOrCtrl+Shift+C"); + + cy.get('@copySelectedItemsPath') + .should('be.calledWith', caches[0], false); + + cy.get(`.${Classes.TOAST}`) + .should('be.visible') + .find('button') + .click(); + }); + + it("should not show toast message on copy filename if no file selected", () => { + // no selection: triggering fake combo should not show toast message + cy.triggerFakeCombo("CmdOrCtrl+Shift+N"); + + cy.get(`.${Classes.TOAST}`) + .should('not.be.visible'); + + cy.get('@copySelectedItemsPath') + .should('be.calledWith', caches[0], true); + }); + + it("should copy file filename & show toast message if a file is selected", () => { + // select first element + cy.get("#view_0 [data-cy-file]:first") + .click(); + + cy.triggerFakeCombo("CmdOrCtrl+Shift+N"); + + cy.get('@copySelectedItemsPath') + .should('be.calledWith', caches[0], true); + + cy.get(`.${Classes.TOAST}`) + .should('be.visible') + .find('button') + .click(); + }); + + it("should open shortcuts dialog", () => { + cy.triggerFakeCombo("CmdOrCtrl+S"); + + cy.get('.shortcutsDialog') + .should('be.visible'); + + // close dialog + cy.get(`.${Classes.DIALOG_FOOTER} .data-cy-close`) + .click(); + + // wait for dialog to be closed otherwise + // it could still be visible in next it() + cy.get('.shortcutsDialog') + .should('not.be.visible'); + }); + + it("should open prefs dialog", () => { + cy.triggerFakeCombo("CmdOrCtrl+,"); + + cy.get('.data-cy-prefs-dialog') + .should('be.visible'); + + // close dialog + cy.get(`.${Classes.DIALOG_FOOTER} .data-cy-close`) + .click(); + + // wait for dialog to be closed otherwise + // it could still be visible in next it() + cy.get('.shortcutsDialog') + .should('not.be.visible'); + }); + + it("should reload file view", () => { + cy.triggerFakeCombo("CmdOrCtrl+R"); + + cy.get('@refreshActiveView') + .should('be.calledOnce'); + }); + + it("should open terminal", () => { + cy.triggerFakeCombo("CmdOrCtrl+K"); + + cy.get('@openTerminal') + .should('be.calledOnce'); + }); + + it("should activate next tab", () => { + cy.triggerFakeCombo("Ctrl+Tab"); + + cy.get('@cycleTab') + .should('be.calledOnce') + .should('be.calledWith', 1); + }); + + it("should activate previous tab", () => { + cy.triggerFakeCombo("Ctrl+Shift+Tab"); + + cy.get('@cycleTab') + .should('be.calledOnce') + .should('be.calledWith', -1); + }); + + it("should open a new tab", () => { + cy.triggerFakeCombo("CmdOrCtrl+T"); + + cy.get('@addCache') + .should('be.calledOnce'); + }); + + it("should close tab", () => { + cy.triggerFakeCombo("CmdOrCtrl+W"); + + cy.get('@closeTab') + .should('be.calledOnce'); + }); + + it("should toggle split view", () => { + // initial state: split view active + cy.get("#view_1") + .should("not.have.class", "active") + .and('be.visible'); + + cy.get("#view_0") + .should("have.class", "active") + .and('be.visible'); + + // de-activate split view + cy.triggerFakeCombo("CmdOrCtrl+Shift+Alt+V"); + + // check status: we should have only one call + cy.get('@toggleSplitViewMode') + .should('be.calledOnce'); + + cy.get("#view_0") + .should('be.visible') + .and('have.class', 'active'); + + cy.get("#view_1") + .should('not.be.visible'); + + // re-activate split view + cy.triggerFakeCombo("CmdOrCtrl+Shift+Alt+V"); + + // check status: should have two calls now + cy.get('@toggleSplitViewMode') + .should('be.calledTwice'); + + cy.get("#view_0") + .should('be.visible') + .and('not.have.class', 'active'); + + cy.get("#view_1") + .should('be.visible') + .and('have.class', 'active'); + }); +}); diff --git a/e2e/cypress/support/commands.ts b/e2e/cypress/support/commands.ts index b324bfc1..4e2d9095 100644 --- a/e2e/cypress/support/commands.ts +++ b/e2e/cypress/support/commands.ts @@ -2,15 +2,32 @@ declare global { namespace Cypress { interface Chainable { /** - * Yields "foo" + * Yields "json object" * - * @returns {typeof foo} + * @returns {typeof object} * @memberof Chainable * @example - * cy.foo().then(f = ...) // f is "foo" + * cy.CDAndList('/').then(json => ...) */ CDAndList: typeof CDList; + /** + * Yields document.body + * + * @returns {typeof Body} + * @memberof Chainable + * @example + * cy.triggerHotkey('{meta}f').then(body => ...) + */ triggerHotkey: typeof triggerHotkey; + /** + * Yields document + * + * @returns {typeof Document} + * @memberof Chainable + * @example + * cy.triggerFakeCombo('CmdOrCtrl+Shift+C').then(doc => ...) + */ + triggerFakeCombo: typeof triggerFakeCombo } } } @@ -29,9 +46,16 @@ export function CDList(viewId = 0, path: string, fixture = "files.json") { }); } -export function triggerHotkey(hotkey: string) { - return cy.get("body").type(hotkey); +export function triggerHotkey(hotkey: string, options = {}) { + return cy.get("body").type(hotkey, options); +} + +export function triggerFakeCombo(combo: string, data = { title: "hey!"}) { + cy.log('triggering', { combo, data }); + return cy.document() + .trigger('menu_accelerator', { combo, data } ); } Cypress.Commands.add("CDAndList", CDList); Cypress.Commands.add("triggerHotkey", triggerHotkey); +Cypress.Commands.add("triggerFakeCombo", triggerFakeCombo); diff --git a/e2e/webpack.config.e2e.js b/e2e/webpack.config.e2e.js index d1ac934a..435f44ea 100644 --- a/e2e/webpack.config.e2e.js +++ b/e2e/webpack.config.e2e.js @@ -14,9 +14,9 @@ const baseConfig = { }, externals: { os: `{release: function() { return "${release}"}, tmpdir: function() { return "/tmpdir" }, homedir: function() { return "/homedir" }}`, - process: `{process: "foo", platform: "${platform}"}`, + process: `{process: "React-Explorer", platform: "${platform}"}`, electron: - '{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; } } } }', + '{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; } } } }', child_process: "{exec: function(str, cb) { cb(); }}", fs: "{}", path: "{}", diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx index 0823597f..a49bd84f 100644 --- a/src/components/Nav.tsx +++ b/src/components/Nav.tsx @@ -50,8 +50,10 @@ class NavComponent extends React.Component { }; onToggleSplitView = () => { - const winState = this.appState.winStates[0]; - winState.toggleSplitViewMode(); + if (this.appState.isExplorer) { + const winState = this.appState.winStates[0]; + winState.toggleSplitViewMode(); + } }; onOpenPrefs = () => { diff --git a/src/components/WithMenuAccelerators.tsx b/src/components/WithMenuAccelerators.tsx index 2e213781..85354af4 100644 --- a/src/components/WithMenuAccelerators.tsx +++ b/src/components/WithMenuAccelerators.tsx @@ -87,6 +87,7 @@ export function WithMenuAccelerators< e: MenuAcceleratorEvent, data: { combo: string; data: any } ) => { + console.log('******* onAccelerator !!', e, data); // check if combo is valid const callback = this.getCallback(data.combo); if (typeof callback === "function") { diff --git a/src/components/dialogs/ShortcutsDialog.tsx b/src/components/dialogs/ShortcutsDialog.tsx index cdbfc0a7..5eb070a8 100644 --- a/src/components/dialogs/ShortcutsDialog.tsx +++ b/src/components/dialogs/ShortcutsDialog.tsx @@ -30,7 +30,8 @@ class ShortcutsDialogClass extends React.Component{ { combo: "mod + s", label: t('SHORTCUT.MAIN.KEYBOARD_SHORTCUTS') }, { combo: "mod + ,", label: t('SHORTCUT.MAIN.PREFERENCES') }, { combo: "alt + mod + i", label: t('SHORTCUT.OPEN_DEVTOOLS') }, - { combo: "mod + q", label: t('SHORTCUT.MAIN.QUIT') } + { combo: "mod + q", label: t('SHORTCUT.MAIN.QUIT') }, + { combo: "mod + alt + shift + v", label: t('NAV.SPLITVIEW') } ], // group: t('SHORTCUT.GROUP.ACTIVE_VIEW') [ @@ -111,7 +112,7 @@ class ShortcutsDialogClass extends React.Component{
-
diff --git a/src/components/shortcuts/MenuAccelerators.tsx b/src/components/shortcuts/MenuAccelerators.tsx index ef2f241a..1b6ce082 100644 --- a/src/components/shortcuts/MenuAccelerators.tsx +++ b/src/components/shortcuts/MenuAccelerators.tsx @@ -170,6 +170,13 @@ class MenuAcceleratorsClass extends React.Component { } }; + onToggleSplitView = () => { + if (this.appState.isExplorer) { + const winState = this.appState.winStates[0]; + winState.toggleSplitViewMode(); + } + } + renderMenuAccelerators() { return ( @@ -217,6 +224,10 @@ class MenuAcceleratorsClass extends React.Component { combo="CmdOrCtrl+W" onClick={this.onCloseTab} > + ); } diff --git a/src/electron/appMenus.ts b/src/electron/appMenus.ts index 8f0e56bd..c587c835 100644 --- a/src/electron/appMenus.ts +++ b/src/electron/appMenus.ts @@ -149,6 +149,12 @@ export class AppMenu { { label: menuStrings['TITLE_VIEW'], submenu: [ + { + label: menuStrings['TOGGLE_SPLITVIEW'], + accelerator: 'CmdOrCtrl+Shift+Alt+V', + click: this.sendComboEvent + }, + { type: 'separator' }, { label: menuStrings['RELOAD_VIEW'], accelerator: 'CmdOrCtrl+R', diff --git a/src/locale/lang/en.json b/src/locale/lang/en.json index 795f76c3..47ab3590 100644 --- a/src/locale/lang/en.json +++ b/src/locale/lang/en.json @@ -15,7 +15,7 @@ "TRANSFERS": "Transfers", "PREFS": "Preferences", "SHORTCUTS": "Keyboard shortcuts", - "SPLITVIEW": "Toggle dual view" + "SPLITVIEW": "Toggle split view" }, "DRAG": { "MULTIPLE": "Copy {{count}} elements" @@ -244,7 +244,8 @@ "SELECT_PREVIOUS_TAB": "Select previous tab", "NEW_TAB": "New tab", "CLOSE_TAB": "Close tab", - "OK": "OK" + "OK": "OK", + "TOGGLE_SPLITVIEW": "Toggle Split View" } } } \ No newline at end of file diff --git a/src/locale/lang/fr.json b/src/locale/lang/fr.json index 3b5d70c3..81ae7e19 100644 --- a/src/locale/lang/fr.json +++ b/src/locale/lang/fr.json @@ -15,7 +15,7 @@ "TRANSFERS": "Téléchargements", "PREFS": "Préférences", "SHORTCUTS": "Raccourcis clavier", - "SPLITVIEW": "Alterner entre double vue et vue unique" + "SPLITVIEW": "Activer/Désactiver double vue" }, "DRAG": { "MULTIPLE": "Copier {{count}} éléments" @@ -244,7 +244,8 @@ "SELECT_PREVIOUS_TAB": "Sélectionner l'onglet précédent", "NEW_TAB": "Nouvel onglet", "CLOSE_TAB": "Fermer l'onglet", - "OK": "OK" + "OK": "OK", + "TOGGLE_SPLITVIEW": "Activer/Désactiver Vue Double" } } } \ No newline at end of file diff --git a/src/services/plugins/FsGeneric.ts b/src/services/plugins/FsGeneric.ts index 101c2ca1..42647ed1 100644 --- a/src/services/plugins/FsGeneric.ts +++ b/src/services/plugins/FsGeneric.ts @@ -15,7 +15,7 @@ class GenericApi implements FsApi { } join(...paths: string[]): string { - return this.join(...paths); + return paths.join('/'); } isConnected(): boolean { @@ -31,21 +31,6 @@ class GenericApi implements FsApi { return Promise.resolve(10); } - // copy(source: string, files: string[], dest: string): Promise & cp.ProgressEmitter { - // console.log('Generic.copy'); - // const prom: Promise & cp.ProgressEmitter = new Promise((resolve, reject) => { - // setTimeout(() => { - // resolve(); - // }, 2000); - // }) as Promise & cp.ProgressEmitter; - - // prom.on = (name, handler): Promise => { - // return prom; - // } - - // return prom; - // } - login(server?: string, credentials?: ICredentials): Promise { return Promise.resolve(); }