瀏覽代碼

Merge branch 'jankapunkt-tests'

Lauri Ojansivu 4 年之前
父節點
當前提交
aa59007409
共有 13 個文件被更改,包括 631 次插入5968 次删除
  1. 8 1
      .babelrc
  2. 161 0
      .github/workflows/test_suite.yml
  3. 1 0
      .gitignore
  4. 2 0
      .meteor/packages
  5. 4 0
      .meteor/versions
  6. 197 0
      client/lib/tests/Utils.tests.js
  7. 1 0
      client/lib/tests/index.js
  8. 14 7
      client/lib/utils.js
  9. 1 5959
      package-lock.json
  10. 8 1
      package.json
  11. 106 0
      server/lib/utils.tests.js
  12. 98 0
      test-wekan.sh
  13. 30 0
      tests/main.js

+ 8 - 1
.babelrc

@@ -1,5 +1,12 @@
 { 
   "presets": [ 
     "@babel/preset-stage-3" 
-  ]
+  ],
+  "env": {
+    "COVERAGE": {
+      "plugins": [
+        "istanbul"
+      ]
+    }
+  }
 }

+ 161 - 0
.github/workflows/test_suite.yml

@@ -0,0 +1,161 @@
+name: Test suite
+
+on:
+  push:
+    branches:
+      - master
+      - develop
+  pull_request:
+
+jobs:
+# the following are optional jobs and need to be configured according
+# to this project's settings:
+#
+#  lintcode:
+#    name: Javascript lint
+#    runs-on: ubuntu-latest
+#    steps:
+#    - name: checkout
+#      uses: actions/checkout@v2
+#
+#    - name: setup node
+#      uses: actions/setup-node@v1
+#      with:
+#        node-version: '12.x'
+#
+#    - name: cache dependencies
+#      uses: actions/cache@v1
+#      with:
+#        path: ~/.npm
+#        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+#        restore-keys: |
+#          ${{ runner.os }}-node-
+#
+#    - run: npm install
+#    - run: npm run lint:code
+#
+#  lintstyle:
+#    name: SCSS lint
+#    runs-on: ubuntu-latest
+#    needs: [lintcode]
+#    steps:
+#    - name: checkout
+#      uses: actions/checkout@v2
+#
+#    - name: setup node
+#      uses: actions/setup-node@v1
+#      with:
+#        node-version: '12.x'
+#
+#    - name: cache dependencies
+#      uses: actions/cache@v1
+#      with:
+#        path: ~/.npm
+#        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+#        restore-keys: |
+#          ${{ runner.os }}-node-
+#    - run: npm install
+#    - run: npm run lint:style
+#
+#  lintdocs:
+#    name: documentation lint
+#    runs-on: ubuntu-latest
+#    needs: [lintcode,lintstyle]
+#    steps:
+#    - name: checkout
+#      uses: actions/checkout@v2
+#
+#    - name: setup node
+#      uses: actions/setup-node@v1
+#      with:
+#        node-version: '12.x'
+#
+#    - name: cache dependencies
+#      uses: actions/cache@v1
+#      with:
+#        path: ~/.npm
+#        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+#        restore-keys: |
+#          ${{ runner.os }}-node-
+#
+#    - run: npm install
+#    - run: npm run lint:markdown
+
+  tests:
+    name: Meteor ${{ matrix.meteor }} tests
+    runs-on: ubuntu-latest
+    steps:
+
+      # CHECKOUTS
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      # CACHING
+      - name: Install Meteor
+        id: cache-meteor-install
+        uses: actions/cache@v2
+        with:
+          path: ~/.meteor
+          key: v1-meteor-${{ hashFiles('.meteor/versions') }}
+          restore-keys: |
+                v1-meteor-
+
+      - name: Cache NPM dependencies
+        id: cache-meteor-npm
+        uses: actions/cache@v2
+        with:
+          path: ~/.npm
+          key: v1-npm-${{ hashFiles('package-lock.json') }}
+          restore-keys: |
+                v1-npm-
+
+      - name: Cache Meteor build
+        id: cache-meteor-build
+        uses: actions/cache@v2
+        with:
+          path: |
+            .meteor/local/resolver-result-cache.json
+            .meteor/local/plugin-cache
+            .meteor/local/isopacks
+            .meteor/local/bundler-cache/scanner
+          key: v1-meteor_build_cache-${{ github.ref }}-${{ github.sha }}
+          restore-key: |
+            v1-meteor_build_cache-
+
+      - name: Setup meteor
+        uses: meteorengineer/setup-meteor@v1
+        with:
+          meteor-release: '2.2'
+
+      - name: Install NPM Dependencies
+        run: meteor npm ci
+
+      - name: Run Tests
+        run: sh ./test-wekan.sh -cv
+
+      - name: Upload coverage
+        uses: actions/upload-artifact@v2
+        with:
+          name: coverage-folder
+          path: .coverage/
+
+  coverage:
+    name: Coverage report
+    runs-on: ubuntu-latest
+    needs: [tests]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Download coverage
+        uses: actions/download-artifact@v2
+        with:
+          name: coverage-folder
+          path: .coverage/
+
+
+      - name: Coverage Report
+        uses: VeryGoodOpenSource/very_good_coverage@v1.1.1
+        with:
+          path: ".coverage/lcov.info"
+          min_coverage: 1 # TODO add tests and increase to 95!

