Pārlūkot izejas kodu

Merge pull request #668 from threema-ch/uitests

UI Tests
Danilo Bargen 6 gadi atpakaļ
vecāks
revīzija
8c59313c64

+ 32 - 10
.circleci/config.yml

@@ -5,25 +5,45 @@ references:
     - checkout
     - restore_cache:
         keys:
-          - v1-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ arch }}-{{ checksum "package.json" }}
+          - v2-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ arch }}-{{ checksum "package.json" }}
     - run: npm install
     - run: npm run build
-    - run: npm run build:tests
-    - run: npm test
+    - run: npm run ${BUILDTARGET}
+    - run: npm run ${TESTTARGET}
     - save_cache:
-        key: v1-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ arch }}-{{ checksum "package.json" }}
+        key: v2-dependencies-{{ .Environment.CIRCLE_JOB }}-{{ arch }}-{{ checksum "package.json" }}
         paths:
           - node_modules
 
 jobs:
-  test-node8:
+  unittest-node8:
     docker:
       - image: circleci/node:8-browsers
     steps: *test-steps
-  test-node9:
+    environment:
+      BUILDTARGET: build:unittests
+      TESTTARGET: test:unittests
+  unittest-node10:
     docker:
-      - image: circleci/node:9-browsers
+      - image: circleci/node:10-browsers
     steps: *test-steps
+    environment:
+      BUILDTARGET: build:unittests
+      TESTTARGET: test:unittests
+  uitest-firefox:
+    docker:
+      - image: circleci/node:10-browsers
+    steps: *test-steps
+    environment:
+      BUILDTARGET: build:uitests
+      TESTTARGET: test:uitests firefox:headless
+  uitest-chrome:
+    docker:
+      - image: circleci/node:10-browsers
+    steps: *test-steps
+    environment:
+      BUILDTARGET: build:uitests
+      TESTTARGET: test:uitests firefox:headless
   lint:
     docker:
       - image: circleci/node:8-browsers
@@ -31,7 +51,7 @@ jobs:
       - checkout
       - restore_cache:
           keys:
-            - v1-dependencies-test-node8-{{ arch }}-{{ checksum "package.json" }}
+            - v2-dependencies-test-node8-{{ arch }}-{{ checksum "package.json" }}
       - run: npm install
       - run: npm run lint
 
@@ -39,6 +59,8 @@ workflows:
   version: 2
   build:
     jobs:
-      - test-node8
-      - test-node9
+      - unittest-node8
+      - unittest-node10
+      - uitest-firefox
+      - uitest-chrome
       - lint

+ 10 - 1
README.md

@@ -64,11 +64,20 @@ Web on a server, please follow the instructions at
 
 ## Testing
 
-To run tests:
+To run unit tests:
 
     npm run build && npm run build:tests
     firefox tests/testsuite.html
 
+To run UI tests:
+
+    npm run test:unittests <browser>
+
+For example:
+
+    npm run test:unittests firefox
+    npm run test:unittests chromium:headless
+
 To run linting checks:
 
     npm run lint

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 733 - 21
package-lock.json


+ 8 - 2
package.json

@@ -7,11 +7,16 @@
     "build:js": "browserify -p tsify src/app.ts -t [ babelify --presets [ es2015 ] --extensions .ts ] -p [ browserify-header --file header.js ] -o dist/app.js",
     "build:css": "node-sass -o public/css/ --output-style compressed src/sass/",
     "build:css:watch": "node-sass -w -r --source-map true --source-map-embed true -o public/css/ --output-style compressed src/sass/",
