diff --git a/integration_test/add_friends_test.js b/integration_test/add_friends_test.js index aa8c04d4d..90dbcf230 100644 --- a/integration_test/add_friends_test.js +++ b/integration_test/add_friends_test.js @@ -1,7 +1,8 @@ /* eslint-disable func-names */ /* eslint-disable import/no-extraneous-dependencies */ -const common = require('./common'); const { afterEach, beforeEach, describe, it } = require('mocha'); + +const common = require('./common'); const ConversationPage = require('./page-objects/conversation.page'); describe('Add friends', function() { diff --git a/integration_test/closed_group_test.js b/integration_test/closed_group_test.js index c937c97ff..12345b3e5 100644 --- a/integration_test/closed_group_test.js +++ b/integration_test/closed_group_test.js @@ -1,7 +1,8 @@ /* eslint-disable func-names */ /* eslint-disable import/no-extraneous-dependencies */ -const common = require('./common'); const { afterEach, beforeEach, describe, it } = require('mocha'); +const common = require('./common'); + const ConversationPage = require('./page-objects/conversation.page'); describe('Closed groups', function() { @@ -27,80 +28,8 @@ describe('Closed groups', function() { await app.client.element(ConversationPage.globeButtonSection).click(); await app.client.element(ConversationPage.createClosedGroupButton).click(); - // fill the groupname - await common.setValueWrapper( - app, - ConversationPage.closedGroupNameTextarea, - common.VALID_CLOSED_GROUP_NAME1 - ); - await app.client - .element(ConversationPage.closedGroupNameTextarea) - .getValue() - .should.eventually.equal(common.VALID_CLOSED_GROUP_NAME1); - - await app.client - .element(ConversationPage.createClosedGroupMemberItem) - .isVisible(); - - // select the first friend as a member of the groups being created - await app.client - .element(ConversationPage.createClosedGroupMemberItem) - .click(); - await app.client - .element(ConversationPage.createClosedGroupMemberItemSelected) - .isVisible(); - - // trigger the creation of the group - await app.client - .element(ConversationPage.validateCreationClosedGroupButton) - .click(); - - await app.client.waitForExist( - ConversationPage.sessionToastGroupCreatedSuccess, - 1000 - ); - await app.client.isExisting( - ConversationPage.headerTitleGroupName(common.VALID_CLOSED_GROUP_NAME1) - ).should.eventually.be.true; - await app.client - .element(ConversationPage.headerTitleMembers(2)) - .isVisible(); - - // validate overlay is closed - await app.client.isExisting(ConversationPage.leftPaneOverlay).should - .eventually.be.false; - - // move back to the conversation section - await app.client - .element(ConversationPage.conversationButtonSection) - .click(); - - // validate open chat has been added - await app.client.isExisting( - ConversationPage.rowOpenGroupConversationName( - common.VALID_CLOSED_GROUP_NAME1 - ) - ).should.eventually.be.true; - - // next check app2 has been invited and has the group in its conversations - await app2.client.waitForExist( - ConversationPage.rowOpenGroupConversationName( - common.VALID_CLOSED_GROUP_NAME1 - ), - 6000 - ); - // open the closed group conversation on app2 - await app2.client - .element(ConversationPage.conversationButtonSection) - .click(); - await common.timeout(500); - await app2.client - .element( - ConversationPage.rowOpenGroupConversationName( - common.VALID_CLOSED_GROUP_NAME1 - ) - ) - .click(); + // create group and add new friend + await common.addFriendToNewClosedGroup(app, app2); // send a message from app and validate it is received on app2 const textMessage = common.generateSendMessageText(); diff --git a/integration_test/common.js b/integration_test/common.js index 306ae9bf1..cac5a9526 100644 --- a/integration_test/common.js +++ b/integration_test/common.js @@ -4,15 +4,17 @@ const { Application } = require('spectron'); const path = require('path'); +const url = require('url'); +const http = require('http'); +const fse = require('fs-extra'); +const { exec } = require('child_process'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); +const CommonPage = require('./page-objects/common.page'); const RegistrationPage = require('./page-objects/registration.page'); const ConversationPage = require('./page-objects/conversation.page'); -const { exec } = require('child_process'); -const url = require('url'); -const http = require('http'); -const fse = require('fs-extra'); +const SettingsPage = require('./page-objects/settings.page'); chai.should(); chai.use(chaiAsPromised); @@ -35,6 +37,12 @@ module.exports = { '054e1ca8681082dbd9aad1cf6fc89a32254e15cba50c75b5a73ac10a0b96bcbd2a', TEST_DISPLAY_NAME2: 'integration_tester_2', + TEST_MNEMONIC3: + 'alpine lukewarm oncoming blender kiwi fuel lobster upkeep vogue simplest gasp fully simplest', + TEST_PUBKEY3: + '05f8662b6e83da5a31007cc3ded44c601f191e07999acb6db2314a896048d9036c', + TEST_DISPLAY_NAME3: 'integration_tester_3', + /* ************** OPEN GROUPS ****************** */ VALID_GROUP_URL: 'https://chat.getsession.org', VALID_GROUP_URL2: 'https://chat-dev.lokinet.org', @@ -50,36 +58,43 @@ module.exports = { return new Promise(resolve => setTimeout(resolve, ms)); }, + async closeToast(app) { + app.client.element(CommonPage.toastCloseButton).click(); + }, + // a wrapper to work around electron/spectron bug async setValueWrapper(app, selector, value) { - await app.client.element(selector).click(); // keys, setValue and addValue hang on certain platforms - // could put a branch here to use one of those - // if we know what platforms are good and which ones are broken - await app.client.execute( - (slctr, val) => { - // eslint-disable-next-line no-undef - const iter = document.evaluate( - slctr, - // eslint-disable-next-line no-undef - document, - null, + + if (process.platform === 'darwin') { + await app.client.execute( + (slctr, val) => { // eslint-disable-next-line no-undef - XPathResult.UNORDERED_NODE_ITERATOR_TYPE, - null - ); - const elem = iter.iterateNext(); - if (elem) { - elem.value = val; - } else { - console.error('Cant find', slctr, elem, iter); - } - }, - selector, - value - ); - // let session js detect the text change - await app.client.element(selector).click(); + const iter = document.evaluate( + slctr, + // eslint-disable-next-line no-undef + document, + null, + // eslint-disable-next-line no-undef + XPathResult.UNORDERED_NODE_ITERATOR_TYPE, + null + ); + const elem = iter.iterateNext(); + if (elem) { + elem.value = val; + } else { + console.error('Cant find', slctr, elem, iter); + } + }, + selector, + value + ); + // let session js detect the text change + await app.client.element(selector).click(); + } else { + // Linux & Windows don't require wrapper + await app.client.element(selector).setValue(value); + } }, async startApp(environment = 'test-integration-session') { @@ -135,12 +150,8 @@ module.exports = { ? 'taskkill /im electron.exe /t /f' : 'pkill -f "node_modules/electron/dist/electron" | pkill -f "node_modules/.bin/electron"'; return new Promise(resolve => { - exec(killStr, (err, stdout, stderr) => { - if (err) { - resolve({ stdout, stderr }); - } else { - resolve({ stdout, stderr }); - } + exec(killStr, (_err, stdout, stderr) => { + resolve({ stdout, stderr }); }); }); }, @@ -197,13 +208,14 @@ module.exports = { return app; }, - async startAndStub2(props) { - const app2 = await this.startAndStub({ - env: 'test-integration-session-2', + async startAndStubN(props, n) { + // Make app with stub as number n + const appN = await this.startAndStub({ + env: `test-integration-session-${n}`, ...props, }); - return app2; + return appN; }, async restoreFromMnemonic(app, mnemonic, displayName) { @@ -221,7 +233,9 @@ module.exports = { displayName ); - await app.client.element(RegistrationPage.continueSessionButton).click(); + // await app.client.element(RegistrationPage.continueSessionButton).click(); + await app.client.keys('Enter'); + await app.client.waitForExist( RegistrationPage.conversationListContainer, 4000 @@ -243,7 +257,7 @@ module.exports = { const [app1, app2] = await Promise.all([ this.startAndStub(app1Props), - this.startAndStub2(app2Props), + this.startAndStubN(app2Props, 2), ]); /** add each other as friends */ @@ -307,11 +321,89 @@ module.exports = { return [app1, app2]; }, + async addFriendToNewClosedGroup(app, app2) { + await this.setValueWrapper( + app, + ConversationPage.closedGroupNameTextarea, + this.VALID_CLOSED_GROUP_NAME1 + ); + await app.client + .element(ConversationPage.closedGroupNameTextarea) + .getValue() + .should.eventually.equal(this.VALID_CLOSED_GROUP_NAME1); + + await app.client + .element(ConversationPage.createClosedGroupMemberItem) + .isVisible().should.eventually.be.true; + + // select the first friend as a member of the groups being created + await app.client + .element(ConversationPage.createClosedGroupMemberItem) + .click(); + await app.client + .element(ConversationPage.createClosedGroupMemberItemSelected) + .isVisible().should.eventually.be.true; + + // trigger the creation of the group + await app.client + .element(ConversationPage.validateCreationClosedGroupButton) + .click(); + + await app.client.waitForExist( + ConversationPage.sessionToastGroupCreatedSuccess, + 1000 + ); + await app.client.isExisting( + ConversationPage.headerTitleGroupName(this.VALID_CLOSED_GROUP_NAME1) + ).should.eventually.be.true; + await app.client.element(ConversationPage.headerTitleMembers(2)).isVisible() + .should.eventually.be.true; + + // validate overlay is closed + await app.client + .isExisting(ConversationPage.leftPaneOverlay) + .should.eventually.be.equal(false); + + // move back to the conversation section + await app.client + .element(ConversationPage.conversationButtonSection) + .click(); + + // validate open chat has been added + await app.client.isExisting( + ConversationPage.rowOpenGroupConversationName( + this.VALID_CLOSED_GROUP_NAME1 + ) + ).should.eventually.be.true; + + // next check app2 has been invited and has the group in its conversations + await app2.client.waitForExist( + ConversationPage.rowOpenGroupConversationName( + this.VALID_CLOSED_GROUP_NAME1 + ), + 6000 + ); + // open the closed group conversation on app2 + await app2.client + .element(ConversationPage.conversationButtonSection) + .click(); + await this.timeout(500); + await app2.client + .element( + ConversationPage.rowOpenGroupConversationName( + this.VALID_CLOSED_GROUP_NAME1 + ) + ) + .click(); + }, + async linkApp2ToApp(app1, app2) { // app needs to be logged in as user1 and app2 needs to be logged out // start the pairing dialog for the first app - await app1.client.element(ConversationPage.settingsButtonSection).click(); - await app1.client.element(ConversationPage.deviceSettingsRow).click(); + await app1.client.element(SettingsPage.settingsButtonSection).click(); + await app1.client + .element(SettingsPage.settingsRowWithText('Devices')) + .click(); await app1.client.isVisible(ConversationPage.noPairedDeviceMessage); // we should not find the linkDeviceButtonDisabled button (as DISABLED) @@ -375,7 +467,9 @@ module.exports = { .should.eventually.be.true; await app1.client.element(ConversationPage.settingsButtonSection).click(); - await app1.client.element(ConversationPage.deviceSettingsRow).click(); + await app1.client + .element(ConversationPage.settingsRowWithText('Devices')) + .click(); await app1.client.isExisting(ConversationPage.linkDeviceButtonDisabled) .should.eventually.be.true; // click the unlink button @@ -411,6 +505,31 @@ module.exports = { } }, + async sendMessage(app, messageText, fileLocation = undefined) { + await this.setValueWrapper( + app, + ConversationPage.sendMessageTextarea, + messageText + ); + await app.client + .element(ConversationPage.sendMessageTextarea) + .getValue() + .should.eventually.equal(messageText); + + // attach a file + if (fileLocation) { + await this.setValueWrapper( + app, + ConversationPage.attachmentInput, + fileLocation + ); + } + + // send message + await app.client.element(ConversationPage.sendMessageTextarea).click(); + await app.client.keys('Enter'); + }, + generateSendMessageText: () => `Test message from integration tests ${Date.now()}`, diff --git a/integration_test/integration_test.js b/integration_test/integration_test.js index 87eee0fcc..f5a78e070 100644 --- a/integration_test/integration_test.js +++ b/integration_test/integration_test.js @@ -11,6 +11,8 @@ require('./open_group_test'); require('./add_friends_test'); require('./link_device_test'); require('./closed_group_test'); +require('./message_functions_test'); +require('./settings_test'); before(async () => { // start the app once before all tests to get the platform-dependent diff --git a/integration_test/link_device_test.js b/integration_test/link_device_test.js index a206a5e9c..63239e58a 100644 --- a/integration_test/link_device_test.js +++ b/integration_test/link_device_test.js @@ -2,8 +2,8 @@ /* eslint-disable more/no-then */ /* eslint-disable func-names */ /* eslint-disable import/no-extraneous-dependencies */ -const common = require('./common'); const { afterEach, beforeEach, describe, it } = require('mocha'); +const common = require('./common'); describe('Link Device', function() { let app; @@ -27,7 +27,7 @@ describe('Link Device', function() { [app, app2] = await Promise.all([ common.startAndStub(app1Props), - common.startAndStub2(app2Props), + common.startAndStubN(app2Props, 2), ]); }); diff --git a/integration_test/message_functions_test.js b/integration_test/message_functions_test.js new file mode 100644 index 000000000..469171f2d --- /dev/null +++ b/integration_test/message_functions_test.js @@ -0,0 +1,86 @@ +/* eslint-disable prefer-destructuring */ +/* eslint-disable more/no-then */ +/* eslint-disable func-names */ +/* eslint-disable import/no-extraneous-dependencies */ +const path = require('path'); + +const { after, before, describe, it } = require('mocha'); +const common = require('./common'); +const ConversationPage = require('./page-objects/conversation.page'); + +describe('Message Functions', function() { + let app; + let app2; + this.timeout(60000); + this.slow(15000); + + before(async () => { + await common.killallElectron(); + await common.stopStubSnodeServer(); + + [app, app2] = await common.startAppsAsFriends(); + }); + + after(async () => { + await common.stopApp(app); + await common.killallElectron(); + await common.stopStubSnodeServer(); + }); + + it('can send attachment', async () => { + await app.client.element(ConversationPage.globeButtonSection).click(); + await app.client.element(ConversationPage.createClosedGroupButton).click(); + + // create group and add new friend + await common.addFriendToNewClosedGroup(app, app2); + + // send attachment from app1 to closed group + const fileLocation = path.join(__dirname, 'test_attachment'); + const messageText = 'test_attachment'; + + await common.sendMessage(app, messageText, fileLocation); + + // validate attachment sent + await app.client.waitForExist( + ConversationPage.existingSendMessageText(messageText), + 3000 + ); + // validate attachment recieved + await app2.client.waitForExist( + ConversationPage.existingReceivedMessageText(messageText), + 5000 + ); + }); + + it('can delete message', async () => { + const messageText = 'delete_me'; + await common.sendMessage(app, messageText); + + await app.client.waitForExist( + ConversationPage.existingSendMessageText(messageText), + 6000 + ); + await app2.client.waitForExist( + ConversationPage.existingReceivedMessageText(messageText), + 7000 + ); + + // delete message in context menu + await app.client + .element(ConversationPage.messageCtxMenu(messageText)) + .click(); + await app.client.element(ConversationPage.deleteMessageCtxButton).click(); + + // delete messaage from modal + await app.client.waitForExist( + ConversationPage.deleteMessageModalButton, + 5000 + ); + await app.client.element(ConversationPage.deleteMessageModalButton).click(); + + // verify the message is actually deleted + await app.client.isExisting( + ConversationPage.existingSendMessageText(messageText) + ).should.eventually.be.false; + }); +}); diff --git a/integration_test/message_sync_test.js b/integration_test/message_sync_test.js new file mode 100644 index 000000000..4d40c97a9 --- /dev/null +++ b/integration_test/message_sync_test.js @@ -0,0 +1,49 @@ +/* eslint-disable func-names */ +/* eslint-disable import/no-extraneous-dependencies */ +const { afterEach, beforeEach, describe, it } = require('mocha'); + +const common = require('./common'); + +describe('Message Syncing', function() { + let app; + let app2; + this.timeout(60000); + this.slow(15000); + + beforeEach(async () => { + await common.killallElectron(); + await common.stopStubSnodeServer(); + + const app1Props = { + mnemonic: common.TEST_MNEMONIC1, + displayName: common.TEST_DISPLAY_NAME1, + stubSnode: true, + }; + + const app2Props = { + mnemonic: common.TEST_MNEMONIC2, + displayName: common.TEST_DISPLAY_NAME2, + stubSnode: true, + }; + + [app, app2] = await Promise.all([ + common.startAndStub(app1Props), + common.startAndStubN(app2Props, 2), + ]); + }); + + afterEach(async () => { + await common.killallElectron(); + await common.stopStubSnodeServer(); + }); + + it('message syncing between linked devices', async () => { + await common.linkApp2ToApp(app, app2); + }); + + it('unlink two devices', async () => { + await common.linkApp2ToApp(app, app2); + await common.timeout(1000); + await common.triggerUnlinkApp2FromApp(app, app2); + }); +}); diff --git a/integration_test/open_group_test.js b/integration_test/open_group_test.js index 9910fd68e..dba3d13e7 100644 --- a/integration_test/open_group_test.js +++ b/integration_test/open_group_test.js @@ -1,7 +1,8 @@ /* eslint-disable func-names */ /* eslint-disable import/no-extraneous-dependencies */ -const common = require('./common'); const { afterEach, beforeEach, describe, it } = require('mocha'); + +const common = require('./common'); const ConversationPage = require('./page-objects/conversation.page'); describe('Open groups', function() { @@ -49,10 +50,9 @@ describe('Open groups', function() { .eventually.be.false; // validate open chat has been added - await app.client.waitForExist( - ConversationPage.rowOpenGroupConversationName(name), - 4000 - ); + await app.client.isExisting( + ConversationPage.rowOpenGroupConversationName(name) + ).should.eventually.be.true; } it('openGroup: works with valid open group url', async () => { diff --git a/integration_test/page-objects/common.page.js b/integration_test/page-objects/common.page.js index e5374f6d6..6d249ff02 100644 --- a/integration_test/page-objects/common.page.js +++ b/integration_test/page-objects/common.page.js @@ -11,6 +11,7 @@ module.exports = { `${module.exports.divRoleButtonWithText(text)}[contains(@class, "danger")]`, inputWithPlaceholder: placeholder => `//input[contains(@placeholder, "${placeholder}")]`, + inputWithId: id => `//input[contains(@id, '${id}')]`, textAreaWithPlaceholder: placeholder => `//textarea[contains(@placeholder, "${placeholder}")]`, byId: id => `//*[@id="${id}"]`, @@ -21,4 +22,6 @@ module.exports = { module.exports.objWithClassAndText('span', classname, text), toastWithText: text => module.exports.divWithClassAndText('session-toast-wrapper', text), + toastCloseButton: + '//div[contains(@class, "session-toast-wrapper")]//div[contains(@class, "toast-close")]/div', }; diff --git a/integration_test/page-objects/conversation.page.js b/integration_test/page-objects/conversation.page.js index a58b5b9e0..bffb5d405 100644 --- a/integration_test/page-objects/conversation.page.js +++ b/integration_test/page-objects/conversation.page.js @@ -9,11 +9,11 @@ module.exports = { 'Send your first message' ), existingSendMessageText: textMessage => - `//*[contains(@class, "module-message__text--outgoing")and .//span[contains(@class, "text-selectable")][contains(string(), '${textMessage}')]]`, + `//*[contains(@class, "module-message__text--outgoing") and .//span[contains(@class, "text-selectable")][contains(string(), '${textMessage}')]]`, existingFriendRequestText: textMessage => - `//*[contains(@class, "module-message-friend-request__container")and .//span[contains(@class, "text-selectable")][contains(string(), '${textMessage}')]]`, + `//*[contains(@class, "module-message-friend-request__container") and .//span[contains(@class, "text-selectable")][contains(string(), '${textMessage}')]]`, existingReceivedMessageText: textMessage => - `//*[contains(@class, "module-message__text--incoming")and .//span[contains(@class, "text-selectable")][contains(string(), '${textMessage}')]]`, + `//*[contains(@class, "module-message__text--incoming") and .//span[contains(@class, "text-selectable")][contains(string(), '${textMessage}')]]`, // conversations conversationButtonSection: @@ -28,6 +28,17 @@ module.exports = { `${number} members` ), + attachmentInput: '//*[contains(@class, "choose-file")]/input[@type="file"]', + attachmentButton: '//*[contains(@class, "choose-file")]/button', + + messageCtxMenu: message => + `//div[contains(@class, 'message-wrapper')]//span[contains(string(), '${message}')]/parent::div/parent::div/parent::div/parent::div//div[contains(@class, 'module-message__buttons__menu')]`, + + deleteMessageCtxButton: + '//*[contains(@class, "react-contextmenu--visible")]/div[contains(string(), "Delete")]', + deleteMessageModalButton: + '//*[contains(@class, "session-modal")]//div[contains(string(), "Delete") and contains(@class, "session-button")]', + // channels globeButtonSection: '//*[contains(@class,"session-icon-button") and .//*[contains(@class, "globe")]]', @@ -87,12 +98,6 @@ module.exports = { acceptedFriendRequestMessage: '//*[contains(@class, "module-friend-request__title")][contains(string(), "Friend request accepted")]', - // settings - settingsButtonSection: - '//*[contains(@class,"session-icon-button") and .//*[contains(@class, "gear")]]', - deviceSettingsRow: - '//*[contains(@class, "left-pane-setting-category-list-item")][contains(string(), "Devices")]', - descriptionDeleteAccount: commonPage.spanWithClassAndText( 'session-confirm-main-message', 'Are you sure you want to delete your account?' diff --git a/integration_test/page-objects/settings.page.js b/integration_test/page-objects/settings.page.js new file mode 100644 index 000000000..3adf33f05 --- /dev/null +++ b/integration_test/page-objects/settings.page.js @@ -0,0 +1,20 @@ +module.exports = { + // settings view + settingsButtonSection: + '//*[contains(@class,"session-icon-button") and .//*[contains(@class, "gear")]]', + settingsRowWithText: text => + `//*[contains(@class, "left-pane-setting-category-list-item")][contains(string(), '${text}')]`, + + leftPaneSettingsButton: `//*[contains(@class,"session-icon-button") and .//*[contains(@class, "gear")]]`, + + settingToggleWithText: text => + `//div[contains(@class, 'session-settings-item') and contains(string(), '${text}')]//*[contains(@class, 'session-toggle')]`, + settingButtonWithText: text => + `//div[contains(@class, 'session-settings-item')]//*[contains(@class, 'session-button') and contains(string(), '${text}')]`, + settingCategoryWithText: text => + `//div[contains(@class, 'left-pane-setting-category-list-item') and contains(string(), '${text}')]`, + + // Confirm is a boolean. Selects confirmation input + passwordSetModalInput: _confirm => + `//input[@id = 'password-modal-input${_confirm ? '-confirm' : ''}']`, +}; diff --git a/integration_test/registration_test.js b/integration_test/registration_test.js index a60a5cb52..69bf4b27f 100644 --- a/integration_test/registration_test.js +++ b/integration_test/registration_test.js @@ -2,8 +2,9 @@ /* eslint-disable func-names */ /* eslint-disable import/no-extraneous-dependencies */ -const common = require('./common'); const { afterEach, beforeEach, describe, it } = require('mocha'); + +const common = require('./common'); const RegistrationPage = require('./page-objects/registration.page'); const ConversationPage = require('./page-objects/conversation.page'); diff --git a/integration_test/settings_test.js b/integration_test/settings_test.js new file mode 100644 index 000000000..b166592b3 --- /dev/null +++ b/integration_test/settings_test.js @@ -0,0 +1,120 @@ +/* eslint-disable prefer-destructuring */ +/* eslint-disable more/no-then */ +/* eslint-disable func-names */ +/* eslint-disable import/no-extraneous-dependencies */ + +const { after, before, describe, it } = require('mocha'); +const common = require('./common'); + +const SettingsPage = require('./page-objects/settings.page'); +const CommonPage = require('./page-objects/common.page'); + +// Generate random password +const password = Math.random() + .toString(36) + .substr(2, 8); +const passwordInputID = 'password-modal-input'; + +describe('Settings', function() { + let app; + this.timeout(60000); + this.slow(15000); + + before(async () => { + await common.killallElectron(); + await common.stopStubSnodeServer(); + + const appProps = { + mnemonic: common.TEST_MNEMONIC1, + displayName: common.TEST_DISPLAY_NAME1, + stubSnode: true, + }; + + app = await common.startAndStub(appProps); + }); + + after(async () => { + await common.stopApp(app); + await common.killallElectron(); + await common.stopStubSnodeServer(); + }); + + it('can toggle menubar', async () => { + const menuBarVisible = await app.browserWindow.isMenuBarVisible(); + + await app.client.element(SettingsPage.settingsButtonSection).click(); + await app.client + .element(SettingsPage.settingToggleWithText('Hide Menu Bar')) + .click(); + + // Confirm that toggling works + const menuBarToggled = await app.browserWindow.isMenuBarVisible(); + menuBarToggled.should.equal(!menuBarVisible); + }); + + it('can set password', async () => { + await app.client + .element(SettingsPage.settingsRowWithText('Privacy')) + .click(); + + await app.client + .element(SettingsPage.settingButtonWithText('Set Password')) + .click(); + + await common.setValueWrapper( + app, + CommonPage.inputWithId(passwordInputID), + password + ); + await common.setValueWrapper( + app, + CommonPage.inputWithId(`${passwordInputID}-confirm`), + password + ); + + await app.client.keys('Enter'); + + // Verify password set + await app.client.waitForExist( + CommonPage.toastWithText('Set Password'), + 2000 + ); + + await common.closeToast(app); + }); + + it('can remove password', async () => { + // Enter password to unlock settings + await common.setValueWrapper( + app, + CommonPage.inputWithId('password-lock-input'), + password + ); + + await app.client.keys('Enter'); + + // Remove password + await app.client + .element(SettingsPage.settingButtonWithText('Remove Password')) + .click(); + + await common.setValueWrapper( + app, + CommonPage.inputWithId(passwordInputID), + password + ); + + await app.client.keys('Enter'); + + // Verify password removed with toast + await app.client.waitForExist( + CommonPage.toastWithText('Removed Password'), + 2000 + ); + + // Verify password actully removed + await app.client.isExisting( + CommonPage.divWithClass('session-settings__password-lock') + ).should.eventually.be.false; + }); +}); diff --git a/integration_test/test_attachment b/integration_test/test_attachment new file mode 100644 index 000000000..06d740502 Binary files /dev/null and b/integration_test/test_attachment differ diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 8c8a04c8a..89c8983bb 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -140,15 +140,15 @@ a { } input[type='file'] { - display: none; + // Must be displayed in order to programmatically + // insert file paths) position: absolute; width: 100%; height: 100%; - opacity: 0; top: 0; left: 0; cursor: pointer; - z-index: 1; + z-index: -100; } } diff --git a/ts/components/session/SessionPasswordModal.tsx b/ts/components/session/SessionPasswordModal.tsx index 4d7091057..ef1a4c38d 100644 --- a/ts/components/session/SessionPasswordModal.tsx +++ b/ts/components/session/SessionPasswordModal.tsx @@ -139,18 +139,28 @@ export class SessionPasswordModal extends React.Component { ); } - private async setPassword(onSuccess: any) { - if (!this.passwordInput.current || !this.passwordInputConfirm.current) { + private async setPassword(onSuccess?: any) { + // Only initial input required for PasswordAction.Remove + if ( + !this.passwordInput.current || + (!this.passwordInputConfirm.current && + this.props.action !== PasswordAction.Remove) + ) { return; } // Trim leading / trailing whitespace for UX const enteredPassword = String(this.passwordInput.current.value).trim(); - const enteredPasswordConfirm = String( - this.passwordInputConfirm.current.value - ).trim(); + const enteredPasswordConfirm = + (this.passwordInputConfirm.current && + String(this.passwordInputConfirm.current.value).trim()) || + ''; - if (enteredPassword.length === 0 || enteredPasswordConfirm.length === 0) { + if ( + enteredPassword.length === 0 || + (enteredPasswordConfirm.length === 0 && + this.props.action !== PasswordAction.Remove) + ) { return; } @@ -178,7 +188,7 @@ export class SessionPasswordModal extends React.Component { // Check if password match, when setting, changing or removing const valid = this.props.action !== PasswordAction.Set - ? !!await this.validatePasswordHash(oldPassword) + ? Boolean(await this.validatePasswordHash(oldPassword)) : enteredPassword === enteredPasswordConfirm; if (!valid) {