+ 1 - 0
.gitignore

@@ -33,3 +33,4 @@ ehthumbs.db
 .meteor/local
 .devcontainer/docker-compose.extend.yml
 .devcontainer/volumes*/
+.coverage

+ 2 - 0
.meteor/packages

@@ -145,3 +145,5 @@ spacebars
 easylogic:summernote
 pascoual:pdfkit
 wekan-accounts-lockout
+lmieulet:meteor-coverage
+meteortesting:mocha

+ 4 - 0
.meteor/versions

@@ -69,6 +69,7 @@ lamhieu:meteorx@2.1.1
 lamhieu:unblock@1.0.0
 launch-screen@1.2.1
 livedata@1.0.18
+lmieulet:meteor-coverage@3.2.0
 localstorage@1.2.0
 logging@1.2.0
 matb33:collection-hooks@0.9.1
@@ -82,6 +83,9 @@ meteorhacks:collection-utils@1.2.0
 meteorhacks:picker@1.0.3
 meteorhacks:subs-manager@1.6.4
 meteorspark:util@0.2.0
+meteortesting:browser-tests@1.3.4
+meteortesting:mocha@2.0.1
+meteortesting:mocha-core@8.0.1
 minifier-css@1.5.4
 minifier-js@2.6.0
 minifiers@1.1.8-faster-rebuild.0

+ 197 - 0
client/lib/tests/Utils.tests.js