-    "build:tests": "browserify -p tsify tests/ts/main.ts -t [ babelify --presets [ es2015 ] --extensions .ts ] -o dist/ts-tests.js",
+    "build:tests": "echo -e 'NOTE: Use either \"npm build:unittests\" or \"npm build:uitests\"\n' && exit 1",
+    "build:unittests": "browserify -p tsify tests/ts/main.ts -t [ babelify --presets [ es2015 ] --extensions .ts ] -o dist/ts-tests.js",
+    "build:uitests": "npm run build:css && browserify -p tsify tests/ui/main.ts -t [ babelify --presets [ es2015 ] --extensions .ts ] -o dist/ui-tests.js",
     "dist": "npm run build && echo \"\" && node dist/build-package.js",
     "serve:live": "echo 'NOTE: serve:live command has been renamed to devserver'",
     "devserver": "npm run build:css && concurrently --kill-others --names \"css,server\" -p name \"npm run build:css:watch\" \"budo src/app.ts:dist/app.js -d public -d . -d src --live -- -d -p tsify -t [ babelify --presets [ es2015 ] --extensions .ts ]\"",
-    "test": "npm run build:tests && karma start --single-run --log-level=debug --colors",
+    "testserver": "budo -d public -d . -d src -p 7777",
+    "test": "echo -e 'NOTE: Use either \"npm test:unittests\" or \"npm test:uitests\"\n' && exit 1",
+    "test:unittests": "npm run build:unittests && karma start --single-run --log-level=debug --colors",
+    "test:uitests": "npm run build:uitests && bash tests/ui/run.sh",
     "lint": "tslint -c tslint.json --project tsconfig.json --exclude \"**/src/config.ts\"",
     "clean": "rm -rf js/ build/ dist/app*"
   },
@@ -77,6 +82,7 @@
     "karma-chrome-launcher": "^2.2.0",
     "karma-firefox-launcher": "^1.1.0",
     "karma-jasmine": "^1.1.2",
+    "testcafe": "^0.23.2",
     "tslint": "~5.10"
   }
 }

+ 8 - 5
src/directives/compose_area.ts

