diff --git a/.babelrc b/.babelrc index 9224ca318..eafcfa0d6 100644 --- a/.babelrc +++ b/.babelrc @@ -7,11 +7,6 @@ "transform-react-display-name" ], "env": { - "test": { - "plugins": [ - "rewire" - ] - }, "development": { "plugins": [ "typecheck" diff --git a/.eslintrc b/.eslintrc index a8d2f2fb3..a277bf4a8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -50,6 +50,8 @@ "webpackIsomorphicTools": true, ga: true, Raven: true, - mixpanel: true + mixpanel: true, + "expect": true, + "browser": true } } diff --git a/nightwatch.js b/nightwatch.js deleted file mode 100644 index eddf548a7..000000000 --- a/nightwatch.js +++ /dev/null @@ -1,11 +0,0 @@ -require('dotenv').load(); -require('app-module-path').addPath(__dirname); -require('app-module-path').addPath('./src/scripts'); - -require("babel-register"); - -global.__CLIENT__ = false; -global.__SERVER__ = true; - - -require('nightwatch/bin/runner.js'); diff --git a/nightwatch.json b/nightwatch.json deleted file mode 100644 index 09b59063f..000000000 --- a/nightwatch.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "src_folders": ["tests/functional/specs"], - "output_folder": "tests/functional/output", - "custom_commands_path": "tests/functional/commands", - "custom_assertions_path": "tests/functional/assertions", - "page_objects_path": "tests/functional/pages", - "globals_path": "tests/functional/globals.js", - "selenium" : { - "start_process" : true, - "server_path" : "node_modules/selenium-server/lib/runner/selenium-server-standalone-2.52.0.jar", - "log_path" : "", - "host" : "127.0.0.1", - "port" : 5555, - "cli_args" : { - "webdriver.chrome.driver" : "node_modules/chromedriver/lib/chromedriver/chromedriver" - } - }, - "test_settings" : { - "default" : { - "exclude": ["./pages", "./commands"], - "filter": "*.spec.js", - "launch_url" : "http://localhost", - "selenium_port" : 5555, - "selenium_host" : "localhost", - "silent": true, - "screenshots" : { - "enabled" : true, - "path" : "tests/functional/screenshots" - }, - "desiredCapabilities": { - "browserName": "chrome", - "javascriptEnabled": true, - "acceptSslCerts": true - } - }, - "production" : { - "exclude": ["./pages", "./commands"], - "filter": "*.spec.js", - "launch_url" : "http://localhost", - "selenium_port" : 5555, - "selenium_host" : "localhost", - "silent": true, - "screenshots" : { - "enabled" : true, - "path" : "tests/functional/screenshots" - }, - "desiredCapabilities": { - "browserName" : "phantomjs", - "javascriptEnabled" : true, - "acceptSslCerts" : true, - "phantomjs.binary.path" : "/usr/local/phantomjs/bin/phantomjs" - } - }, - "mobile": { - "exclude": ["./pages", "./commands"], - "filter": "**/*.spec.js", - "launch_url" : "http://localhost", - "selenium_port" : 5555, - "selenium_host" : "localhost", - "silent": true, - "screenshots" : { - "enabled" : true, - "path" : "tests/functional/screenshots" - }, - "desiredCapabilities": { - "browserName": "chrome", - "javascriptEnabled": true, - "acceptSslCerts": true, - "chromeOptions": { - "args": [ - "--user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_2 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A4449d Safari/9537.53", - "--window-size=320,568" - ] - } - } - } - } -} diff --git a/package.json b/package.json index 46366c901..120cdff41 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,18 @@ "version": "1.0.0", "private": false, "scripts": { - "test": "./node_modules/karma/bin/karma start", + "test": "npm run test:dev:unit", "test:ci:unit": "./node_modules/karma/bin/karma start --browsers PhantomJS --single-run; npm run test:ci:lint", - "test:ci:functional": "node ./nightwatch.js -c ./nightwatch.json -e production", + "test:ci:functional": "BROWSER=phantomjs bash tests/functional/test.sh start-ci", + "posttest:ci:functional": "bash tests/functional/test.sh stop", + "test:ci:unit": "karma start --browsers PhantomJS --single-run; npm run test:ci:lint", + "test:dev:unit": "karma start", "test:ci:lint": "eslint ./src/**/*.js", "test:dev:unit": "./node_modules/karma/bin/karma start", - "test:dev:functional": "node ./nightwatch.js -c ./nightwatch.json", + "test:dev:functional": "BROWSER=chrome bash tests/functional/test.sh start", + "posttest:dev:functional": "bash tests/functional/test.sh stop", "test:dev:lint": "eslint ./src/scripts/**/*.js", + "test:stylelint": "stylelint './src/**/*.scss' --config webpack/.stylelintrc", "dev": "node webpack/dev-server.js & PORT=8000 node start.js", "start": "NODE_PATH=\"./src\" node ./start", "build": "node ./node_modules/webpack/bin/webpack.js --config webpack/prod.config.js", @@ -134,11 +139,12 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.6.0", "mocha": "^2.2.5", - "nightwatch": "^0.8.6", "nodemon": "^1.7.1", "path": "^0.11.14", + "phantomjs": "^1.9.20", "phantomjs-polyfill": "0.0.1", "piping": "^0.3.0", + "pre-commit": "^1.1.3", "react-addons-test-utils": "^0.14.7", "react-transform-catch-errors": "^1.0.0", "react-transform-hmr": "^1.0.1", @@ -149,11 +155,14 @@ "selenium-server": "^2.48.2", "sinon": "^1.15.3", "sinon-chai": "^2.8.0", + "stylelint-webpack-plugin": "^0.2.0", + "wdio-mocha-framework": "^0.3.7", + "wdio-spec-reporter": "0.0.3", + "webdriverio": "4.2.1", "webpack-dev-server": "^1.6.5" }, "pre-commit": [ - "lint", - "validate", - "test" + "test:dev:lint", + "test:stylelint" ] } diff --git a/src/components/Ayah/index.js b/src/components/Ayah/index.js index bbb7aad26..193522f36 100644 --- a/src/components/Ayah/index.js +++ b/src/components/Ayah/index.js @@ -20,6 +20,7 @@ export default class Ayah extends Component { ayah: PropTypes.object.isRequired, match: PropTypes.array, isSearch: PropTypes.bool, + isPlaying: PropTypes.bool, tooltip: PropTypes.string, currentWord: PropTypes.any, // gets passed in an integer, null by default onWordClick: PropTypes.func, @@ -46,11 +47,14 @@ export default class Ayah extends Component { } handlePlay(ayah) { - const {stop, setAyah, start} = this.props.actions; + const {isPlaying, actions} = this.props; + const {pause, setAyah, play} = actions; - stop(); + if (isPlaying) { + pause(); + } setAyah(ayah); - start(); + play(); } renderTranslations() { diff --git a/src/components/Ayah/style.scss b/src/components/Ayah/style.scss index f76320937..11c5b7089 100644 --- a/src/components/Ayah/style.scss +++ b/src/components/Ayah/style.scss @@ -91,7 +91,7 @@ b{ float: right; - border-color:: transparent; + border-color: transparent; border-width: 0px 0px 1px 0px; border-style: solid; &.active { diff --git a/src/config.js b/src/config.js index 1e21ea8c4..eec53bbc6 100644 --- a/src/config.js +++ b/src/config.js @@ -79,7 +79,7 @@ module.exports = Object.assign({ ], style: [ {cssText: `@font-face {font-family: 'bismillah'; - src: url('http://quran-1f14.kxcdn.com/fonts/ttf/bismillah.ttf') format('truetype')} + src: url('//quran-1f14.kxcdn.com/fonts/ttf/bismillah.ttf') format('truetype')} .bismillah{font-family: 'bismillah'; font-size: 36px !important; color: #000; padding: 25px 0px;}` // eslint-disable-line max-len } ] diff --git a/src/containers/Surah/Title/index.js b/src/containers/Surah/Title/index.js index cef83c594..6b95a2472 100644 --- a/src/containers/Surah/Title/index.js +++ b/src/containers/Surah/Title/index.js @@ -13,7 +13,6 @@ const zeroPad = (num, places) => { const Title = ({ surah }) => { const title = require(`../../../../static/images/titles/${zeroPad(surah.id, 3)}.svg`); // eslint-disable-line global-require,max-len - return (
@@ -23,7 +22,7 @@ const Title = ({ surah }) => { alt="Ornament left" /> { - surah.id >= 1 && + surah.id > 1 && {
{ - surah.id <= 114 && + surah.id < 114 && ', () => { + + it('should render', () => { + wrapper = renderComponent(surah); + expect(wrapper).to.be.ok; + }); + + it('should not show previous surah if on the first surah', () => { + wrapper = renderComponent(surah); + + const previous = wrapper.find('.previous-chapter').length; + const next = wrapper.find('.next-chapter').length; + + expect(previous).to.equal(0); + expect(next).to.equal(1); + }); + + it('should not show next surah if on the last surah', () => { + surah.id = 114; + wrapper = renderComponent(surah); + + const previous = wrapper.find('.previous-chapter').length; + const next = wrapper.find('.next-chapter').length; + + expect(previous).to.equal(1); + expect(next).to.equal(0); + }); + + it('should show both next and previous if surah is neither last or first', () => { + surah.id = 14; + wrapper = renderComponent(surah); + + const previous = wrapper.find('.previous-chapter').length; + const next = wrapper.find('.next-chapter').length; + + expect(previous).to.equal(1); + expect(next).to.equal(1); + }); +}); + +function renderComponent(data) { + return shallow(); +} diff --git a/src/containers/Surah/index.js b/src/containers/Surah/index.js index 1a39dd665..ce95999d2 100644 --- a/src/containers/Surah/index.js +++ b/src/containers/Surah/index.js @@ -64,7 +64,8 @@ class Surah extends Component { options: PropTypes.object.isRequired, params: PropTypes.object.isRequired, ayahs: PropTypes.object.isRequired, - isStarted: PropTypes.bool + isStarted: PropTypes.bool, + isPlaying: PropTypes.bool }; state = { @@ -280,7 +281,7 @@ class Surah extends Component { } renderAyahs() { - const { ayahs, actions, options } = this.props; // eslint-disable-line no-shadow + const { ayahs, actions, options, isPlaying } = this.props; // eslint-disable-line no-shadow return Object.values(ayahs).map(ayah => ( <Ayah @@ -288,6 +289,7 @@ class Surah extends Component { tooltip={options.tooltip} onWordClick={actions.audio.setCurrentWord} actions={actions.audio} + isPlaying={isPlaying} key={`${ayah.surahId}-${ayah.ayahNum}-ayah`} /> )); @@ -467,12 +469,12 @@ function mapStateToProps(state, ownProps) { const surah: Object = state.surahs.entities[surahId]; const ayahs: Object = state.ayahs.entities[surahId]; const ayahIds = new Set(Object.keys(ayahs).map(key => parseInt(key.split(':')[1], 10))); - return { surah, ayahs, ayahIds, isStarted: state.audioplayer.isStarted, + isPlaying: state.audioplayer.isPlaying, currentWord: state.ayahs.currentWord, isEndOfSurah: ayahIds.size === surah.ayat, surahs: state.surahs.entities, diff --git a/src/styles/fonts/_fonts.scss b/src/styles/fonts/_fonts.scss index b00d7cadb..c1ef8d838 100644 --- a/src/styles/fonts/_fonts.scss +++ b/src/styles/fonts/_fonts.scss @@ -1,4 +1,3 @@ -//@import url(http://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600|Montserrat); @font-face{ font-family: 'Montserrat'; src: url('./static/fonts/montserrat/Montserrat-Regular.otf'); diff --git a/src/styles/main.scss b/src/styles/main.scss index f83a5e3ff..f52f30248 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -1,6 +1,4 @@ -//@import 'bootstrap'; $icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/"; -//@import '~bootstrap-sass/assets/stylesheets/bootstrap'; @import 'mixins/center'; @import 'components/SurahNav'; @@ -44,38 +42,6 @@ a:hover{ display: none; } } - .right-side{ - padding-top: 190px; - width: 83.33333333%; - float: right; - display: block; - position: relative; - background-color: #fff; - transition: width 0.5s; - min-height: 100vh; - z-index: 9999; - - .navbar-brand{ - display: none; - } - - @media (max-width: $screen-sm-max) { - width: 100%; - padding-top: 150px; - - .navbar-brand{ - display: block; - } - } - - &.active{ - width: 99%; - - .navbar{ - width: 99; // This is almost like `noop` to get the css working - } - } - } } .form-control:focus{ diff --git a/src/styles/partials/_navbar-brand.scss b/src/styles/partials/_navbar-brand.scss index b73100571..33214c595 100644 --- a/src/styles/partials/_navbar-brand.scss +++ b/src/styles/partials/_navbar-brand.scss @@ -18,7 +18,6 @@ } img { display: inline !important; - height: 100%; &.logo{ height: 80%; vertical-align: top; diff --git a/src/styles/partials/_search-input.scss b/src/styles/partials/_search-input.scss index b81eee249..494456dd9 100644 --- a/src/styles/partials/_search-input.scss +++ b/src/styles/partials/_search-input.scss @@ -27,7 +27,7 @@ padding: 15px; height: 100%; text-align: center; - border: initial; + border: 0; background: transparent; right: 0px; } diff --git a/tests/functional/assertions/default.js b/tests/functional/assertions/default.js deleted file mode 100644 index f053ebf79..000000000 --- a/tests/functional/assertions/default.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/tests/functional/commands/default.js b/tests/functional/commands/default.js deleted file mode 100644 index f053ebf79..000000000 --- a/tests/functional/commands/default.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/tests/functional/pageobjects/BasePage.js b/tests/functional/pageobjects/BasePage.js new file mode 100644 index 000000000..788b2c0d0 --- /dev/null +++ b/tests/functional/pageobjects/BasePage.js @@ -0,0 +1,16 @@ +export default class BasePage { + constructor(selectors) { + this.selectors = Object.assign(selectors, { + SURAH_PAGE_SURAH_NAME: '.surah-body .navbar-text.surah-name' + }); + } + + getSurahName() { + return browser.getText(this.selectors.SURAH_PAGE_SURAH_NAME); + } + + goHome() { + browser.url('http://quran.com'); + } + +}; diff --git a/tests/functional/pageobjects/HomePage.js b/tests/functional/pageobjects/HomePage.js new file mode 100644 index 000000000..f0d282122 --- /dev/null +++ b/tests/functional/pageobjects/HomePage.js @@ -0,0 +1,33 @@ +import BasePage from './BasePage'; +import selectors from './selectors'; + +export default class HomePage extends BasePage { + constructor() { + super(selectors); + this.selectors = selectors; + } + + getLandingText() { + const landingText = browser.getText(this.selectors.LANDING_TEXT); + return landingText; + } + + getNumberOfSurahs() { + const surahs = browser.elements(this.selectors.SURAH_LIST); + return surahs.value.length; + } + + searchForSurahAndGoToSurahPage(searchQuery) { + browser.setValue(this.selectors.SEARCH_FORM, searchQuery); + browser.waitForVisible(this.selectors.SEARCH_RESULT_LIST); + browser.click(this.selectors.SEARCH_RESULT_LIST); + return this.getSurahName(); + } + + clickOntheSurahByNumber(number) { + const surahs = browser.elements('.row .col-xs-7 span'); + const surahID = surahs.value[number].ELEMENT; + browser.elementIdClick(surahID); + return browser.getUrl(); + } +} diff --git a/tests/functional/pageobjects/selectors.js b/tests/functional/pageobjects/selectors.js new file mode 100644 index 000000000..1a8d66fb8 --- /dev/null +++ b/tests/functional/pageobjects/selectors.js @@ -0,0 +1,10 @@ +export default { + LANDING_TEXT: 'h4.title', + NEXT: 'a[data-direction="next"]', + PREVIOUS: 'a[data-direction="previous"]', + SURAH_LIST: '.row .col-md-4 li', + SEARCH_FORM: '.searchinput input', + SEARCH_RESULT_LIST: '.searchinput a', + SURAH_PAGE: '.surah-body .surah-title', + SURAH_NAME: '.navbar-text.surah-name' +}; diff --git a/tests/functional/pages/index.js b/tests/functional/pages/index.js deleted file mode 100644 index 498efda57..000000000 --- a/tests/functional/pages/index.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - url: 'http://localhost:8000', - elements: { - surahList: { - selector: '.surah-list', - }, - searchInput: { - selector: '.searchinput' - }, - indexHeader: { - selector: '.index-header' - }, - firstSurah: { - selector: '.surah-list a[href="/1"]' - }, - lastSurah: { - selector: '.surah-list a[href="/114"]' - } - } -}; diff --git a/tests/functional/specs/HomePageTests.js b/tests/functional/specs/HomePageTests.js new file mode 100644 index 000000000..e860bde0b --- /dev/null +++ b/tests/functional/specs/HomePageTests.js @@ -0,0 +1,27 @@ +import Homepage from '../pageobjects/HomePage'; +const homePage = new Homepage(browser); + +describe('Home Page', () => { + + it('Should Have English Heading', () => { + homePage.goHome(); + const text = homePage.getLandingText(); + expect(text).to.equal('THE NOBLE QUR\'AN'); + }); + + it('Should have 114 surahs', () => { + const numberSurahs = homePage.getNumberOfSurahs(); + expect(numberSurahs).to.equal(114); + }); + + it('Should search for surah, click the first result item and land on the surah page', () => { + const surahName = homePage.searchForSurahAndGoToSurahPage('ya-sin'); + expect(surahName).to.equal('YA-SIN (YA SIN) - سورة يس'); + }); + + it('Should click a surah from the list of surahs and land on the surah page', () => { + homePage.goHome(); + const url = homePage.clickOntheSurahByNumber(1); + expect(url).to.contain('/1'); + }); +}); diff --git a/tests/functional/specs/Index.spec.js b/tests/functional/specs/Index.spec.js deleted file mode 100644 index 29bec56a8..000000000 --- a/tests/functional/specs/Index.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - 'can view index page': function(client) { - var index = client.page.index(); - - index.navigate() - .waitForElementVisible('body', 2000) - .assert.title("The Noble Qur'an - القرآن الكريم"); - - index.expect.element('@indexHeader').to.be.present; - index.expect.element('@surahList').to.be.present; - index.expect.element('@indexHeader').to.be.present; - - client.end(); - }, - - 'can view surah list': function(client) { - var index = client.page.index(); - - index.navigate(); - - index.expect.element('@surahList').to.be.present; - index.expect.element('@firstSurah').to.be.present; - index.expect.element('@lastSurah').to.be.present; - - index.expect.element('@firstSurah').text.to.contain('The Opener'); - index.expect.element('@firstSurah').text.to.contain('Al-Fatihah'); - index.expect.element('@lastSurah').text.to.contain('The Mankind'); - index.expect.element('@lastSurah').text.to.contain('An-Nas'); - - client.end(); - } -} diff --git a/tests/functional/test.sh b/tests/functional/test.sh new file mode 100755 index 000000000..16b55a933 --- /dev/null +++ b/tests/functional/test.sh @@ -0,0 +1,26 @@ + +#!/bin/bash +if [ "$1" == 'stop' ]; then + echo "Stopping selenium server..." + pid=`ps -eo pid,args | grep selenium-server-standalone | grep -v grep | cut -c1-6` + echo $pid + kill $pid +elif [ "$1" == 'start' ]; then + echo "Starting selenium server..." + PATH="./node_modules/phantomjs/bin:$PATH" java -jar ./node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.0.jar &> /dev/null & + sleep 2 + BROWSERNAME=$BROWSER ./node_modules/.bin/wdio ./tests/functional/wdio.conf.js +elif [ "$1" == 'start-ci' ]; then + echo "[CI] Starting selenium server..." + + if [ ! -f selenium-server-standalone-2.53.1.jar ]; then + echo "selenium jar File not found!" + wget https://selenium-release.storage.googleapis.com/2.53/selenium-server-standalone-2.53.1.jar + fi + + PATH="./node_modules/phantomjs/bin:$PATH" java -jar selenium-server-standalone-2.53.1.jar &> /dev/null & + sleep 5 + BROWSERNAME=$BROWSER ./node_modules/.bin/wdio ./tests/functional/wdio.conf.js +else + echo "Nothing to do!" +fi diff --git a/tests/functional/wdio.conf.js b/tests/functional/wdio.conf.js new file mode 100644 index 000000000..b02f41928 --- /dev/null +++ b/tests/functional/wdio.conf.js @@ -0,0 +1,44 @@ +const browser = process.env.BROWSERNAME || 'phantomjs'; + +exports.config = { + + maxInstances: 4, + sync: true, + specs: [ + './tests/functional/specs/**/*.js' + ], + exclude: [ + ], + + capabilities: [ + { + browserName: browser + } + ], + + logLevel: 'error', + coloredLogs: true, + screenshotPath: './errorShots/', + + waitforTimeout: 10000, + framework: 'mocha', + reporters: ['dot', 'spec'], + onPrepare: () => { + }, + before: () => { + + const chai = require('chai'); // eslint-disable-line global-require + global.chai = chai; + global.expect = chai.expect; + + process.on('unhandledRejection', (e) => { + console.error(e); + if (e.stack) { + console.error(e.stack); + } + }); + + require('babel-core/register'); // eslint-disable-line global-require + require('babel-polyfill'); // eslint-disable-line global-require + } +}; diff --git a/webpack/.stylelintrc b/webpack/.stylelintrc new file mode 100644 index 000000000..78c5d9e14 --- /dev/null +++ b/webpack/.stylelintrc @@ -0,0 +1,7 @@ +{ + "rules": { + "unit-no-unknown": true, + "no-duplicate-selectors": true, + "block-no-empty": true + } +}