@@ -0,0 +1,197 @@
+/* eslint-env mocha */
+import sinon from 'sinon';
+import { expect } from 'chai';
+import { Random } from 'meteor/random';
+import '../utils';
+
+
+describe('Utils', function() {
+  beforeEach(function() {
+    sinon.stub(Utils, 'reload').callsFake(() => {});
+  });
+
+  afterEach(function() {
+    window.localStorage.removeItem(boardView);
+    sinon.restore();
+  });
+
+  const boardView = 'boardView';
+
+  describe(Utils.setBoardView.name, function() {
+    it('sets the board view if the user exists', function(done) {
+      const viewId = Random.id();
+      const user = {
+        setBoardView: (view) => {
+          expect(view).to.equal(viewId);
+          done();
+        },
+      };
+      sinon.stub(Meteor, 'user').callsFake(() => user);
+      Utils.setBoardView(viewId);
+
+      expect(window.localStorage.getItem(boardView)).to.equal(viewId);
+    });
+
+    it('sets a specific view if no user exists but a view is defined', function() {
+      const views = [
+        'board-view-swimlanes',
+        'board-view-lists',
+        'board-view-cal'
+      ];
+
+      sinon.stub(Meteor, 'user').callsFake(() => {});
+
+      views.forEach(viewName => {
+        Utils.setBoardView(viewName);
+        expect(window.localStorage.getItem(boardView)).to.equal(viewName);
+      });
+    });
+
+    it('sets a default view if no user and no view are given', function() {
+      sinon.stub(Meteor, 'user').callsFake(() => {});
+      Utils.setBoardView();
+      expect(window.localStorage.getItem(boardView)).to.equal('board-view-swimlanes');
+    });
+  });
+
+  describe(Utils.unsetBoardView.name, function() {
+    it('removes the boardview from localStoage', function() {
+      window.localStorage.setItem(boardView, Random.id());
+      window.localStorage.setItem('collapseSwimlane', Random.id());
+
+      Utils.unsetBoardView();
+
+      expect(window.localStorage.getItem(boardView)).to.equal(null);
+      expect(window.localStorage.getItem('collapseSwimlane')).to.equal(null);
+    });
+  });
+
+  describe(Utils.boardView.name, function() {
+    it('returns the user\'s board view if a user exists', function() {
+      const viewId = Random.id();
+      const user = {};
+      sinon.stub(Meteor, 'user').callsFake(() => user);
+      expect(Utils.boardView()).to.equal(undefined);
+
+      const boardView = Random.id();
+      user.profile = { boardView };
+
+      expect(Utils.boardView()).to.equal(boardView);
+    });
+    it('returns the current defined view', function() {
+      const views = [
+        'board-view-swimlanes',
+        'board-view-lists',
+        'board-view-cal'
+      ];
+
+      sinon.stub(Meteor, 'user').callsFake(() => {});
+
+      views.forEach(viewName => {
+        window.localStorage.setItem(boardView, viewName);
+        expect(Utils.boardView()).to.equal(viewName);
+      });
+    });
+    it('returns a default if nothing is set', function() {
+      sinon.stub(Meteor, 'user').callsFake(() => {});
+      expect(Utils.boardView()).to.equal('board-view-swimlanes');
+      expect(window.localStorage.getItem(boardView)).to.equal('board-view-swimlanes');
+    });
+  });
+
+  describe(Utils.myCardsSort.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.myCardsSortToggle.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.setMyCardsSort.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.archivedBoardIds.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.dueCardsView.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.setDueCardsView.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.goBoardId.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.goCardId.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.processUploadedAttachment.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.shrinkImage.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.capitalize.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.isMiniScreen.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.isShowDesktopDragHandles.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.isMiniScreenOrShowDesktopDragHandles.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.calculateIndexData.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.calculateIndex.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.isTouchDevice.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.calculateTouchDistance.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.enableClickOnTouch.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.manageCustomUI.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.setCustomUI.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.setMatomo.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.manageMatomo.name, function() {
+    it('has no tests yet');
+  });
+
+  describe(Utils.getTriggerActionDesc.name, function() {
+    it('has no tests yet');
+  });
+});

+ 1 - 0
client/lib/tests/index.js

@@ -0,0 +1 @@
+import './Utils.tests';

+ 14 - 7
client/lib/utils.js

@@ -1,20 +1,27 @@
 Utils = {
+  reload () {
+    // we move all window.location.reload calls into this function
+    // so we can disable it when running tests.
+    // This is because we are not allowed to override location.reload but
+    // we can override Utils.reload to prevent reload during tests.
+    window.location.reload();
+  },
   setBoardView(view) {
     currentUser = Meteor.user();
     if (currentUser) {
       Meteor.user().setBoardView(view);
     } else if (view === 'board-view-swimlanes') {
       window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true
-      location.reload();
+      Utils.reload();
     } else if (view === 'board-view-lists') {
       window.localStorage.setItem('boardView', 'board-view-lists'); //true
-      location.reload();
+      Utils.reload();
     } else if (view === 'board-view-cal') {
       window.localStorage.setItem('boardView', 'board-view-cal'); //true
-      location.reload();
+      Utils.reload();
     } else {
       window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true
-      location.reload();
+      Utils.reload();
     }
   },
 
@@ -39,7 +46,7 @@ Utils = {
       return 'board-view-cal';
     } else {
       window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true
-      location.reload();
+      Utils.reload();
       return 'board-view-swimlanes';
     }
   },