@@ -60,6 +60,9 @@ export default [
                 // Callback that is called when uploading files
                 onUploading: '=',
                 maxTextLength: '=',
+
+                // Optional emoji PNG path prefix
+                emojiImagePath: '@?',
             },
             link(scope: any, element) {
                 // Logging
@@ -463,8 +466,9 @@ export default [
                         const text = ev.clipboardData.getData('text/plain');
 
                         // Look up some filter functions
+                        // tslint:disable-next-line:max-line-length
+                        const emojify = $filter('emojify') as (a: string, b?: boolean, c?: boolean, d?: string) => string;
                         const escapeHtml = $filter('escapeHtml') as (a: string) => string;
-                        const emojify = $filter('emojify') as (a: string, b?: boolean) => string;
                         const mentionify = $filter('mentionify') as (a: string) => string;
                         const nlToBr = $filter('nlToBr') as (a: string, b?: boolean) => string;
 
@@ -472,7 +476,7 @@ export default [
                         const escaped = escapeHtml(text);
 
                         // Apply filters (emojify, convert newline, etc)
-                        const formatted = nlToBr(mentionify(emojify(escaped, true)), true);
+                        const formatted = nlToBr(mentionify(emojify(escaped, true, false, scope.emojiImagePath)), true);
 
                         // Insert resulting HTML
                         document.execCommand('insertHTML', false, formatted);
@@ -537,11 +541,11 @@ export default [
                 // Emoji is chosen
                 function onEmojiChosen(ev: MouseEvent): void {
                     ev.stopPropagation();
-                    insertEmoji (this.textContent);
+                    insertEmoji(this.textContent);
                 }
 
                 function insertEmoji(emoji, posFrom = null, posTo = null): void {
-                    const emojiElement = ($filter('emojify') as any)(emoji, true, true) as string;
+                    const emojiElement = ($filter('emojify') as any)(emoji, true, true, scope.emojiImagePath) as string;
                     insertHTMLElement(emoji, emojiElement, posFrom, posTo);
                 }
 
@@ -860,7 +864,6 @@ export default [
                 <div class="emoji-keyboard">
                     <ng-include src="'partials/emoji-picker.html'" include-replace></ng-include>
                 </div>
-
             `,
         };
     },

+ 2 - 2
src/filters.ts

@@ -101,11 +101,11 @@ angular.module('3ema.filters', [])
  * Set the `imgTag` parameter to `true` to use inline PNGs instead of sprites.
  */
 .filter('emojify', function() {
-    return function(text, imgTag = false, greedyMatch = false) {
+    return function(text, imgTag = false, greedyMatch = false, imagePath = 'img/e1/') {
         if (text !== null) {
             emojione.sprites = imgTag !== true;
             emojione.emojiSize = '32';
-            emojione.imagePathPNG = 'img/e1/';
+            emojione.imagePathPNG = imagePath;
             emojione.greedyMatch = greedyMatch;
             return emojione.unicodeToImage(text);
         } else {

+ 1 - 1
src/sass/sections/_compose_area.scss

@@ -1,4 +1,4 @@
-#conversation #conversation-footer compose-area {
+compose-area {
 
     > div:first-child {
         display: flex;

+ 38 - 0
tests/ui/compose_area.html

@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html ng-strict-di>
+<head>
+    <meta charset="utf-8">
+
+    <!-- Third party -->
+    <script src="../../public/libs/emojione/emojione.min.js?v=[[VERSION]]"></script>
+
+    <!-- Third party stylesheets -->
+    <link rel="stylesheet" href="../../node_modules/angular/angular-csp.css?v=[[VERSION]]">
+    <link rel="stylesheet" href="../../node_modules/angular-material/angular-material.min.css?v=[[VERSION]]">
+    <link rel="stylesheet" href="../../public/libs/emojione/emojione-sprite-32.min.css?v=[[VERSION]]">
+    <link rel="stylesheet" href="../../public/fonts/roboto.css?v=[[VERSION]]" type="text/css">
+    <link rel="stylesheet" href="../../public/fonts/material.css?v=[[VERSION]]" type="text/css">
+
+    <!-- Own stylesheets -->
+    <link rel="stylesheet" href="../../public/css/app.css?v=[[VERSION]]">
+
+    <!-- Scripts -->
+    <script src="../../dist/ui-tests.js"></script>
+    <script>window.uiTests.initComposeArea();</script>
+</head>
+<body ng-app="uitest">
+    <div ng-controller="ComposeAreaController as ctrl">
+        <compose-area
+            submit="ctrl.submit"
+            initial-data="ctrl.initialData"
+            start-typing="ctrl.startTyping"
+            on-typing="ctrl.onTyping"
+            stop-typing="ctrl.stopTyping"
+            on-key-down="ctrl.onComposeKeyDown"
+            on-uploading="ctrl.onUploading"
+            max-text-length="ctrl.maxTextLength"
+            emoji-image-path="../../public/img/e1/">
+        </compose-area>
+    <div>
+</body>
+</html>

+ 97 - 0
tests/ui/compose_area.ts

@@ -0,0 +1,97 @@
+/**
+ * Copyright © 2016-2018 Threema GmbH (https://threema.ch/).
+ *
+ * This file is part of Threema Web.
+ */
+
+// tslint:disable:no-console
+// tslint:disable:no-reference
+
+// Import AngularJS
+import * as angular from 'angular';
+import 'angular-material';
+import 'angular-translate';
+
+// Import types
+import '../../src/threema.d';
+
+// Import dependencies
+import config from '../../src/config';
+import '../../src/directives';
+import '../../src/filters';
+import '../../src/services';
+
+export function init() {
+    console.info('Init UI Test: compose_area');
+
+    const app = angular.module('uitest', [
+        '3ema.directives',
+        '3ema.filters',
+        '3ema.services',
+        'ngMaterial',
+        'pascalprecht.translate',
+    ]);
+    app.constant('CONFIG', config);
+    app.controller('ComposeAreaController', ComposeAreaController);
+
+    // Provide mock translations
+    app.config(function($translateProvider) {
+        $translateProvider.translations('en', {
+            messenger: {
+                COMPOSE_MESSAGE: 'compose_message',
+                COMPOSE_MESSAGE_DRAGOVER: 'compose_message_dragover',
+            },
+        });
+        $translateProvider.preferredLanguage('en');
+    });
+
+    // Fix paths
+    app.config(['$httpProvider', ($httpProvider: ng.IHttpProvider) => {
+        $httpProvider.interceptors.push([() => {
+            return {
+                request: (conf) => {
+                    if (conf.url.indexOf('partials/') !== -1 ||
+                        conf.url.indexOf('directives/') !== -1 ||
+                        conf.url.indexOf('components/') !== -1) {
+                        conf.url = '../../src/' + conf.url;
+                    }
+                    return conf;
+                },
+            };
+        }]);
+    }]);
+
+}
+
+class ComposeAreaController {
+    public static $inject = [];
+
+    public initialData: threema.InitialConversationData;
+
+    constructor() {
+        this.initialData = {
+            draft: '',
+            initialText: '',
+        };
+    }
+
+    public startTyping() {
+        // ignore
+    }
+
+    public onTyping() {
+        // ignore
+    }
+
+    public stopTyping() {
+        // ignore
+    }
+
+    public onComposeKeyDown() {
+        return true;
+    }
+
+    public submit(msgtype, data) {
+        // ignore
+    }
+}

+ 11 - 0
tests/ui/main.ts

@@ -0,0 +1,11 @@
+/**
+ * Copyright © 2016-2018 Threema GmbH (https://threema.ch/).
+ *
+ * This file is part of Threema Web.
+ */
+import {init as initComposeArea} from './compose_area';
+
+// Expose global functions
+(window as any).uiTests = {
+    initComposeArea: initComposeArea,
+};

+ 16 - 0
tests/ui/run.sh

@@ -0,0 +1,16 @@
+#!/bin/bash
+if [ $# -lt 1 ]; then
+    echo "Error: Please specify a browser target argument"
+    exit 1
+fi
+
+bin_path=node_modules/.bin
+browser=$1
+shift
+
+$bin_path/concurrently \
+    --kill-others \
+    -s first \
+    --names \"server,test\" \
+    "npm run testserver" \
+    "$bin_path/testcafe $browser tests/ui/run.ts $*"

+ 61 - 0
tests/ui/run.ts

@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016-2018 Threema GmbH (https://threema.ch/).
+ *
+ * This file is part of Threema Web.
+ */
+// tslint:disable:no-unused-expression
+
+import { Selector, ClientFunction } from 'testcafe';
+
+// NOTE: These tests use test cafe.
+// See http://devexpress.github.io/testcafe/documentation/getting-started/ for
+// documentation on how to write UI tests.
+
+fixture `Compose Area`
+    .page `http://localhost:7777/tests/ui/compose_area.html`;
+
+test('Show and hide emoji selector', async (t) => {
+    const keyboard = await Selector('.emoji-keyboard');
+
+    // Not visible initially
+    await t.expect(keyboard.visible).eql(false);
+
+    // Show
+    await t.click('.emoji-trigger');
+
+    // Visible
+    await t.expect(keyboard.visible).eql(true);
+
+    // Hide
+    await t.click('.emoji-trigger');
+
+    // Visible
+    await t.expect(keyboard.visible).eql(false);
+});
+
+test('Insert emoji', async (t) => {
+    // Show emoji keyboard
+    await t.click('.emoji-trigger');
+
+    // Insert woman zombie emoji
+    await t.click('.e1._1f9df-2640');
+
+    // Insert beer
+    await t.click('.e1-food').click('.e1._1f37b');
+
+    // Ensure both have been inserted
+    const getChildNodeCount = await ClientFunction(() => {
+        return document.querySelector('div.compose').childNodes.length;
+    });
+    await t.expect(await getChildNodeCount()).eql(2);
+
+    const firstEmoji = await Selector('div.compose img').nth(0)();
+    await t.expect(firstEmoji.tagName).eql('img');
+    await t.expect(firstEmoji.attributes.title).eql(':woman_zombie:');
+    await t.expect(firstEmoji.classNames).eql(['e1']);
+
+    const secondEmoji = await Selector('div.compose img').nth(1)();
+    await t.expect(secondEmoji.tagName).eql('img');
+    await t.expect(secondEmoji.attributes.title).eql(':beers:');
+    await t.expect(secondEmoji.classNames).eql(['e1']);
+});

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels