diff --git a/packages/desktop-gui/cypress/integration/login_spec.js b/packages/desktop-gui/cypress/integration/login_spec.js index 91ec33e4dc4c..5a9ec1865502 100644 --- a/packages/desktop-gui/cypress/integration/login_spec.js +++ b/packages/desktop-gui/cypress/integration/login_spec.js @@ -96,9 +96,7 @@ describe('Login', function () { }) it('displays username in UI', function () { - cy.get('nav a').should(function ($a) { - expect($a).to.contain(this.user.name) - }) + cy.get('.user-dropdown .dropdown-chosen').should('contain', this.user.name) }) it('displays username in success dialog', () => { @@ -117,14 +115,14 @@ describe('Login', function () { context('log out', function () { it('displays login button on logout', () => { - cy.get('nav a').contains('Jane').click() + cy.get('.user-dropdown .dropdown-chosen').contains('Jane').click() cy.contains('Log Out').click() cy.get('.nav').contains('Log In') }) it('calls log:out', function () { - cy.get('nav a').contains('Jane').click() + cy.get('.user-dropdown .dropdown-chosen').contains('Jane').click() cy.contains('Log Out').click().then(function () { expect(this.ipc.logOut).to.be.called @@ -132,7 +130,7 @@ describe('Login', function () { }) it('has login button enabled when returning to login after logout', function () { - cy.get('nav a').contains('Jane').click() + cy.get('.user-dropdown .dropdown-chosen').contains('Jane').click() cy.contains('Log Out').click() cy.contains('Log In').click() diff --git a/packages/desktop-gui/cypress/integration/nav_spec.js b/packages/desktop-gui/cypress/integration/nav_spec.js index 7c24d44083d3..5ad4a2f7b26f 100644 --- a/packages/desktop-gui/cypress/integration/nav_spec.js +++ b/packages/desktop-gui/cypress/integration/nav_spec.js @@ -61,7 +61,7 @@ describe('Navigation', function () { }) it('displays user name', () => { - cy.get('nav a').should(function ($a) { + cy.get('.user-dropdown .dropdown-chosen').should(function ($a) { expect($a).to.contain(this.user.name) }) }) @@ -121,7 +121,7 @@ describe('Navigation', function () { }) it('displays email instead of name', () => { - cy.get('nav a').should(function ($a) { + cy.get('.user-dropdown .dropdown-chosen').should(function ($a) { expect($a).to.contain(this.user.email) }) }) diff --git a/packages/desktop-gui/src/app/nav.jsx b/packages/desktop-gui/src/app/nav.jsx index 6005db6a0a5a..defd83a909d1 100644 --- a/packages/desktop-gui/src/app/nav.jsx +++ b/packages/desktop-gui/src/app/nav.jsx @@ -1,5 +1,6 @@ import { observer } from 'mobx-react' import React, { Component } from 'react' +import { Dropdown } from '@packages/ui-components' import appStore from '../lib/app-store' import authApi from '../auth/auth-api' @@ -9,8 +10,6 @@ import ipc from '../lib/ipc' import { gravatarUrl } from '../lib/utils' import { Link, routes } from '../lib/routing' -import Dropdown from '../dropdown/dropdown' - @observer export default class Nav extends Component { render () { @@ -87,7 +86,7 @@ export default class Nav extends Component { return ( .open { - > a, > a:hover, > a:focus { + .dropdown-chosen { + &:hover, &:focus { background-color: #111; color: #fff; } @@ -140,6 +140,13 @@ font-size: 12px; } + .dropdown-menu > li { + line-height: 30px; + font-size: 14px; + padding: 5px 15px; + } + + .dropdown-chosen, > li > div, > li > a, > li > span > a { @@ -150,6 +157,7 @@ display: inline-block; } + .dropdown-chosen, > li > a, > li > span > a { &:hover, @@ -163,16 +171,15 @@ .browsers-list { margin: 5px; - border: 1px solid #c7c7c7; - border-radius: 4px; li { padding: 9px 15px; white-space: nowrap; } - &>a.dropdown-chosen { + .dropdown-chosen { border-radius: 4px; + border: 1px solid #c7c7c7; line-height: 28px !important; background-color: #f6f6f6; @@ -180,6 +187,11 @@ position: relative; top: -1px; } + + &.disabled, + &:active { + background-color: #dedede; + } } .fa-check-circle, .fa-sync-alt { diff --git a/packages/desktop-gui/src/main.scss b/packages/desktop-gui/src/main.scss index 821afa6b83ab..edd50e3a0bf7 100644 --- a/packages/desktop-gui/src/main.scss +++ b/packages/desktop-gui/src/main.scss @@ -3,3 +3,4 @@ @import 'styles/vendor'; @import 'styles/components/*'; @import '!(styles)*/**/*'; +@import '../../ui-components/src/dropdown'; diff --git a/packages/desktop-gui/src/project-nav/browsers.jsx b/packages/desktop-gui/src/project-nav/browsers.jsx index 1c6e689ea1d6..dbc76d94eabd 100644 --- a/packages/desktop-gui/src/project-nav/browsers.jsx +++ b/packages/desktop-gui/src/project-nav/browsers.jsx @@ -1,11 +1,9 @@ import React, { Component } from 'react' import { observer } from 'mobx-react' import Tooltip from '@cypress/react-tooltip' -import { BrowserIcon } from '@packages/ui-components' +import { BrowserIcon, Dropdown } from '@packages/ui-components' -import Dropdown from '../dropdown/dropdown' import MarkdownRenderer from '../lib/markdown-renderer' - import projectsApi from '../projects/projects-api' @observer diff --git a/packages/runner/src/dropdown/dropdown.jsx b/packages/runner/src/dropdown/dropdown.jsx deleted file mode 100644 index b331137cf7e8..000000000000 --- a/packages/runner/src/dropdown/dropdown.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import cs from 'classnames' -import _ from 'lodash' -import React, { Component } from 'react' - -export default class Dropdown extends Component { - constructor (props) { - super(props) - - this.state = { open: false } - } - - render () { - return ( -
- - {this._caret()} - {this._items()} -
- ) - } - - _caret () { - if (!this.props.others.length) return null - - return ( - - ) - } - - _toggleOpen = () => { - this.setState({ open: !this.state.open }) - } - - _items () { - if (!this.props.others.length) return null - - return ( - - ) - } -} diff --git a/packages/runner/src/dropdown/dropdown.scss b/packages/runner/src/dropdown/dropdown.scss deleted file mode 100644 index 1a9ee06cb4e9..000000000000 --- a/packages/runner/src/dropdown/dropdown.scss +++ /dev/null @@ -1,78 +0,0 @@ -.runner { - .dropdown { - display: inline-block; - position: relative; - vertical-align: middle; - - button { - border-radius: 6px 0 0 6px; - } - - .dropdown-toggle { - border-radius: 0 6px 6px 0; - - &:focus { - outline: 0; - } - } - - .browser-icon { - margin-right: 5px; - vertical-align: middle; - position: relative; - top: -1px; - } - } - - .dropdown-caret { - border-top: 4px dashed; - border-right: 4px solid transparent; - border-left: 4px solid transparent; - border-width: 5px 5px 0; - display: inline-block; - height: 0; - vertical-align: middle; - width: 0; - } - - .dropdown-menu { - background-clip: padding-box; - background-color: #fff; - border: 1px solid rgba(0, 0, 0, 0.15); - border-radius: 2px; - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); - font-size: 14px; - display: none; - float: left; - left: 0; - margin: 2px 0 0; - min-width: 160px; - padding: 5px 0; - position: absolute; - top: 100%; - list-style: none; - text-align: left; - z-index: 1000; - - li { - color: #333; - cursor: pointer; - display: block; - font-weight: normal; - line-height: 1.4; - padding: 3px 12px; - white-space: nowrap; - - &:hover, - &:focus, - &:active { - background-color: #f5f5f5; - color: #262626; - } - } - } - - .open .dropdown-menu { - display: block; - } -} diff --git a/packages/runner/src/errors/errors.scss b/packages/runner/src/errors/errors.scss index 65053e65187b..790e1ca785c1 100644 --- a/packages/runner/src/errors/errors.scss +++ b/packages/runner/src/errors/errors.scss @@ -47,11 +47,29 @@ top: -1px; &.fa-globe { - top: 0; + top: 0; } } } + &.automation-failure { + .browser-icon { + margin-right: 5px; + vertical-align: middle; + position: relative; + top: -1px; + } + + .dropdown-toggle { + margin-left: 5px; + } + + .dropdown-menu li { + padding: 8px 12px; + white-space: nowrap; + } + } + .automation-disconnected button { font-size: 18px; line-height: 1.3; diff --git a/packages/runner/src/errors/no-automation.jsx b/packages/runner/src/errors/no-automation.jsx index c9e0e27e2577..fc2e555880e2 100644 --- a/packages/runner/src/errors/no-automation.jsx +++ b/packages/runner/src/errors/no-automation.jsx @@ -1,8 +1,6 @@ import _ from 'lodash' import React from 'react' -import { BrowserIcon } from '@packages/ui-components' - -import Dropdown from '../dropdown/dropdown' +import { BrowserIcon, Dropdown } from '@packages/ui-components' const displayName = (name) => _.capitalize(name) @@ -42,6 +40,7 @@ const browserPicker = (browsers, onLaunchBrowser) => { others={otherBrowsers} onSelect={onLaunchBrowser} renderItem={browser} + keyProperty='key' /> ) diff --git a/packages/runner/src/main.scss b/packages/runner/src/main.scss index 00703a2fff22..272f7fafffba 100644 --- a/packages/runner/src/main.scss +++ b/packages/runner/src/main.scss @@ -6,4 +6,5 @@ @import 'lib/shared'; @import '!(lib)*/**/!(selector-playground.scss)'; +@import '../../ui-components/src/dropdown'; @import '../../reporter/src/main-runner'; diff --git a/packages/ui-components/cypress/integration/dropdown_spec.jsx b/packages/ui-components/cypress/integration/dropdown_spec.jsx new file mode 100644 index 000000000000..f24629c22c2a --- /dev/null +++ b/packages/ui-components/cypress/integration/dropdown_spec.jsx @@ -0,0 +1,92 @@ +import React from 'react' +import { render } from 'react-dom' +import { Dropdown } from '../../' + +describe('', () => { + let defaultProps + + beforeEach(() => { + defaultProps = { + chosen: { name: 'First' }, + others: [{ name: 'Second' }, { name: 'Third' }], + keyProperty: 'name', + renderItem: ({ name }) => name, + onSelect: () => {}, + } + + cy.visit('dist/index.html') + + cy.viewport(400, 600) + }) + + it('displays chosen option and hides others', () => { + cy.render(render, ) + + cy.contains('First').should('be.visible') + cy.contains('Second').should('not.be.visible') + cy.contains('Third').should('not.be.visible') + }) + + it('shows others after clicking chosen option', () => { + cy.render(render, ) + + cy.contains('First').click() + cy.contains('Second').should('be.visible') + cy.contains('Third').should('be.visible') + }) + + it('calls onSelect after clicking option', () => { + const onSelect = cy.stub() + + cy.render(render, ) + + cy.contains('First').click() + cy.contains('Second').click().then(() => { + expect(onSelect).to.be.calledWith({ name: 'Second' }) + }) + }) + + it('applies className to container', () => { + cy.render(render, ) + + cy.get('.dropdown').should('have.class', 'custom-class') + }) + + it('renders item as specified by renderItem prop', () => { + cy.render(render, {name}} />) + + cy.contains('First').children().should('match', 'span') + cy.contains('Second').should('match', 'span') + cy.contains('Third').should('match', 'span') + }) + + it('renders caret if there are items', () => { + cy.render(render, ) + + cy.get('.dropdown-caret') + }) + + it('does not render caret if there are no items', () => { + cy.render(render, ) + + cy.get('.dropdown-caret').should('not.exist') + }) + + it('disables if disabled specified and does not render options or caret', () => { + cy.render(render, ) + + cy.contains('First').should('have.class', 'disabled').click({ force: true }) + cy.get('.dropdown li').should('not.exist') + cy.get('.dropdown-caret').should('not.exist') + }) + + it('closes dropdown when clicking outside of it', () => { + cy.document().then((doc) => { + cy.render(render, ) + }) + + cy.contains('First').click() + cy.get('body').click() + cy.contains('Second').should('not.be.visible') + }) +}) diff --git a/packages/ui-components/cypress/plugins/index.js b/packages/ui-components/cypress/plugins/index.js index 81660d2f1d6c..6147a75b7edb 100644 --- a/packages/ui-components/cypress/plugins/index.js +++ b/packages/ui-components/cypress/plugins/index.js @@ -12,6 +12,10 @@ const webpackOptions = { use: { loader: require.resolve('babel-loader'), options: { + plugins: [ + [require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }], + [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }], + ], presets: [ require.resolve('@babel/preset-env'), require.resolve('@babel/preset-react'), diff --git a/packages/ui-components/cypress/support/test-entry.scss b/packages/ui-components/cypress/support/test-entry.scss index af0454153164..26f4aa4b26e1 100644 --- a/packages/ui-components/cypress/support/test-entry.scss +++ b/packages/ui-components/cypress/support/test-entry.scss @@ -2,3 +2,5 @@ @import "../../node_modules/@fortawesome/fontawesome-free/scss/solid.scss"; @import "../../node_modules/@fortawesome/fontawesome-free/scss/brands.scss"; @import "../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss"; + +@import '../../src/dropdown'; diff --git a/packages/ui-components/package.json b/packages/ui-components/package.json index 99f7203116cb..4077efa11da7 100644 --- a/packages/ui-components/package.json +++ b/packages/ui-components/package.json @@ -14,6 +14,8 @@ "watch": "npm run build -- --watch --progress" }, "devDependencies": { + "@babel/plugin-proposal-class-properties": "7.8.3", + "@babel/plugin-proposal-decorators": "7.8.3", "@babel/preset-env": "7.8.3", "@babel/preset-react": "7.8.3", "@cypress/eslint-plugin-dev": "5.0.0", diff --git a/packages/desktop-gui/src/dropdown/dropdown.jsx b/packages/ui-components/src/dropdown.jsx similarity index 85% rename from packages/desktop-gui/src/dropdown/dropdown.jsx rename to packages/ui-components/src/dropdown.jsx index 4e611393c80a..aa7562c44fe3 100644 --- a/packages/desktop-gui/src/dropdown/dropdown.jsx +++ b/packages/ui-components/src/dropdown.jsx @@ -7,11 +7,11 @@ import { findDOMNode } from 'react-dom' class Dropdown extends Component { static defaultProps = { className: '', + document, } static propTypes = { className: PropTypes.string, - icon: PropTypes.string, chosen: PropTypes.object.isRequired, others: PropTypes.arrayOf(PropTypes.object).isRequired, onSelect: PropTypes.func.isRequired, @@ -30,11 +30,11 @@ class Dropdown extends Component { } } - document.body.addEventListener('click', this.outsideClickHandler) + this.props.document.body.addEventListener('click', this.outsideClickHandler) } componentWillUnmount () { - document.body.removeEventListener('click', this.outsideClickHandler) + this.props.document.body.removeEventListener('click', this.outsideClickHandler) } render () { @@ -49,9 +49,9 @@ class Dropdown extends Component { _button () { if (this.props.others.length) { return ( - + ) } @@ -64,10 +64,10 @@ class Dropdown extends Component { _buttonContent () { return ( - + <> {this.props.renderItem(this.props.chosen)}{' '} {this._caret()} - + ) } @@ -75,7 +75,7 @@ class Dropdown extends Component { if (!this.props.others.length || this.props.disabled) return null return ( - + Toggle Dropdown diff --git a/packages/desktop-gui/src/dropdown/dropdown.scss b/packages/ui-components/src/dropdown.scss similarity index 91% rename from packages/desktop-gui/src/dropdown/dropdown.scss rename to packages/ui-components/src/dropdown.scss index 6a73c11280cf..48357b544062 100644 --- a/packages/desktop-gui/src/dropdown/dropdown.scss +++ b/packages/ui-components/src/dropdown.scss @@ -2,8 +2,14 @@ display: inline-block; position: relative; vertical-align: middle; +} + +.dropdown-chosen { + background: none; + border: none; + outline: none; - > a.disabled { + &.disabled { pointer-events: none; opacity: 0.65; } @@ -44,8 +50,6 @@ } li { - cursor: pointer; - &:last-of-type { border-bottom: 0; } diff --git a/packages/ui-components/src/index.jsx b/packages/ui-components/src/index.jsx index b697fa866d1b..6913e2a0594e 100644 --- a/packages/ui-components/src/index.jsx +++ b/packages/ui-components/src/index.jsx @@ -1 +1,3 @@ export { default as BrowserIcon } from './browser-icon' + +export { default as Dropdown } from './dropdown' diff --git a/yarn.lock b/yarn.lock index 1a892a51fb31..96a22b0b80e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -227,6 +227,18 @@ "@babel/helper-replace-supers" "^7.7.0" "@babel/helper-split-export-declaration" "^7.7.0" +"@babel/helper-create-class-features-plugin@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.3.tgz#5b94be88c255f140fd2c10dd151e7f98f4bff397" + integrity sha512-qmp4pD7zeTxsv0JNecSBsEmG1ei2MqwJq4YQcK3ZWm/0t07QstWfvuV/vm3Qt5xNMFETn2SZqpMx2MQzbtq+KA== + dependencies: + "@babel/helper-function-name" "^7.8.3" + "@babel/helper-member-expression-to-functions" "^7.8.3" + "@babel/helper-optimise-call-expression" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-replace-supers" "^7.8.3" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/helper-create-regexp-features-plugin@^7.7.0": version "7.7.2" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.7.2.tgz#6f20443778c8fce2af2ff4206284afc0ced65db6" @@ -621,6 +633,14 @@ "@babel/helper-create-class-features-plugin" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-proposal-class-properties@7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz#5e06654af5cd04b608915aada9b2a6788004464e" + integrity sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-proposal-class-properties@^7.1.0": version "7.7.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.7.0.tgz#ac54e728ecf81d90e8f4d2a9c05a890457107917" @@ -638,6 +658,15 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-decorators" "^7.2.0" +"@babel/plugin-proposal-decorators@7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.8.3.tgz#2156860ab65c5abf068c3f67042184041066543e" + integrity sha512-e3RvdvS4qPJVTe288DlXjwKflpfy1hr0j5dz5WpIYYeP7vQZg2WfAEIp8k5/Lwis/m5REXEteIz6rrcDtXXG7w== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-decorators" "^7.8.3" + "@babel/plugin-proposal-dynamic-import@^7.5.0", "@babel/plugin-proposal-dynamic-import@^7.7.0": version "7.7.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.7.0.tgz#dc02a8bad8d653fb59daf085516fa416edd2aa7f" @@ -771,6 +800,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-decorators@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.8.3.tgz#8d2c15a9f1af624b0025f961682a9d53d3001bda" + integrity sha512-8Hg4dNNT9/LcA1zQlfwuKR8BUc/if7Q7NkTam9sGTcJphLwpf2g4S42uhspQrIrR+dpzE0dtTqBVFoHl8GtnnQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-dynamic-import@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612" @@ -22099,11 +22135,10 @@ socket.io-circular-parser@cypress-io/socket.io-circular-parser#4d3076af68ea8192c version "3.1.2-patch1" resolved "https://codeload.github.com/cypress-io/socket.io-circular-parser/tar.gz/4d3076af68ea8192c2e53f9d185eaa166359b4c5" dependencies: - circular-json "^0.4.0" + circular-json "0.5.9" component-emitter "1.2.1" - debug "~2.6.4" + debug "~4.1.0" has-binary2 cypress-io/has-binary#8580a33df21e8b36a43f57872a82c60829636a92 - isarray "2.0.1" socket.io-client@2.3.0: version "2.3.0"