@@ -64,7 +71,7 @@ Utils = {
 
   setMyCardsSort(sort) {
     window.localStorage.setItem('myCardsSort', sort);
-    location.reload();
+    Utils.reload();
   },
 
   archivedBoardIds() {
@@ -87,7 +94,7 @@ Utils = {
 
   setDueCardsView(view) {
     window.localStorage.setItem('dueCardsView', view);
-    location.reload();
+    Utils.reload();
   },
 
   // XXX We should remove these two methods

File diff suppressed because it is too large
+ 1 - 5959
package-lock.json


+ 8 - 1
package.json

@@ -13,7 +13,11 @@
   },
   "homepage": "https://wekan.github.io",
   "devDependencies": {
-    "flatted": "^3.1.1"
+    "babel-plugin-istanbul": "^6.0.0",
+    "chai": "^4.3.4",
+    "flatted": "^3.1.1",
+    "puppeteer": "^10.0.0",
+    "sinon": "^11.1.1"
   },
   "dependencies": {
     "@babel/core": "^7.14.0",
@@ -41,5 +45,8 @@
     "papaparse": "^5.3.0",
     "qs": "^6.10.1",
     "source-map-support": "^0.5.19"
+  },
+  "meteor": {
+    "testModule": "tests/main.js"
   }
 }

+ 106 - 0
server/lib/utils.tests.js

@@ -0,0 +1,106 @@
+/* eslint-env mocha */
+import { Random } from 'meteor/random';
+import { expect } from 'chai';
+import './utils';
+
+describe('utils', function() {
+  describe(allowIsBoardAdmin.name, function() {
+    it('returns if a board has an admin', function() {
+      const userId = Random.id();
+      const board = {
+        hasAdmin: id => {
+          return id === userId;
+        }
+      };
+
+      expect(allowIsBoardAdmin(userId, board)).to.equal(true);
+      expect(allowIsBoardAdmin(Random.id(), board)).to.equal(false);
+    });
+  });
+
+  describe(allowIsBoardMember.name, function() {
+    it('returns if a board has a member', function() {
+      const userId = Random.id();
+      const board = {
+        hasMember: id => {
+          return id === userId;
+        }
+      };
+
+      expect(allowIsBoardMember(userId, board)).to.equal(true);
+      expect(allowIsBoardMember(Random.id(), board)).to.equal(false);
+    });
+  });
+
+  describe(allowIsAnyBoardMember.name, function() {
+    it('returns if any board has a member', function() {
+      const userId = Random.id();
+      const boardsExpectedTrue = [{
+        hasMember: id => {
+          return id === userId;
+        }
+      }];
+
+      expect(allowIsAnyBoardMember(userId, boardsExpectedTrue)).to.equal(true);
+      expect(allowIsAnyBoardMember(Random.id(), boardsExpectedTrue)).to.equal(false);
+
+      const boardsExpectedFalse = [{
+        hasMember: () => false
+      }];
+
+      expect(allowIsAnyBoardMember(userId, boardsExpectedFalse)).to.equal(false);
+      expect(allowIsAnyBoardMember(Random.id(), boardsExpectedFalse)).to.equal(false);
+    });
+  });
+
+  describe(allowIsBoardMemberCommentOnly.name, function() {
+    it('returns if a board has a member that is not comment-only member', function() {
+      const userId = Random.id();
+      const board = {
+        hasMember: id => {
+          return id === userId;
+        },
+        hasCommentOnly: id => {
+          return id !== userId;
+        }
+      };
+
+      expect(allowIsBoardMemberCommentOnly(userId, board)).to.equal(true);
+      expect(allowIsBoardMemberCommentOnly(Random.id(), board)).to.equal(false);
+    });
+  });
+
+  describe(allowIsBoardMemberNoComments.name, function() {
+    it('returns if a board has a member that has comment any comments', function() {
+      const userId = Random.id();
+      const board = {
+        hasMember: id => {
+          return id === userId;
+        },
+        hasNoComments: id => {
+          return id !== userId;
+        }
+      };
+
+      expect(allowIsBoardMemberNoComments(userId, board)).to.equal(true);
+      expect(allowIsBoardMemberNoComments(Random.id(), board)).to.equal(false);
+    });
+  });
+
+  describe(allowIsBoardMemberByCard.name, function() {
+    it('returns if the board for a given card has a member', function() {
+      const userId = Random.id();
+      const board = {
+        hasMember: id => {
+          return id === userId;
+        }
+      };
+      const card = {
+        board: () => board
+      };
+
+      expect(allowIsBoardMemberByCard(userId, card)).to.equal(true);
+      expect(allowIsBoardMemberByCard(Random.id(), card)).to.equal(false);
+    });
+  });
+});

+ 98 - 0
test-wekan.sh

@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+
+
+set -e
+
+# ------------------------------------------
+#
+# Variable declarations
+#
+# ------------------------------------------
+
+PROJECT_ROOT=$(pwd)
+PORT=4040
+RUN_ONCE='--once'
+VERBOSE_MODE=0
+WATCH_MODE=0
+COVERAGE=0
+
+# ------------------------------------------
+#
+# Read args from script call
+#
+# ------------------------------------------
+
+while getopts "vcw" opt; do
+  case $opt in
+    v)
+	  VERBOSE_MODE=1
+      ;;
+    c)
+    COVERAGE=1
+      ;;
+    w)
+    WATCH_MODE=1
+    RUN_ONCE=''
+      ;;
+    \?)
+      echo "Invalid option: -$OPTARG" >&2
+      exit 1
+      ;;
+  esac
+done
+
+# ------------------------------------------
+#
+# Print variables on verbose mode
+#
+# ------------------------------------------
+
+if [ "$VERBOSE_MODE" -eq "1" ];
+then
+    PROJECT_NAME=`basename "$PROJECT_ROOT"`
+	echo "=> Test $PROJECT_NAME"
+	echo "=> Path: [${PROJECT_ROOT}]"
+	echo "=> Port: [${PORT}]"
+	echo "=> Watch mode: [${WATCH_MODE}] ${RUN_ONCE}"
+	echo "=> COVERAGE: [${COVERAGE}]"
+fi
+
+
+if [ "$WATCH_MODE" -eq "0" ];
+then
+    # ---------------------------------------------------------------
+    # in cli mode we use a headless browser to include client tests
+    # and we activate the coverage reporting functionality
+    # ---------------------------------------------------------------
+    BABEL_ENV=COVERAGE \
+    TEST_BROWSER_DRIVER=puppeteer \
+    TEST_SERVER=1 \
+    TEST_CLIENT=1 \
+    COVERAGE=${COVERAGE} \
+    COVERAGE_OUT_HTML=1 \
+    COVERAGE_OUT_LCOVONLY=1 \
+    COVERAGE_OUT_TEXT_SUMMARY=1 \
+    COVERAGE_OUT_JSON_SUMMARY=1 \
+    COVERAGE_APP_FOLDER=$PWD/ \
+    COVERAGE_VERBOSE_MODE=${VERBOSE_MODE} \
+            meteor test \
+                --exclude-archs=web.browser.legacy,web.cordova \
+                --driver-package=meteortesting:mocha \
+                --settings=settings.json \
+                --port=${PORT} \
+                --once
+    cat ./.coverage/summary.txt
+    else
+    # ---------------------------------------------------------------
+    # in watch mode we neither use a browser driver, nor coverage
+    # se we speed up the test reload in the development phase
+    # ---------------------------------------------------------------
+    TEST_BROWSER_DRIVER=puppeteer \
+    TEST_SERVER=1 \
+    TEST_CLIENT=1 \
+        meteor test \
+            --exclude-archs=web.browser.legacy,web.cordova \
+            --driver-package=meteortesting:mocha \
+            --settings=settings.json \
+            --port=${PORT}
+fi

+ 30 - 0
tests/main.js

@@ -0,0 +1,30 @@
+/* eslint-env mocha */
+
+// This is the main test file from which all tests can be imported top-down,
+// creating a directed sequence for tests that sums up to our test-suite.
+//
+// You propably want to start with low-level code and follow up to higher-level
+// code, like for example:
+//
+// infrastructure
+// utils / helpers
+// contexts
+// api
+// components
+// ui
+
+// If you want to run tests on both, server AND client, simply import them as
+// they are. However, if you want to restict tests to server-only or client-only
+// you need to wrap them inside a new describe-block
+
+if (Meteor.isServer) {
+  describe('server', function() {
+    import '../server/lib/utils.tests';
+  });
+}
+
+if (Meteor.isClient) {
+  describe('lib', function() {
+    import '../client/lib/tests';
+  });
+}

Some files were not shown because too many files changed in this diff