diff --git a/.travis.yml b/.travis.yml index 93f9da4ec..737e80ed1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -dist: xenial +dist: bionic language: node_js cache: directories: @@ -6,7 +6,11 @@ cache: addons: chrome: stable node_js: - - "10" + - "14" install: make node_modules -before_script: make serve_bg -script: make check +services: + - xvfb +before_script: + - make serve_bg + - export DISPLAY=:99.0 +script: make check ARGS=--single-run diff --git a/Makefile b/Makefile index da9e94407..ac86a4f0e 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ BABEL ?= node_modules/.bin/babel BOOTSTRAP = ./node_modules/ BUILDDIR = ./docs +KARMA ?= ./node_modules/.bin/karma CHROMIUM ?= ./node_modules/.bin/run-headless-chromium CLEANCSS ?= ./node_modules/clean-css-cli/bin/cleancss --skip-rebase ESLINT ?= ./node_modules/.bin/eslint @@ -197,7 +198,11 @@ eslint: node_modules .PHONY: check check: eslint dev - LOG_CR_VERBOSITY=INFO $(CHROMIUM) --disable-gpu --no-sandbox http://localhost:$(HTTPSERVE_PORT)/tests/index.html + $(KARMA) start karma.conf.js $(ARGS) + +.PHONY: test +test: + $(KARMA) start karma.conf.js $(ARGS) ######################################################################## ## Documentation diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 000000000..cc3fffd03 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,107 @@ +/* global module */ +const path = require('path'); + +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + frameworks: ['jasmine'], + files: [ + { pattern: 'dist/*.js.map', included: false }, + { pattern: 'dist/*.css.map', included: false }, + { pattern: "dist/emojis.js", served: true }, + "dist/converse.js", + "dist/converse.css", + { pattern: "dist/webfonts/**/*.*", included: false }, + { pattern: "node_modules/sinon/pkg/sinon.js", type: 'module' }, + { pattern: "tests/console-reporter.js", type: 'module' }, + { pattern: "tests/mock.js", type: 'module' }, + + { pattern: "spec/spoilers.js", type: 'module' }, + { pattern: "spec/roomslist.js", type: 'module' }, + { pattern: "spec/utils.js", type: 'module' }, + { pattern: "spec/converse.js", type: 'module' }, + { pattern: "spec/bookmarks.js", type: 'module' }, + { pattern: "spec/headline.js", type: 'module' }, + { pattern: "spec/disco.js", type: 'module' }, + { pattern: "spec/protocol.js", type: 'module' }, + { pattern: "spec/presence.js", type: 'module' }, + { pattern: "spec/eventemitter.js", type: 'module' }, + { pattern: "spec/smacks.js", type: 'module' }, + { pattern: "spec/ping.js", type: 'module' }, + { pattern: "spec/push.js", type: 'module' }, + { pattern: "spec/xmppstatus.js", type: 'module' }, + { pattern: "spec/mam.js", type: 'module' }, + { pattern: "spec/omemo.js", type: 'module' }, + { pattern: "spec/controlbox.js", type: 'module' }, + { pattern: "spec/roster.js", type: 'module' }, + { pattern: "spec/chatbox.js", type: 'module' }, + { pattern: "spec/user-details-modal.js", type: 'module' }, + { pattern: "spec/messages.js", type: 'module' }, + { pattern: "spec/muc_messages.js", type: 'module' }, + { pattern: "spec/retractions.js", type: 'module' }, + { pattern: "spec/muc.js", type: 'module' }, + { pattern: "spec/modtools.js", type: 'module' }, + { pattern: "spec/room_registration.js", type: 'module' }, + { pattern: "spec/autocomplete.js", type: 'module' }, + { pattern: "spec/minchats.js", type: 'module' }, + { pattern: "spec/notification.js", type: 'module' }, + { pattern: "spec/login.js", type: 'module' }, + { pattern: "spec/register.js", type: 'module' }, + { pattern: "spec/hats.js", type: 'module' }, + { pattern: "spec/http-file-upload.js", type: 'module' }, + { pattern: "spec/emojis.js", type: 'module' }, + { pattern: "spec/xss.js", type: 'module' }, + + ], + exclude: ['**/*.sw?'], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: {}, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress', 'kjhtml'], + + webpack: { + mode: 'development', + devtool: 'inline-source-map', + module: { + rules: [{ + test: /\.js$/, + exclude: /(node_modules|test)/ + }] + }, + output: { + path: path.resolve('test'), + filename: '[name].out.js', + chunkFilename: '[id].[chunkHash].js' + } + }, + + + port: 9876, + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome'], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity + }) +} diff --git a/package-lock.json b/package-lock.json index 30d72cd49..4bbe23726 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2244,8 +2244,7 @@ "dependencies": { "filesize": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", - "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==" + "resolved": false }, "fs-extra": { "version": "8.1.0", @@ -2279,8 +2278,7 @@ }, "jed": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz", - "integrity": "sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ=" + "resolved": false }, "jsonfile": { "version": "5.0.0", @@ -2301,21 +2299,18 @@ }, "localforage": { "version": "1.7.3", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz", - "integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==", + "resolved": false, "requires": { "lie": "3.1.1" } }, "lodash": { "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "resolved": false }, "pluggable.js": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pluggable.js/-/pluggable.js-2.0.1.tgz", - "integrity": "sha512-SBt6v6Tbp20Jf8hU0cpcc/+HBHGMY8/Q+yA6Ih0tBQE8tfdZ6U4PRG0iNvUUjLx/hVyOP53n0UfGBymlfaaXCg==", + "resolved": false, "requires": { "lodash": "^4.17.11" } @@ -2329,13 +2324,11 @@ }, "strophe.js": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.3.4.tgz", - "integrity": "sha512-jSLDG8jolhAwGOSgiJ7DTMSYK3wVoEJHKtpVRyEacQZ6CWA6z2WRPJpcFMjsIweq5aP9/XIvKUQqHBu/ZhvESA==" + "resolved": false }, "twemoji": { "version": "12.1.5", - "resolved": "https://registry.npmjs.org/twemoji/-/twemoji-12.1.5.tgz", - "integrity": "sha512-B0PBVy5xomwb1M/WZxf/IqPZfnoIYy1skXnlHjMwLwTNfZ9ljh8VgWQktAPcJXu8080WoEh6YwQGPVhDVqvrVQ==", + "resolved": false, "requires": { "fs-extra": "^8.0.1", "jsonfile": "^5.0.0", @@ -4231,6 +4224,12 @@ "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, "agent-base": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", @@ -4446,6 +4445,12 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", + "dev": true + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -4523,6 +4528,15 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, "async-each": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", @@ -4909,6 +4923,12 @@ "object.assign": "^4.1.0" } }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, "bail": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", @@ -4982,12 +5002,24 @@ } } }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", "dev": true }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "dev": true + }, "basic-auth": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", @@ -5015,6 +5047,15 @@ "integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==", "dev": true }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "requires": { + "callsite": "1.0.0" + } + }, "big.js": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", @@ -5037,6 +5078,12 @@ "file-uri-to-path": "1.0.0" } }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", + "dev": true + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -5484,6 +5531,12 @@ "caller-callsite": "^2.0.0" } }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5862,12 +5915,24 @@ } } }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", "dev": true }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, "compressible": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", @@ -5937,6 +6002,29 @@ "proto-list": "~1.2.1" } }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, "connect-history-api-fallback": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", @@ -6640,6 +6728,12 @@ "array-find-index": "^1.0.1" } }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, "cyclist": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", @@ -6670,6 +6764,12 @@ "integrity": "sha512-hpA5C/YrPjucXypHPPc0oJ1l9Hf6wWbiOL7Ik42cxnsUOhWiCB/fylKbKqqJalW9FgkNQCw16YO8uW9Hs0Iy1A==", "dev": true }, + "date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "dev": true + }, "dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", @@ -6957,6 +7057,12 @@ "wrappy": "1" } }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -7026,6 +7132,18 @@ "utila": "~0.4" } }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, "dom-serializer": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", @@ -7266,6 +7384,84 @@ "once": "^1.4.0" } }, + "engine.io": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", + "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.0", + "ws": "~3.3.1" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + } + } + }, + "engine.io-client": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.1", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~3.3.1", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", + "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", + "dev": true, + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, "enhanced-resolve": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", @@ -7289,6 +7485,12 @@ } } }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, "entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", @@ -9920,6 +10122,29 @@ "ansi-regex": "^2.0.0" } }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "dev": true, + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -10521,6 +10746,12 @@ "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", "dev": true }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -10968,6 +11199,12 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, + "isbinaryfile": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.6.tgz", + "integrity": "sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg==", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -10992,11 +11229,37 @@ "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", "dev": true }, - "jasmine-core": { - "version": "2.99.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", - "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=", - "dev": true + "jasmine": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz", + "integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==", + "dev": true, + "requires": { + "glob": "^7.1.4", + "jasmine-core": "~3.5.0" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "jasmine-core": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", + "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "dev": true + } + } }, "jed": { "version": "1.1.1", @@ -11198,6 +11461,400 @@ "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", "dev": true }, + "karma": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/karma/-/karma-5.0.2.tgz", + "integrity": "sha512-RpUuCuGJfN3WnjYPGIH+VBF8023Lfm3TQH6D1kcNL+FxtEPc2UUz/nVjbVAGXH4Pm+Q7FVOAQjdAeFUpXpQ3IA==", + "dev": true, + "requires": { + "body-parser": "^1.16.1", + "braces": "^3.0.2", + "chokidar": "^3.0.0", + "colors": "^1.1.0", + "connect": "^3.6.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.0", + "flatted": "^2.0.0", + "glob": "^7.1.1", + "graceful-fs": "^4.1.2", + "http-proxy": "^1.13.0", + "isbinaryfile": "^4.0.2", + "lodash": "^4.17.14", + "log4js": "^4.0.0", + "mime": "^2.3.1", + "minimatch": "^3.0.2", + "qjobs": "^1.1.4", + "range-parser": "^1.2.0", + "rimraf": "^2.6.0", + "socket.io": "2.1.1", + "source-map": "^0.6.1", + "tmp": "0.0.33", + "ua-parser-js": "0.7.21", + "yargs": "^15.3.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz", + "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.3.0" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "readdirp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", + "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.7" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "karma-chrome-launcher": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz", + "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==", + "dev": true, + "requires": { + "which": "^1.2.1" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "karma-cli": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-2.0.0.tgz", + "integrity": "sha512-1Kb28UILg1ZsfqQmeELbPzuEb5C6GZJfVIk0qOr8LNYQuYWmAaqP16WpbpKEjhejDrDYyYOwwJXSZO6u7q5Pvw==", + "dev": true, + "requires": { + "resolve": "^1.3.3" + } + }, + "karma-jasmine": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-3.1.1.tgz", + "integrity": "sha512-pxBmv5K7IkBRLsFSTOpgiK/HzicQT3mfFF+oHAC7nxMfYKhaYFgxOa5qjnHW4sL5rUnmdkSajoudOnnOdPyW4Q==", + "dev": true, + "requires": { + "jasmine-core": "^3.5.0" + }, + "dependencies": { + "jasmine-core": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", + "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "dev": true + } + } + }, + "karma-jasmine-html-reporter": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.5.3.tgz", + "integrity": "sha512-ci0VrjuCaFj+9d1tYlTE3KIPUCp0rz874zWWU3JgCMqGIyw5ke+BXWFPOAGAqUdCJcrMwneyvp1zFXA74MiPUA==", + "dev": true + }, + "karma-webpack": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-4.0.2.tgz", + "integrity": "sha512-970/okAsdUOmiMOCY8sb17A2I8neS25Ad9uhyK3GHgmRSIFJbDcNEFE8dqqUhNe9OHiCC9k3DMrSmtd/0ymP1A==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "loader-utils": "^1.1.0", + "neo-async": "^2.6.1", + "schema-utils": "^1.0.0", + "source-map": "^0.7.3", + "webpack-dev-middleware": "^3.7.0" + }, + "dependencies": { + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -11485,6 +12142,36 @@ "integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=", "dev": true }, + "log4js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz", + "integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==", + "dev": true, + "requires": { + "date-format": "^2.0.0", + "debug": "^4.1.1", + "flatted": "^2.0.0", + "rfdc": "^1.1.4", + "streamroller": "^1.0.6" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "loglevel": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", @@ -16744,6 +17431,12 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -17193,6 +17886,24 @@ "protocols": "^1.4.0" } }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -17337,6 +18048,12 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -18294,6 +19011,12 @@ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", "dev": true }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true + }, "qs": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.1.tgz", @@ -18921,6 +19644,12 @@ "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", "dev": true }, + "rfdc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", + "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==", + "dev": true + }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", @@ -19770,6 +20499,67 @@ "kind-of": "^3.2.0" } }, + "socket.io": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", + "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", + "dev": true, + "requires": { + "debug": "~3.1.0", + "engine.io": "~3.2.0", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.1.1", + "socket.io-parser": "~3.2.0" + } + }, + "socket.io-adapter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", + "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==", + "dev": true + }, + "socket.io-client": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", + "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", + "dev": true, + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "engine.io-client": "~3.2.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.2.0", + "to-array": "0.1.4" + } + }, + "socket.io-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + } + } + }, "sockjs": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", @@ -20194,6 +20984,47 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", "dev": true }, + "streamroller": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz", + "integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==", + "dev": true, + "requires": { + "async": "^2.6.2", + "date-format": "^2.0.0", + "debug": "^3.2.6", + "fs-extra": "^7.0.1", + "lodash": "^4.17.14" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", @@ -20561,6 +21392,12 @@ "os-tmpdir": "~1.0.2" } }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -20776,6 +21613,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "ua-parser-js": { + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", + "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==", + "dev": true + }, "uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", @@ -20815,6 +21658,12 @@ "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", "dev": true }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "dev": true + }, "umask": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz", @@ -21310,6 +22159,12 @@ } } }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, "watchpack": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", @@ -22032,6 +22887,12 @@ "integrity": "sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==", "dev": true }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", + "dev": true + }, "xss": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.6.tgz", @@ -22162,6 +23023,12 @@ "dev": true } } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true } } } diff --git a/package.json b/package.json index 80901ae8b..dfe529776 100644 --- a/package.json +++ b/package.json @@ -84,8 +84,14 @@ "http-server": "^0.12.1", "imports-loader": "^0.8.0", "install": "^0.13.0", - "jasmine-core": "2.99.1", + "jasmine": "^3.5.0", "jsdoc": "^3.6.4", + "karma": "^5.0.2", + "karma-chrome-launcher": "^3.1.0", + "karma-cli": "^2.0.0", + "karma-jasmine": "^3.1.1", + "karma-jasmine-html-reporter": "^1.5.3", + "karma-webpack": "^4.0.2", "lerna": "^3.20.2", "lit-html": "^1.2.1", "lodash-template-webpack-loader": "jcbrand/lodash-template-webpack-loader", diff --git a/spec/autocomplete.js b/spec/autocomplete.js index 3cb7e9ebc..c2bd2bfef 100644 --- a/spec/autocomplete.js +++ b/spec/autocomplete.js @@ -1,217 +1,215 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const $pres = converse.env.$pres; - const $msg = converse.env.$msg; - const Strophe = converse.env.Strophe; - const u = converse.env.utils; +/*global mock */ - describe("The nickname autocomplete feature", function () { +const $pres = converse.env.$pres; +const $msg = converse.env.$msg; +const Strophe = converse.env.Strophe; +const u = converse.env.utils; - it("shows all autocompletion options when the user presses @", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { +describe("The nickname autocomplete feature", function () { - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); + it("shows all autocompletion options when the user presses @", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - // Nicknames from presences - ['dick', 'harry'].forEach((nick) => { - _converse.connection._dataRecv(test_utils.createRequest( - $pres({ - 'to': 'tom@montague.lit/resource', - 'from': `lounge@montague.lit/${nick}` - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': `${nick}@montague.lit/resource`, - 'role': 'participant' - }))); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'tom'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + + // Nicknames from presences + ['dick', 'harry'].forEach((nick) => { + _converse.connection._dataRecv(mock.createRequest( + $pres({ + 'to': 'tom@montague.lit/resource', + 'from': `lounge@montague.lit/${nick}` + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': `${nick}@montague.lit/resource`, + 'role': 'participant' + }))); + }); + + // Nicknames from messages + const msg = $msg({ + from: 'lounge@montague.lit/jane', + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('Hello world').tree(); + await view.model.queueMessage(msg); + + // Test that pressing @ brings up all options + const textarea = view.el.querySelector('textarea.chat-textarea'); + const at_event = { + 'target': textarea, + 'preventDefault': function preventDefault () {}, + 'stopPropagation': function stopPropagation () {}, + 'keyCode': 50, + 'key': '@' + }; + view.onKeyDown(at_event); + textarea.value = '@'; + view.onKeyUp(at_event); + + await u.waitUntil(() => view.el.querySelectorAll('.suggestion-box__results li').length === 4); + expect(view.el.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick'); + expect(view.el.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry'); + expect(view.el.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane'); + expect(view.el.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom'); + done(); + })); + + it("autocompletes when the user presses tab", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + expect(view.model.occupants.length).toBe(1); + let presence = $pres({ + 'to': 'romeo@montague.lit/orchard', + 'from': 'lounge@montague.lit/some1' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'some1@montague.lit/resource', + 'role': 'participant' }); + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.model.occupants.length).toBe(2); - // Nicknames from messages - const msg = $msg({ - from: 'lounge@montague.lit/jane', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('Hello world').tree(); - await view.model.queueMessage(msg); + const textarea = view.el.querySelector('textarea.chat-textarea'); + textarea.value = "hello som"; - // Test that pressing @ brings up all options - const textarea = view.el.querySelector('textarea.chat-textarea'); - const at_event = { - 'target': textarea, - 'preventDefault': function preventDefault () {}, - 'stopPropagation': function stopPropagation () {}, - 'keyCode': 50, - 'key': '@' - }; - view.onKeyDown(at_event); - textarea.value = '@'; - view.onKeyUp(at_event); + // Press tab + const tab_event = { + 'target': textarea, + 'preventDefault': function preventDefault () {}, + 'stopPropagation': function stopPropagation () {}, + 'keyCode': 9, + 'key': 'Tab' + } + view.onKeyDown(tab_event); + view.onKeyUp(tab_event); + await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false); + expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(1); + expect(view.el.querySelector('.suggestion-box__results li').textContent).toBe('some1'); - await u.waitUntil(() => view.el.querySelectorAll('.suggestion-box__results li').length === 4); - expect(view.el.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick'); - expect(view.el.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('harry'); - expect(view.el.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('jane'); - expect(view.el.querySelector('.suggestion-box__results li:nth-child(4)').textContent).toBe('tom'); - done(); - })); - - it("autocompletes when the user presses tab", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - expect(view.model.occupants.length).toBe(1); - let presence = $pres({ - 'to': 'romeo@montague.lit/orchard', - 'from': 'lounge@montague.lit/some1' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'some1@montague.lit/resource', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.model.occupants.length).toBe(2); - - const textarea = view.el.querySelector('textarea.chat-textarea'); - textarea.value = "hello som"; - - // Press tab - const tab_event = { - 'target': textarea, - 'preventDefault': function preventDefault () {}, - 'stopPropagation': function stopPropagation () {}, - 'keyCode': 9, - 'key': 'Tab' - } - view.onKeyDown(tab_event); - view.onKeyUp(tab_event); - await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false); - expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(1); - expect(view.el.querySelector('.suggestion-box__results li').textContent).toBe('some1'); - - const backspace_event = { - 'target': textarea, - 'preventDefault': function preventDefault () {}, - 'keyCode': 8 - } - for (var i=0; i<3; i++) { - // Press backspace 3 times to remove "som" - view.onKeyDown(backspace_event); - textarea.value = textarea.value.slice(0, textarea.value.length-1) - view.onKeyUp(backspace_event); - } - await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === true); - - presence = $pres({ - 'to': 'romeo@montague.lit/orchard', - 'from': 'lounge@montague.lit/some2' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'some2@montague.lit/resource', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - textarea.value = "hello s s"; - view.onKeyDown(tab_event); - view.onKeyUp(tab_event); - await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false); - expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2); - - const up_arrow_event = { - 'target': textarea, - 'preventDefault': () => (up_arrow_event.defaultPrevented = true), - 'stopPropagation': function stopPropagation () {}, - 'keyCode': 38 - } - view.onKeyDown(up_arrow_event); - view.onKeyUp(up_arrow_event); - expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2); - expect(view.el.querySelector('.suggestion-box__results li[aria-selected="false"]').textContent).toBe('some1'); - expect(view.el.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('some2'); - - view.onKeyDown({ - 'target': textarea, - 'preventDefault': function preventDefault () {}, - 'stopPropagation': function stopPropagation () {}, - 'keyCode': 13 // Enter - }); - expect(textarea.value).toBe('hello s @some2 '); - - // Test that pressing tab twice selects - presence = $pres({ - 'to': 'romeo@montague.lit/orchard', - 'from': 'lounge@montague.lit/z3r0' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'z3r0@montague.lit/resource', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - textarea.value = "hello z"; - view.onKeyDown(tab_event); - view.onKeyUp(tab_event); - await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false); - - view.onKeyDown(tab_event); - view.onKeyUp(tab_event); - await u.waitUntil(() => textarea.value === 'hello @z3r0 '); - done(); - })); - - it("autocompletes when the user presses backspace", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - expect(view.model.occupants.length).toBe(1); - const presence = $pres({ - 'to': 'romeo@montague.lit/orchard', - 'from': 'lounge@montague.lit/some1' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'some1@montague.lit/resource', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.model.occupants.length).toBe(2); - - const textarea = view.el.querySelector('textarea.chat-textarea'); - textarea.value = "hello @some1 "; - - // Press backspace - const backspace_event = { - 'target': textarea, - 'preventDefault': function preventDefault () {}, - 'stopPropagation': function stopPropagation () {}, - 'keyCode': 8, - 'key': 'Backspace' - } + const backspace_event = { + 'target': textarea, + 'preventDefault': function preventDefault () {}, + 'keyCode': 8 + } + for (var i=0; i<3; i++) { + // Press backspace 3 times to remove "som" view.onKeyDown(backspace_event); - textarea.value = "hello @some1"; // Mimic backspace + textarea.value = textarea.value.slice(0, textarea.value.length-1) view.onKeyUp(backspace_event); - await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false); - expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(1); - expect(view.el.querySelector('.suggestion-box__results li').textContent).toBe('some1'); - done(); - })); - }); + } + await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === true); + + presence = $pres({ + 'to': 'romeo@montague.lit/orchard', + 'from': 'lounge@montague.lit/some2' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'some2@montague.lit/resource', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + + textarea.value = "hello s s"; + view.onKeyDown(tab_event); + view.onKeyUp(tab_event); + await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false); + expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2); + + const up_arrow_event = { + 'target': textarea, + 'preventDefault': () => (up_arrow_event.defaultPrevented = true), + 'stopPropagation': function stopPropagation () {}, + 'keyCode': 38 + } + view.onKeyDown(up_arrow_event); + view.onKeyUp(up_arrow_event); + expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2); + expect(view.el.querySelector('.suggestion-box__results li[aria-selected="false"]').textContent).toBe('some1'); + expect(view.el.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('some2'); + + view.onKeyDown({ + 'target': textarea, + 'preventDefault': function preventDefault () {}, + 'stopPropagation': function stopPropagation () {}, + 'keyCode': 13 // Enter + }); + expect(textarea.value).toBe('hello s @some2 '); + + // Test that pressing tab twice selects + presence = $pres({ + 'to': 'romeo@montague.lit/orchard', + 'from': 'lounge@montague.lit/z3r0' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'z3r0@montague.lit/resource', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + textarea.value = "hello z"; + view.onKeyDown(tab_event); + view.onKeyUp(tab_event); + await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false); + + view.onKeyDown(tab_event); + view.onKeyUp(tab_event); + await u.waitUntil(() => textarea.value === 'hello @z3r0 '); + done(); + })); + + it("autocompletes when the user presses backspace", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + expect(view.model.occupants.length).toBe(1); + const presence = $pres({ + 'to': 'romeo@montague.lit/orchard', + 'from': 'lounge@montague.lit/some1' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'some1@montague.lit/resource', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.model.occupants.length).toBe(2); + + const textarea = view.el.querySelector('textarea.chat-textarea'); + textarea.value = "hello @some1 "; + + // Press backspace + const backspace_event = { + 'target': textarea, + 'preventDefault': function preventDefault () {}, + 'stopPropagation': function stopPropagation () {}, + 'keyCode': 8, + 'key': 'Backspace' + } + view.onKeyDown(backspace_event); + textarea.value = "hello @some1"; // Mimic backspace + view.onKeyUp(backspace_event); + await u.waitUntil(() => view.el.querySelector('.suggestion-box__results').hidden === false); + expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(1); + expect(view.el.querySelector('.suggestion-box__results li').textContent).toBe('some1'); + done(); + })); }); diff --git a/spec/bookmarks.js b/spec/bookmarks.js index 20d52cedb..3dc93aea3 100644 --- a/spec/bookmarks.js +++ b/spec/bookmarks.js @@ -1,101 +1,266 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const $iq = converse.env.$iq, - $msg = converse.env.$msg, - Strophe = converse.env.Strophe, - sizzle = converse.env.sizzle, - _ = converse.env._, - u = converse.env.utils; +/* global mock */ + +describe("A chat room", function () { + + it("can be bookmarked", mock.initConverse(['rosterGroupsFetched'], {}, async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, + [{'category': 'pubsub', 'type': 'pep'}], + ['http://jabber.org/protocol/pubsub#publish-options'] + ); + const { u, $iq } = converse.env; + let sent_stanza, IQ_id; + const sendIQ = _converse.connection.sendIQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + spyOn(_converse.connection, 'getUniqueId').and.callThrough(); + + await mock.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC'); + var jid = 'theplay@conference.shakespeare.lit'; + const view = _converse.chatboxviews.get(jid); + spyOn(view, 'renderBookmarkForm').and.callThrough(); + spyOn(view, 'closeForm').and.callThrough(); + await u.waitUntil(() => view.el.querySelector('.toggle-bookmark') !== null); + const toggle = view.el.querySelector('.toggle-bookmark'); + expect(toggle.title).toBe('Bookmark this groupchat'); + toggle.click(); + expect(view.renderBookmarkForm).toHaveBeenCalled(); + + view.el.querySelector('.button-cancel').click(); + expect(view.closeForm).toHaveBeenCalled(); + expect(u.hasClass('on-button', toggle), false); + expect(toggle.title).toBe('Bookmark this groupchat'); + + toggle.click(); + expect(view.renderBookmarkForm).toHaveBeenCalled(); + + /* Client uploads data: + * -------------------- + * + * + * + * + * + * + * JC + * + * + * + * + * + * + * + * http://jabber.org/protocol/pubsub#publish-options + * + * + * true + * + * + * whitelist + * + * + * + * + * + */ + expect(view.model.get('bookmarked')).toBeFalsy(); + const form = view.el.querySelector('.chatroom-form'); + form.querySelector('input[name="name"]').value = 'Play's the Thing'; + form.querySelector('input[name="autojoin"]').checked = 'checked'; + form.querySelector('input[name="nick"]').value = 'JC'; + + _converse.connection.IQ_stanzas = []; + view.el.querySelector('.btn-primary').click(); + + await u.waitUntil(() => sent_stanza); + expect(sent_stanza.toLocaleString()).toBe( + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `JC`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `http://jabber.org/protocol/pubsub#publish-options`+ + ``+ + ``+ + `true`+ + ``+ + ``+ + `whitelist`+ + ``+ + ``+ + ``+ + ``+ + `` + ); + /* Server acknowledges successful storage + * + * + */ + const stanza = $iq({ + 'to':_converse.connection.jid, + 'type':'result', + 'id':IQ_id + }); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.get('bookmarked')); + expect(view.model.get('bookmarked')).toBeTruthy(); + await u.waitUntil(() => view.el.querySelector('.toggle-bookmark')?.title === 'Unbookmark this groupchat'); + expect(u.hasClass('on-button', view.el.querySelector('.toggle-bookmark')), true); + // We ignore this IQ stanza... (unless it's an error stanza), so + // nothing to test for here. + done(); + })); - describe("A chat room", function () { + it("will be automatically opened if 'autojoin' is set on the bookmark", mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - it("can be bookmarked", mock.initConverse(['rosterGroupsFetched'], {}, async function (done, _converse) { + const { u, _ } = converse.env; + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, + [{'category': 'pubsub', 'type': 'pep'}], + ['http://jabber.org/protocol/pubsub#publish-options'] + ); + await u.waitUntil(() => _converse.bookmarks); + let jid = 'lounge@montague.lit'; + _converse.bookmarks.create({ + 'jid': jid, + 'autojoin': false, + 'name': 'The Lounge', + 'nick': ' Othello' + }); + expect(_converse.chatboxviews.get(jid) === undefined).toBeTruthy(); - await test_utils.waitUntilDiscoConfirmed( + jid = 'theplay@conference.shakespeare.lit'; + _converse.bookmarks.create({ + 'jid': jid, + 'autojoin': true, + 'name': 'The Play', + 'nick': ' Othello' + }); + await new Promise(resolve => _converse.api.listen.once('chatRoomViewInitialized', resolve)); + expect(_.isUndefined(_converse.chatboxviews.get(jid))).toBeFalsy(); + + // Check that we don't auto-join if muc_respect_autojoin is false + _converse.muc_respect_autojoin = false; + jid = 'balcony@conference.shakespeare.lit'; + _converse.bookmarks.create({ + 'jid': jid, + 'autojoin': true, + 'name': 'Balcony', + 'nick': ' Othello' + }); + expect(_converse.chatboxviews.get(jid) === undefined).toBe(true); + done(); + })); + + + describe("when bookmarked", function () { + + it("will use the nickname from the bookmark", mock.initConverse( + ['rosterGroupsFetched'], {}, async function (done, _converse) { + + const { u } = converse.env; + await mock.waitUntilBookmarksReturned(_converse); + const muc_jid = 'coven@chat.shakespeare.lit'; + _converse.bookmarks.create({ + 'jid': muc_jid, + 'autojoin': false, + 'name': 'The Play', + 'nick': 'Othello' + }); + spyOn(_converse.ChatRoom.prototype, 'getAndPersistNickname').and.callThrough(); + const room_creation_promise = _converse.api.rooms.open(muc_jid); + await mock.getRoomFeatures(_converse, muc_jid); + const room = await room_creation_promise; + await u.waitUntil(() => room.getAndPersistNickname.calls.count()); + expect(room.get('nick')).toBe('Othello'); + done(); + })); + + it("displays that it's bookmarked through its bookmark icon", mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const { u } = converse.env; + mock.waitUntilDiscoConfirmed( _converse, _converse.bare_jid, [{'category': 'pubsub', 'type': 'pep'}], ['http://jabber.org/protocol/pubsub#publish-options'] ); - let sent_stanza, IQ_id; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_stanza = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); + await _converse.api.rooms.open(`lounge@montague.lit`); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).toBe(null); + _converse.bookmarks.create({ + 'jid': view.model.get('jid'), + 'autojoin': false, + 'name': 'The lounge', + 'nick': ' some1' }); + view.model.set('bookmarked', true); + await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') !== null); + view.model.set('bookmarked', false); + await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') === null); + done(); + })); + + it("can be unbookmarked", mock.initConverse( + ['rosterGroupsFetched'], {}, async function (done, _converse) { + + const { u, Strophe } = converse.env; + await mock.waitUntilBookmarksReturned(_converse); + const muc_jid = 'theplay@conference.shakespeare.lit'; + await _converse.api.rooms.open(muc_jid); + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => view.el.querySelector('.toggle-bookmark')); + + spyOn(view, 'toggleBookmark').and.callThrough(); + spyOn(_converse.bookmarks, 'sendBookmarkStanza').and.callThrough(); + view.delegateEvents(); + + _converse.bookmarks.create({ + 'jid': view.model.get('jid'), + 'autojoin': false, + 'name': 'The Play', + 'nick': ' Othello' + }); + + expect(_converse.bookmarks.length).toBe(1); + await u.waitUntil(() => _converse.chatboxes.length >= 1); + expect(view.model.get('bookmarked')).toBeTruthy(); + await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') !== null); spyOn(_converse.connection, 'getUniqueId').and.callThrough(); + const bookmark_icon = view.el.querySelector('.toggle-bookmark'); + bookmark_icon.click(); + expect(view.toggleBookmark).toHaveBeenCalled(); + await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') === null); + expect(_converse.bookmarks.length).toBe(0); - await test_utils.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC'); - var jid = 'theplay@conference.shakespeare.lit'; - const view = _converse.chatboxviews.get(jid); - spyOn(view, 'renderBookmarkForm').and.callThrough(); - spyOn(view, 'closeForm').and.callThrough(); - await u.waitUntil(() => view.el.querySelector('.toggle-bookmark') !== null); - const toggle = view.el.querySelector('.toggle-bookmark'); - expect(toggle.title).toBe('Bookmark this groupchat'); - toggle.click(); - expect(view.renderBookmarkForm).toHaveBeenCalled(); - - view.el.querySelector('.button-cancel').click(); - expect(view.closeForm).toHaveBeenCalled(); - expect(u.hasClass('on-button', toggle), false); - expect(toggle.title).toBe('Bookmark this groupchat'); - - toggle.click(); - expect(view.renderBookmarkForm).toHaveBeenCalled(); - - /* Client uploads data: - * -------------------- - * - * - * - * - * - * - * JC - * - * - * - * - * - * - * - * http://jabber.org/protocol/pubsub#publish-options - * - * - * true - * - * - * whitelist - * - * - * - * - * - */ - expect(view.model.get('bookmarked')).toBeFalsy(); - const form = view.el.querySelector('.chatroom-form'); - form.querySelector('input[name="name"]').value = 'Play's the Thing'; - form.querySelector('input[name="autojoin"]').checked = 'checked'; - form.querySelector('input[name="nick"]').value = 'JC'; - - _converse.connection.IQ_stanzas = []; - view.el.querySelector('.btn-primary').click(); - - await u.waitUntil(() => sent_stanza); - expect(sent_stanza.toLocaleString()).toBe( - ``+ + // Check that an IQ stanza is sent out, containing no + // conferences to bookmark (since we removed the one and + // only bookmark). + const sent_stanza = _converse.connection.IQ_stanzas.pop(); + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ ``+ ``+ ``+ - ``+ - ``+ - `JC`+ - ``+ - ``+ + ``+ ``+ ``+ ``+ @@ -114,278 +279,184 @@ window.addEventListener('converse-loaded', () => { ``+ `` ); - /* Server acknowledges successful storage - * - * - */ - const stanza = $iq({ - 'to':_converse.connection.jid, - 'type':'result', - 'id':IQ_id - }); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.get('bookmarked')); - expect(view.model.get('bookmarked')).toBeTruthy(); - await u.waitUntil(() => view.el.querySelector('.toggle-bookmark')?.title === 'Unbookmark this groupchat'); - expect(u.hasClass('on-button', view.el.querySelector('.toggle-bookmark')), true); - // We ignore this IQ stanza... (unless it's an error stanza), so - // nothing to test for here. done(); })); - - - it("will be automatically opened if 'autojoin' is set on the bookmark", mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - ['http://jabber.org/protocol/pubsub#publish-options'] - ); - await u.waitUntil(() => _converse.bookmarks); - let jid = 'lounge@montague.lit'; - _converse.bookmarks.create({ - 'jid': jid, - 'autojoin': false, - 'name': 'The Lounge', - 'nick': ' Othello' - }); - expect(_converse.chatboxviews.get(jid) === undefined).toBeTruthy(); - - jid = 'theplay@conference.shakespeare.lit'; - _converse.bookmarks.create({ - 'jid': jid, - 'autojoin': true, - 'name': 'The Play', - 'nick': ' Othello' - }); - await new Promise(resolve => _converse.api.listen.once('chatRoomViewInitialized', resolve)); - expect(_.isUndefined(_converse.chatboxviews.get(jid))).toBeFalsy(); - - // Check that we don't auto-join if muc_respect_autojoin is false - _converse.muc_respect_autojoin = false; - jid = 'balcony@conference.shakespeare.lit'; - _converse.bookmarks.create({ - 'jid': jid, - 'autojoin': true, - 'name': 'Balcony', - 'nick': ' Othello' - }); - expect(_converse.chatboxviews.get(jid) === undefined).toBe(true); - done(); - })); - - - describe("when bookmarked", function () { - - it("will use the nickname from the bookmark", mock.initConverse( - ['rosterGroupsFetched'], {}, async function (done, _converse) { - - await test_utils.waitUntilBookmarksReturned(_converse); - const muc_jid = 'coven@chat.shakespeare.lit'; - _converse.bookmarks.create({ - 'jid': muc_jid, - 'autojoin': false, - 'name': 'The Play', - 'nick': 'Othello' - }); - spyOn(_converse.ChatRoom.prototype, 'getAndPersistNickname').and.callThrough(); - const room_creation_promise = _converse.api.rooms.open(muc_jid); - await test_utils.getRoomFeatures(_converse, muc_jid); - const room = await room_creation_promise; - await u.waitUntil(() => room.getAndPersistNickname.calls.count()); - expect(room.get('nick')).toBe('Othello'); - done(); - })); - - it("displays that it's bookmarked through its bookmark icon", mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - test_utils.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - ['http://jabber.org/protocol/pubsub#publish-options'] - ); - await _converse.api.rooms.open(`lounge@montague.lit`); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).toBe(null); - _converse.bookmarks.create({ - 'jid': view.model.get('jid'), - 'autojoin': false, - 'name': 'The lounge', - 'nick': ' some1' - }); - view.model.set('bookmarked', true); - await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') !== null); - view.model.set('bookmarked', false); - await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') === null); - done(); - })); - - it("can be unbookmarked", mock.initConverse( - ['rosterGroupsFetched'], {}, async function (done, _converse) { - - await test_utils.waitUntilBookmarksReturned(_converse); - const muc_jid = 'theplay@conference.shakespeare.lit'; - await _converse.api.rooms.open(muc_jid); - const view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => view.el.querySelector('.toggle-bookmark')); - - spyOn(view, 'toggleBookmark').and.callThrough(); - spyOn(_converse.bookmarks, 'sendBookmarkStanza').and.callThrough(); - view.delegateEvents(); - - _converse.bookmarks.create({ - 'jid': view.model.get('jid'), - 'autojoin': false, - 'name': 'The Play', - 'nick': ' Othello' - }); - - expect(_converse.bookmarks.length).toBe(1); - await u.waitUntil(() => _converse.chatboxes.length >= 1); - expect(view.model.get('bookmarked')).toBeTruthy(); - await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') !== null); - spyOn(_converse.connection, 'getUniqueId').and.callThrough(); - const bookmark_icon = view.el.querySelector('.toggle-bookmark'); - bookmark_icon.click(); - expect(view.toggleBookmark).toHaveBeenCalled(); - await u.waitUntil(() => view.el.querySelector('.chatbox-title__text .fa-bookmark') === null); - expect(_converse.bookmarks.length).toBe(0); - - // Check that an IQ stanza is sent out, containing no - // conferences to bookmark (since we removed the one and - // only bookmark). - const sent_stanza = _converse.connection.IQ_stanzas.pop(); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - `http://jabber.org/protocol/pubsub#publish-options`+ - ``+ - ``+ - `true`+ - ``+ - ``+ - `whitelist`+ - ``+ - ``+ - ``+ - ``+ - `` - ); - done(); - })); - }); - - describe("and when autojoin is set", function () { - - it("will be be opened and joined automatically upon login", mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitUntilBookmarksReturned(_converse); - spyOn(_converse.api.rooms, 'create').and.callThrough(); - const jid = 'theplay@conference.shakespeare.lit'; - const model = _converse.bookmarks.create({ - 'jid': jid, - 'autojoin': false, - 'name': 'The Play', - 'nick': '' - }); - expect(_converse.api.rooms.create).not.toHaveBeenCalled(); - _converse.bookmarks.remove(model); - _converse.bookmarks.create({ - 'jid': jid, - 'autojoin': true, - 'name': 'Hamlet', - 'nick': '' - }); - expect(_converse.api.rooms.create).toHaveBeenCalled(); - done(); - })); - }); }); - describe("Bookmarks", function () { + describe("and when autojoin is set", function () { - it("can be pushed from the XMPP server", mock.initConverse( - ['rosterGroupsFetched', 'connected'], {}, async function (done, _converse) { + it("will be be opened and joined automatically upon login", mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - await test_utils.waitUntilBookmarksReturned(_converse); + await mock.waitUntilBookmarksReturned(_converse); + spyOn(_converse.api.rooms, 'create').and.callThrough(); + const jid = 'theplay@conference.shakespeare.lit'; + const model = _converse.bookmarks.create({ + 'jid': jid, + 'autojoin': false, + 'name': 'The Play', + 'nick': '' + }); + expect(_converse.api.rooms.create).not.toHaveBeenCalled(); + _converse.bookmarks.remove(model); + _converse.bookmarks.create({ + 'jid': jid, + 'autojoin': true, + 'name': 'Hamlet', + 'nick': '' + }); + expect(_converse.api.rooms.create).toHaveBeenCalled(); + done(); + })); + }); +}); - /* The stored data is automatically pushed to all of the user's - * connected resources. - * - * Publisher receives event notification - * ------------------------------------- - * - * - * - * - * - * - * JC - * - * - * - * - * - * - */ - const stanza = $msg({ - 'from': 'romeo@montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'type': 'headline', - 'id': 'rnfoo1' - }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) +describe("Bookmarks", function () { + + it("can be pushed from the XMPP server", mock.initConverse( + ['rosterGroupsFetched', 'connected'], {}, async function (done, _converse) { + + const { $msg, u } = converse.env; + await mock.waitUntilBookmarksReturned(_converse); + + /* The stored data is automatically pushed to all of the user's + * connected resources. + * + * Publisher receives event notification + * ------------------------------------- + * + * + * + * + * + * + * JC + * + * + * + * + * + * + */ + const stanza = $msg({ + 'from': 'romeo@montague.lit', + 'to': 'romeo@montague.lit/orchard', + 'type': 'headline', + 'id': 'rnfoo1' + }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) + .c('items', {'node': 'storage:bookmarks'}) + .c('item', {'id': 'current'}) + .c('storage', {'xmlns': 'storage:bookmarks'}) + .c('conference', {'name': 'The Play's the Thing', + 'autojoin': 'true', + 'jid':'theplay@conference.shakespeare.lit'}) + .c('nick').t('JC'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.bookmarks.length); + expect(_converse.bookmarks.length).toBe(1); + expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined(); + done(); + })); + + + it("can be retrieved from the XMPP server", mock.initConverse( + ['chatBoxesFetched', 'roomsPanelRendered', 'rosterGroupsFetched'], {}, + async function (done, _converse) { + + const { Strophe, sizzle, u, $iq } = converse.env; + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, + [{'category': 'pubsub', 'type': 'pep'}], + ['http://jabber.org/protocol/pubsub#publish-options'] + ); + /* Client requests all items + * ------------------------- + * + * + * + * + * + * + */ + const IQ_stanzas = _converse.connection.IQ_stanzas; + const sent_stanza = await u.waitUntil( + () => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop()); + + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + ''+ + ''+ + ''+ + ''); + + /* + * Server returns all items + * ------------------------ + * + * + * + * + * + * + * JC + * + * + * + * + * + * + */ + expect(_converse.bookmarks.models.length).toBe(0); + + spyOn(_converse.bookmarks, 'onBookmarksReceived').and.callThrough(); + var stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':sent_stanza.getAttribute('id')}) + .c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) .c('items', {'node': 'storage:bookmarks'}) .c('item', {'id': 'current'}) .c('storage', {'xmlns': 'storage:bookmarks'}) - .c('conference', {'name': 'The Play's the Thing', - 'autojoin': 'true', - 'jid':'theplay@conference.shakespeare.lit'}) - .c('nick').t('JC'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.bookmarks.length); - expect(_converse.bookmarks.length).toBe(1); - expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined(); - done(); - })); + .c('conference', { + 'name': 'The Play's the Thing', + 'autojoin': 'true', + 'jid': 'theplay@conference.shakespeare.lit' + }).c('nick').t('JC').up().up() + .c('conference', { + 'name': 'Another room', + 'autojoin': 'false', + 'jid': 'another@conference.shakespeare.lit' + }); // Purposefully exclude the element to test #1043 + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.bookmarks.onBookmarksReceived.calls.count()); + await _converse.api.waitUntil('bookmarksInitialized'); + expect(_converse.bookmarks.models.length).toBe(2); + expect(_converse.bookmarks.findWhere({'jid': 'theplay@conference.shakespeare.lit'}).get('autojoin')).toBe(true); + expect(_converse.bookmarks.findWhere({'jid': 'another@conference.shakespeare.lit'}).get('autojoin')).toBe(false); + done(); + })); + describe("The rooms panel", function () { - it("can be retrieved from the XMPP server", mock.initConverse( - ['chatBoxesFetched', 'roomsPanelRendered', 'rosterGroupsFetched'], {}, - async function (done, _converse) { + it("shows a list of bookmarks", mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - await test_utils.waitUntilDiscoConfirmed( + await mock.waitUntilDiscoConfirmed( _converse, _converse.bare_jid, [{'category': 'pubsub', 'type': 'pep'}], ['http://jabber.org/protocol/pubsub#publish-options'] ); - /* Client requests all items - * ------------------------- - * - * - * - * - * - * - */ + mock.openControlBox(_converse); + + const { Strophe, u, sizzle, $iq } = converse.env; const IQ_stanzas = _converse.connection.IQ_stanzas; const sent_stanza = await u.waitUntil( () => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop()); @@ -395,222 +466,151 @@ window.addEventListener('converse-loaded', () => { ''+ ''+ ''+ - ''); + '' + ); - /* - * Server returns all items - * ------------------------ - * - * - * - * - * - * - * JC - * - * - * - * - * - * - */ - expect(_converse.bookmarks.models.length).toBe(0); - - spyOn(_converse.bookmarks, 'onBookmarksReceived').and.callThrough(); - var stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':sent_stanza.getAttribute('id')}) + const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':sent_stanza.getAttribute('id')}) .c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) .c('items', {'node': 'storage:bookmarks'}) .c('item', {'id': 'current'}) .c('storage', {'xmlns': 'storage:bookmarks'}) .c('conference', { 'name': 'The Play's the Thing', - 'autojoin': 'true', + 'autojoin': 'false', 'jid': 'theplay@conference.shakespeare.lit' }).c('nick').t('JC').up().up() + .c('conference', { + 'name': '1st Bookmark', + 'autojoin': 'false', + 'jid': 'first@conference.shakespeare.lit' + }).c('nick').t('JC').up().up() + .c('conference', { + 'autojoin': 'false', + 'jid': 'noname@conference.shakespeare.lit' + }).c('nick').t('JC').up().up() + .c('conference', { + 'name': 'Bookmark with a very very long name that will be shortened', + 'autojoin': 'false', + 'jid': 'longname@conference.shakespeare.lit' + }).c('nick').t('JC').up().up() .c('conference', { 'name': 'Another room', 'autojoin': 'false', 'jid': 'another@conference.shakespeare.lit' - }); // Purposefully exclude the element to test #1043 - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.bookmarks.onBookmarksReceived.calls.count()); - await _converse.api.waitUntil('bookmarksInitialized'); - expect(_converse.bookmarks.models.length).toBe(2); - expect(_converse.bookmarks.findWhere({'jid': 'theplay@conference.shakespeare.lit'}).get('autojoin')).toBe(true); - expect(_converse.bookmarks.findWhere({'jid': 'another@conference.shakespeare.lit'}).get('autojoin')).toBe(false); + }).c('nick').t('JC').up().up(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + await u.waitUntil(() => document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length); + expect(document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length).toBe(5); + let els = document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item a.list-item-link'); + expect(els[0].textContent).toBe("1st Bookmark"); + expect(els[1].textContent).toBe("Another room"); + expect(els[2].textContent).toBe("Bookmark with a very very long name that will be shortened"); + expect(els[3].textContent).toBe("noname@conference.shakespeare.lit"); + expect(els[4].textContent).toBe("The Play's the Thing"); + + spyOn(window, 'confirm').and.returnValue(true); + document.querySelector('#chatrooms .bookmarks.rooms-list .room-item:nth-child(2) a:nth-child(2)').click(); + expect(window.confirm).toHaveBeenCalled(); + await u.waitUntil(() => document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length === 4) + els = document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item a.list-item-link'); + expect(els[0].textContent).toBe("1st Bookmark"); + expect(els[1].textContent).toBe("Bookmark with a very very long name that will be shortened"); + expect(els[2].textContent).toBe("noname@conference.shakespeare.lit"); + expect(els[3].textContent).toBe("The Play's the Thing"); done(); })); - describe("The rooms panel", function () { - it("shows a list of bookmarks", mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("remembers the toggle state of the bookmarks list", mock.initConverse( + ['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - ['http://jabber.org/protocol/pubsub#publish-options'] - ); - test_utils.openControlBox(_converse); + await mock.openControlBox(_converse); + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, + [{'category': 'pubsub', 'type': 'pep'}], + ['http://jabber.org/protocol/pubsub#publish-options'] + ); - const IQ_stanzas = _converse.connection.IQ_stanzas; - const sent_stanza = await u.waitUntil( - () => IQ_stanzas.filter(s => sizzle('items[node="storage:bookmarks"]', s).length).pop()); + const { Strophe, u, sizzle, $iq } = converse.env; + const IQ_stanzas = _converse.connection.IQ_stanzas; + const sent_stanza = await u.waitUntil( + () => IQ_stanzas.filter(s => sizzle('iq items[node="storage:bookmarks"]', s).length).pop()); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ''+ - ''+ - ''+ - '' - ); + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + ''+ + ''+ + ''+ + '' + ); + const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id': sent_stanza.getAttribute('id')}) + .c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) + .c('items', {'node': 'storage:bookmarks'}) + .c('item', {'id': 'current'}) + .c('storage', {'xmlns': 'storage:bookmarks'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await _converse.api.waitUntil('bookmarksInitialized'); - const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id':sent_stanza.getAttribute('id')}) - .c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) - .c('items', {'node': 'storage:bookmarks'}) - .c('item', {'id': 'current'}) - .c('storage', {'xmlns': 'storage:bookmarks'}) - .c('conference', { - 'name': 'The Play's the Thing', - 'autojoin': 'false', - 'jid': 'theplay@conference.shakespeare.lit' - }).c('nick').t('JC').up().up() - .c('conference', { - 'name': '1st Bookmark', - 'autojoin': 'false', - 'jid': 'first@conference.shakespeare.lit' - }).c('nick').t('JC').up().up() - .c('conference', { - 'autojoin': 'false', - 'jid': 'noname@conference.shakespeare.lit' - }).c('nick').t('JC').up().up() - .c('conference', { - 'name': 'Bookmark with a very very long name that will be shortened', - 'autojoin': 'false', - 'jid': 'longname@conference.shakespeare.lit' - }).c('nick').t('JC').up().up() - .c('conference', { - 'name': 'Another room', - 'autojoin': 'false', - 'jid': 'another@conference.shakespeare.lit' - }).c('nick').t('JC').up().up(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - await u.waitUntil(() => document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length); - expect(document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length).toBe(5); - let els = document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item a.list-item-link'); - expect(els[0].textContent).toBe("1st Bookmark"); - expect(els[1].textContent).toBe("Another room"); - expect(els[2].textContent).toBe("Bookmark with a very very long name that will be shortened"); - expect(els[3].textContent).toBe("noname@conference.shakespeare.lit"); - expect(els[4].textContent).toBe("The Play's the Thing"); - - spyOn(window, 'confirm').and.returnValue(true); - document.querySelector('#chatrooms .bookmarks.rooms-list .room-item:nth-child(2) a:nth-child(2)').click(); - expect(window.confirm).toHaveBeenCalled(); - await u.waitUntil(() => document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item').length === 4) - els = document.querySelectorAll('#chatrooms div.bookmarks.rooms-list .room-item a.list-item-link'); - expect(els[0].textContent).toBe("1st Bookmark"); - expect(els[1].textContent).toBe("Bookmark with a very very long name that will be shortened"); - expect(els[2].textContent).toBe("noname@conference.shakespeare.lit"); - expect(els[3].textContent).toBe("The Play's the Thing"); - done(); - })); - - - it("remembers the toggle state of the bookmarks list", mock.initConverse( - ['rosterGroupsFetched'], {}, async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - ['http://jabber.org/protocol/pubsub#publish-options'] - ); - - const IQ_stanzas = _converse.connection.IQ_stanzas; - const sent_stanza = await u.waitUntil( - () => IQ_stanzas.filter(s => sizzle('iq items[node="storage:bookmarks"]', s).length).pop()); - - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ''+ - ''+ - ''+ - '' - ); - const stanza = $iq({'to': _converse.connection.jid, 'type':'result', 'id': sent_stanza.getAttribute('id')}) - .c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) - .c('items', {'node': 'storage:bookmarks'}) - .c('item', {'id': 'current'}) - .c('storage', {'xmlns': 'storage:bookmarks'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await _converse.api.waitUntil('bookmarksInitialized'); - - _converse.bookmarks.create({ - 'jid': 'theplay@conference.shakespeare.lit', - 'autojoin': false, - 'name': 'The Play', - 'nick': '' - }); - const el = _converse.chatboxviews.el - const selector = '#chatrooms .bookmarks.rooms-list .room-item'; - await u.waitUntil(() => sizzle(selector, el).filter(u.isVisible).length); - expect(u.hasClass('collapsed', sizzle('#chatrooms .bookmarks.rooms-list', el).pop())).toBeFalsy(); - expect(sizzle(selector, el).filter(u.isVisible).length).toBe(1); - expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED); - sizzle('#chatrooms .bookmarks-toggle', el).pop().click(); - expect(u.hasClass('collapsed', sizzle('#chatrooms .bookmarks.rooms-list', el).pop())).toBeTruthy(); - expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.CLOSED); - sizzle('#chatrooms .bookmarks-toggle', el).pop().click(); - expect(u.hasClass('collapsed', sizzle('#chatrooms .bookmarks.rooms-list', el).pop())).toBeFalsy(); - expect(sizzle(selector, el).filter(u.isVisible).length).toBe(1); - expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED); - done(); - })); - }); - }); - - describe("When hide_open_bookmarks is true and a bookmarked room is opened", function () { - - it("can be closed", mock.initConverse( - ['rosterGroupsFetched'], - { hide_open_bookmarks: true }, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitUntilBookmarksReturned(_converse); - - // Check that it's there - const jid = 'room@conference.example.org'; _converse.bookmarks.create({ - 'jid': jid, + 'jid': 'theplay@conference.shakespeare.lit', 'autojoin': false, 'name': 'The Play', - 'nick': ' Othello' + 'nick': '' }); - expect(_converse.bookmarks.length).toBe(1); - - const bmarks_view = _converse.bookmarksview; - await u.waitUntil(() => bmarks_view.el.querySelectorAll(".open-room").length, 500); - const room_els = bmarks_view.el.querySelectorAll(".open-room"); - expect(room_els.length).toBe(1); - - const bookmark = _converse.bookmarksview.el.querySelector(".open-room"); - bookmark.click(); - await u.waitUntil(() => _converse.chatboxviews.get(jid)); - - expect(u.hasClass('hidden', _converse.bookmarksview.el.querySelector(".available-chatroom"))).toBeTruthy(); - // Check that it reappears once the room is closed - const view = _converse.chatboxviews.get(jid); - view.close(); - await u.waitUntil(() => !u.hasClass('hidden', _converse.bookmarksview.el.querySelector(".available-chatroom"))); + const el = _converse.chatboxviews.el + const selector = '#chatrooms .bookmarks.rooms-list .room-item'; + await u.waitUntil(() => sizzle(selector, el).filter(u.isVisible).length); + expect(u.hasClass('collapsed', sizzle('#chatrooms .bookmarks.rooms-list', el).pop())).toBeFalsy(); + expect(sizzle(selector, el).filter(u.isVisible).length).toBe(1); + expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED); + sizzle('#chatrooms .bookmarks-toggle', el).pop().click(); + expect(u.hasClass('collapsed', sizzle('#chatrooms .bookmarks.rooms-list', el).pop())).toBeTruthy(); + expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.CLOSED); + sizzle('#chatrooms .bookmarks-toggle', el).pop().click(); + expect(u.hasClass('collapsed', sizzle('#chatrooms .bookmarks.rooms-list', el).pop())).toBeFalsy(); + expect(sizzle(selector, el).filter(u.isVisible).length).toBe(1); + expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED); done(); })); }); }); + +describe("When hide_open_bookmarks is true and a bookmarked room is opened", function () { + + it("can be closed", mock.initConverse( + ['rosterGroupsFetched'], + { hide_open_bookmarks: true }, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitUntilBookmarksReturned(_converse); + + // Check that it's there + const jid = 'room@conference.example.org'; + _converse.bookmarks.create({ + 'jid': jid, + 'autojoin': false, + 'name': 'The Play', + 'nick': ' Othello' + }); + expect(_converse.bookmarks.length).toBe(1); + + const u = converse.env.utils; + const bmarks_view = _converse.bookmarksview; + await u.waitUntil(() => bmarks_view.el.querySelectorAll(".open-room").length, 500); + const room_els = bmarks_view.el.querySelectorAll(".open-room"); + expect(room_els.length).toBe(1); + + const bookmark = _converse.bookmarksview.el.querySelector(".open-room"); + bookmark.click(); + await u.waitUntil(() => _converse.chatboxviews.get(jid)); + + expect(u.hasClass('hidden', _converse.bookmarksview.el.querySelector(".available-chatroom"))).toBeTruthy(); + // Check that it reappears once the room is closed + const view = _converse.chatboxviews.get(jid); + view.close(); + await u.waitUntil(() => !u.hasClass('hidden', _converse.bookmarksview.el.querySelector(".available-chatroom"))); + done(); + })); +}); diff --git a/spec/chatbox.js b/spec/chatbox.js index f947ebc8d..2c63b7b5c 100644 --- a/spec/chatbox.js +++ b/spec/chatbox.js @@ -1,1611 +1,1609 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const _ = converse.env._; - const $msg = converse.env.$msg; - const Strophe = converse.env.Strophe; - const u = converse.env.utils; - const sizzle = converse.env.sizzle; +/*global mock */ - return describe("Chatboxes", function () { +const _ = converse.env._; +const $msg = converse.env.$msg; +const Strophe = converse.env.Strophe; +const u = converse.env.utils; +const sizzle = converse.env.sizzle; - describe("A Chatbox", function () { +describe("Chatboxes", function () { - it("has a /help command to show the available commands", mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { + describe("A Chatbox", function () { - await test_utils.waitForRoster(_converse, 'current', 1); - await test_utils.openControlBox(_converse); + it("has a /help command to show the available commands", mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - test_utils.sendMessage(view, '/help'); + await mock.waitForRoster(_converse, 'current', 1); + await mock.openControlBox(_converse); - const info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info:not(.chat-date)'), 0); - expect(info_messages.length).toBe(4); - expect(info_messages.pop().textContent).toBe('/help: Show this menu'); - expect(info_messages.pop().textContent).toBe('/me: Write in the third person'); - expect(info_messages.pop().textContent).toBe('/close: Close this chat'); - expect(info_messages.pop().textContent).toBe('/clear: Remove messages'); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + mock.sendMessage(view, '/help'); - const msg = $msg({ - from: contact_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t('hello world').tree(); - await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); - expect(view.msgs_container.lastElementChild.textContent.trim().indexOf('hello world')).not.toBe(-1); - done(); - })); + const info_messages = Array.prototype.slice.call(view.el.querySelectorAll('.chat-info:not(.chat-date)'), 0); + expect(info_messages.length).toBe(4); + expect(info_messages.pop().textContent).toBe('/help: Show this menu'); + expect(info_messages.pop().textContent).toBe('/me: Write in the third person'); + expect(info_messages.pop().textContent).toBe('/close: Close this chat'); + expect(info_messages.pop().textContent).toBe('/clear: Remove messages'); + + const msg = $msg({ + from: contact_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('body').t('hello world').tree(); + await _converse.handleMessageStanza(msg); + await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); + expect(view.msgs_container.lastElementChild.textContent.trim().indexOf('hello world')).not.toBe(-1); + done(); + })); - it("supports the /me command", mock.initConverse(['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); - await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); - await test_utils.openControlBox(_converse); - expect(_converse.chatboxes.length).toEqual(1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - let message = '/me is tired'; - const msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t(message).up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + it("supports the /me command", mock.initConverse(['rosterGroupsFetched'], {}, async function (done, _converse) { + await mock.waitForRoster(_converse, 'current'); + await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); + await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); + await mock.openControlBox(_converse); + expect(_converse.chatboxes.length).toEqual(1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + let message = '/me is tired'; + const msg = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('body').t(message).up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); - await _converse.handleMessageStanza(msg); - const view = _converse.chatboxviews.get(sender_jid); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1); - expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Mercutio')).toBeTruthy(); - expect(view.el.querySelector('.chat-msg__text').textContent).toBe('is tired'); - message = '/me is as well'; - await test_utils.sendMessage(view, message); - expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2); - await u.waitUntil(() => sizzle('.chat-msg__author:last', view.el).pop().textContent.trim() === '**Romeo Montague'); - const last_el = sizzle('.chat-msg__text:last', view.el).pop(); - expect(last_el.textContent).toBe('is as well'); - expect(u.hasClass('chat-msg--followup', last_el)).toBe(false); - // Check that /me messages after a normal message don't - // get the 'chat-msg--followup' class. - message = 'This a normal message'; - await test_utils.sendMessage(view, message); - let message_el = view.el.querySelector('.message:last-child'); - expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy(); - message = '/me wrote a 3rd person message'; - await test_utils.sendMessage(view, message); - message_el = view.el.querySelector('.message:last-child'); - expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(3); - expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe('wrote a 3rd person message'); - expect(u.isVisible(sizzle('.chat-msg__author:last', view.el).pop())).toBeTruthy(); - expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy(); - done(); - })); + await _converse.handleMessageStanza(msg); + const view = _converse.chatboxviews.get(sender_jid); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1); + expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Mercutio')).toBeTruthy(); + expect(view.el.querySelector('.chat-msg__text').textContent).toBe('is tired'); + message = '/me is as well'; + await mock.sendMessage(view, message); + expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(2); + await u.waitUntil(() => sizzle('.chat-msg__author:last', view.el).pop().textContent.trim() === '**Romeo Montague'); + const last_el = sizzle('.chat-msg__text:last', view.el).pop(); + expect(last_el.textContent).toBe('is as well'); + expect(u.hasClass('chat-msg--followup', last_el)).toBe(false); + // Check that /me messages after a normal message don't + // get the 'chat-msg--followup' class. + message = 'This a normal message'; + await mock.sendMessage(view, message); + let message_el = view.el.querySelector('.message:last-child'); + expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy(); + message = '/me wrote a 3rd person message'; + await mock.sendMessage(view, message); + message_el = view.el.querySelector('.message:last-child'); + expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(3); + expect(sizzle('.chat-msg__text:last', view.el).pop().textContent).toBe('wrote a 3rd person message'); + expect(u.isVisible(sizzle('.chat-msg__author:last', view.el).pop())).toBeTruthy(); + expect(u.hasClass('chat-msg--followup', message_el)).toBeFalsy(); + done(); + })); - it("is created when you click on a roster item", mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - // openControlBox was called earlier, so the controlbox is - // visible, but no other chat boxes have been created. - expect(_converse.chatboxes.length).toEqual(1); - spyOn(_converse.chatboxviews, 'trimChats'); - expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open - - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length, 700); - const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat'); - expect(online_contacts.length).toBe(17); - let el = online_contacts[0]; - el.click(); - await u.waitUntil(() => document.querySelectorAll("#conversejs .chatbox").length == 2); - expect(_converse.chatboxviews.trimChats).toHaveBeenCalled(); - online_contacts[1].click(); - await u.waitUntil(() => _converse.chatboxes.length == 3); - el = online_contacts[1]; - expect(_converse.chatboxviews.trimChats).toHaveBeenCalled(); - // Check that new chat boxes are created to the left of the - // controlbox (but to the right of all existing chat boxes) - expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(3); - done(); - })); - - it("opens when a new message is received", mock.initConverse( - ['rosterGroupsFetched'], {'allow_non_roster_messaging': true}, + it("is created when you click on a roster item", mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 0); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const stanza = u.toStanza(` - - Hey\nHave you heard the news? - `); + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); - const message_promise = new Promise(resolve => _converse.api.listen.on('message', resolve)); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve)); - await u.waitUntil(() => message_promise); - expect(_converse.chatboxviews.keys().length).toBe(2); - done(); - })); + // openControlBox was called earlier, so the controlbox is + // visible, but no other chat boxes have been created. + expect(_converse.chatboxes.length).toEqual(1); + spyOn(_converse.chatboxviews, 'trimChats'); + expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open - it("doesn't open when a message without body is received", mock.initConverse( + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length, 700); + const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat'); + expect(online_contacts.length).toBe(17); + let el = online_contacts[0]; + el.click(); + await u.waitUntil(() => document.querySelectorAll("#conversejs .chatbox").length == 2); + expect(_converse.chatboxviews.trimChats).toHaveBeenCalled(); + online_contacts[1].click(); + await u.waitUntil(() => _converse.chatboxes.length == 3); + el = online_contacts[1]; + expect(_converse.chatboxviews.trimChats).toHaveBeenCalled(); + // Check that new chat boxes are created to the left of the + // controlbox (but to the right of all existing chat boxes) + expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(3); + done(); + })); + + it("opens when a new message is received", mock.initConverse( + ['rosterGroupsFetched'], {'allow_non_roster_messaging': true}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 0); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const stanza = u.toStanza(` + + Hey\nHave you heard the news? + `); + + const message_promise = new Promise(resolve => _converse.api.listen.on('message', resolve)); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve)); + await u.waitUntil(() => message_promise); + expect(_converse.chatboxviews.keys().length).toBe(2); + done(); + })); + + it("doesn't open when a message without body is received", mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const stanza = u.toStanza(` + + + `); + const message_promise = new Promise(resolve => _converse.api.listen.on('message', resolve)) + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => message_promise); + expect(_converse.chatboxviews.keys().length).toBe(1); + done(); + })); + + it("can be trimmed to conserve space", + mock.initConverse(['rosterGroupsFetched'], {}, + async function (done, _converse) { + + spyOn(_converse.chatboxviews, 'trimChats'); + + const trimmed_chatboxes = _converse.minimized_chats; + spyOn(trimmed_chatboxes, 'addChat').and.callThrough(); + spyOn(trimmed_chatboxes, 'removeChat').and.callThrough(); + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + expect(_converse.chatboxviews.trimChats.calls.count()).toBe(1); + + let jid, chatboxview; + // openControlBox was called earlier, so the controlbox is + // visible, but no other chat boxes have been created. + expect(_converse.chatboxes.length).toEqual(1); + expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open + + _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attached. + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length); + // Test that they can be maximized again + const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat'); + expect(online_contacts.length).toBe(17); + let i; + for (i=0; i _converse.chatboxes.length == 16); + expect(_converse.chatboxviews.trimChats.calls.count()).toBe(16); + + _converse.api.chatviews.get().forEach(v => spyOn(v, 'onMinimized').and.callThrough()); + for (i=0; i _converse.chatboxviews.keys().length); + var key = _converse.chatboxviews.keys()[1]; + const trimmedview = trimmed_chatboxes.get(key); + const chatbox = trimmedview.model; + spyOn(chatbox, 'maximize').and.callThrough(); + spyOn(trimmedview, 'restore').and.callThrough(); + trimmedview.delegateEvents(); + trimmedview.el.querySelector("a.restore-chat").click(); + + expect(trimmedview.restore).toHaveBeenCalled(); + expect(chatbox.maximize).toHaveBeenCalled(); + expect(_converse.chatboxviews.trimChats.calls.count()).toBe(17); + done(); + })); + + it("can be opened in minimized mode initially", + mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const stanza = u.toStanza(` - - - `); - const message_promise = new Promise(resolve => _converse.api.listen.on('message', resolve)) - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => message_promise); + await mock.waitForRoster(_converse, 'current'); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await _converse.api.chats.create(sender_jid, {'minimized': true}); + await u.waitUntil(() => _converse.chatboxes.length > 1); + const chatBoxView = _converse.chatboxviews.get(sender_jid); + expect(u.isVisible(chatBoxView.el)).toBeFalsy(); + + const minimized_chat = _converse.minimized_chats.get(sender_jid); + expect(minimized_chat).toBeTruthy(); + expect(u.isVisible(minimized_chat.el)).toBeTruthy(); + done(); + })); + + + it("is focused if its already open and you click on its corresponding roster item", + mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + expect(_converse.chatboxes.length).toEqual(1); + + const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const view = await mock.openChatBoxFor(_converse, contact_jid); + const el = sizzle('a.open-chat:contains("'+view.model.getDisplayName()+'")', _converse.rosterview.el).pop(); + await u.waitUntil(() => u.isVisible(el)); + const textarea = view.el.querySelector('.chat-textarea'); + await u.waitUntil(() => u.isVisible(textarea)); + textarea.blur(); + spyOn(view.model, 'maybeShow').and.callThrough(); + spyOn(view, 'focus').and.callThrough(); + el.click(); + await u.waitUntil(() => view.model.maybeShow.calls.count(), 1000); + expect(view.model.maybeShow).toHaveBeenCalled(); + expect(view.focus).toHaveBeenCalled(); + expect(_converse.chatboxes.length).toEqual(2); + done(); + })); + + it("can be saved to, and retrieved from, browserStorage", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + spyOn(_converse.ChatBoxViews.prototype, 'trimChats'); + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + spyOn(_converse.api, "trigger").and.callThrough(); + + mock.openChatBoxes(_converse, 6); + await u.waitUntil(() => _converse.chatboxes.length == 7); + expect(_converse.chatboxviews.trimChats).toHaveBeenCalled(); + // We instantiate a new ChatBoxes collection, which by default + // will be empty. + const newchatboxes = new _converse.ChatBoxes(); + expect(newchatboxes.length).toEqual(0); + // The chatboxes will then be fetched from browserStorage inside the + // onConnected method + newchatboxes.onConnected(); + await new Promise(resolve => _converse.api.listen.on('chatBoxesFetched', resolve)); + expect(newchatboxes.length).toEqual(7); + // Check that the chatboxes items retrieved from browserStorage + // have the same attributes values as the original ones. + const attrs = ['id', 'box_id', 'visible']; + let new_attrs, old_attrs; + for (var i=0; i _converse.rosterview.el.querySelectorAll('.roster-group').length); + await mock.openChatBoxFor(_converse, contact_jid); + const controlview = _converse.chatboxviews.get('controlbox'), // The controlbox is currently open + chatview = _converse.chatboxviews.get(contact_jid); + + spyOn(chatview, 'close').and.callThrough(); + spyOn(controlview, 'close').and.callThrough(); + spyOn(_converse.api, "trigger").and.callThrough(); + + // We need to rebind all events otherwise our spy won't be called + controlview.delegateEvents(); + chatview.delegateEvents(); + + controlview.el.querySelector('.close-chatbox-button').click(); + expect(controlview.close).toHaveBeenCalled(); + await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve)); + expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); + + chatview.el.querySelector('.close-chatbox-button').click(); + expect(chatview.close).toHaveBeenCalled(); + await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve)); + expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); + done(); + })); + + it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + await mock.openChatBoxFor(_converse, contact_jid); + const trimmed_chatboxes = _converse.minimized_chats; + const chatview = _converse.chatboxviews.get(contact_jid); + spyOn(chatview, 'minimize').and.callThrough(); + spyOn(_converse.api, "trigger").and.callThrough(); + // We need to rebind all events otherwise our spy won't be called + chatview.delegateEvents(); + chatview.el.querySelector('.toggle-chatbox-button').click(); + + expect(chatview.minimize).toHaveBeenCalled(); + expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object)); + expect(_converse.api.trigger.calls.count(), 2); + expect(u.isVisible(chatview.el)).toBeFalsy(); + expect(chatview.model.get('minimized')).toBeTruthy(); + chatview.el.querySelector('.toggle-chatbox-button').click(); + const trimmedview = trimmed_chatboxes.get(chatview.model.get('id')); + spyOn(trimmedview, 'restore').and.callThrough(); + trimmedview.delegateEvents(); + trimmedview.el.querySelector("a.restore-chat").click(); + + expect(trimmedview.restore).toHaveBeenCalled(); + expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object)); + expect(chatview.model.get('minimized')).toBeFalsy(); + done(); + })); + + it("will be removed from browserStorage when closed", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + spyOn(_converse.ChatBoxViews.prototype, 'trimChats'); + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + spyOn(_converse.api, "trigger").and.callThrough(); + + mock.closeControlBox(); + await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve)); + expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); + expect(_converse.chatboxes.length).toEqual(1); + expect(_converse.chatboxes.pluck('id')).toEqual(['controlbox']); + mock.openChatBoxes(_converse, 6); + await u.waitUntil(() => _converse.chatboxes.length == 7) + expect(_converse.chatboxviews.trimChats).toHaveBeenCalled(); + expect(_converse.chatboxes.length).toEqual(7); + expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxViewInitialized', jasmine.any(Object)); + await mock.closeAllChatBoxes(_converse); + + expect(_converse.chatboxes.length).toEqual(1); + expect(_converse.chatboxes.pluck('id')).toEqual(['controlbox']); + expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); + const newchatboxes = new _converse.ChatBoxes(); + expect(newchatboxes.length).toEqual(0); + expect(_converse.chatboxes.pluck('id')).toEqual(['controlbox']); + // onConnected will fetch chatboxes in browserStorage, but + // because there aren't any open chatboxes, there won't be any + // in browserStorage either. XXX except for the controlbox + newchatboxes.onConnected(); + await new Promise(resolve => _converse.api.listen.on('chatBoxesFetched', resolve)); + expect(newchatboxes.length).toEqual(1); + expect(newchatboxes.models[0].id).toBe("controlbox"); + done(); + })); + + describe("A chat toolbar", function () { + + it("can be found on each chat box", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 3); + await mock.openControlBox(_converse); + const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const chatbox = _converse.chatboxes.get(contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + expect(chatbox).toBeDefined(); + expect(view).toBeDefined(); + const toolbar = view.el.querySelector('ul.chat-toolbar'); + expect(_.isElement(toolbar)).toBe(true); + expect(toolbar.querySelectorAll(':scope > li').length).toBe(2); + done(); + })); + + it("shows the remaining character count if a message_limit is configured", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 200}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 3); + await mock.openControlBox(_converse); + const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + const toolbar = view.el.querySelector('.chat-toolbar'); + const counter = toolbar.querySelector('.message-limit'); + expect(counter.textContent).toBe('200'); + view.insertIntoTextArea('hello world'); + expect(counter.textContent).toBe('188'); + + toolbar.querySelector('a.toggle-smiley').click(); + const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__lists')); + const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a')); + item.click() + expect(counter.textContent).toBe('179'); + + const textarea = view.el.querySelector('.chat-textarea'); + const ev = { + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }; + view.onKeyDown(ev); + await new Promise(resolve => view.once('messageInserted', resolve)); + view.onKeyUp(ev); + expect(counter.textContent).toBe('200'); + + textarea.value = 'hello world'; + view.onKeyUp(ev); + expect(counter.textContent).toBe('189'); + done(); + })); + + + it("does not show a remaining character count if message_limit is zero", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 0}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 3); + await mock.openControlBox(_converse); + const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + const counter = view.el.querySelector('.chat-toolbar .message-limit'); + expect(counter).toBe(null); + done(); + })); + + + it("can contain a button for starting a call", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + let toolbar, call_button; + const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + spyOn(_converse.api, "trigger").and.callThrough(); + // First check that the button doesn't show if it's not enabled + // via "visible_toolbar_buttons" + _converse.visible_toolbar_buttons.call = false; + await mock.openChatBoxFor(_converse, contact_jid); + let view = _converse.chatboxviews.get(contact_jid); + toolbar = view.el.querySelector('ul.chat-toolbar'); + call_button = toolbar.querySelector('.toggle-call'); + expect(call_button === null).toBeTruthy(); + view.close(); + // Now check that it's shown if enabled and that it emits + // callButtonClicked + _converse.visible_toolbar_buttons.call = true; // enable the button + await mock.openChatBoxFor(_converse, contact_jid); + view = _converse.chatboxviews.get(contact_jid); + toolbar = view.el.querySelector('ul.chat-toolbar'); + call_button = toolbar.querySelector('.toggle-call'); + call_button.click(); + expect(_converse.api.trigger).toHaveBeenCalledWith('callButtonClicked', jasmine.any(Object)); + done(); + })); + }); + + describe("A Chat Status Notification", function () { + + it("does not open a new chatbox", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + // state + const stanza = $msg({ + 'from': sender_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': u.getUniqueId() + }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + + spyOn(_converse.api, "trigger").and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.api.trigger.calls.count()); + expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); expect(_converse.chatboxviews.keys().length).toBe(1); done(); })); - it("can be trimmed to conserve space", - mock.initConverse(['rosterGroupsFetched'], {}, - async function (done, _converse) { + describe("An active notification", function () { - spyOn(_converse.chatboxviews, 'trimChats'); - - const trimmed_chatboxes = _converse.minimized_chats; - spyOn(trimmed_chatboxes, 'addChat').and.callThrough(); - spyOn(trimmed_chatboxes, 'removeChat').and.callThrough(); - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - expect(_converse.chatboxviews.trimChats.calls.count()).toBe(1); - - let jid, chatboxview; - // openControlBox was called earlier, so the controlbox is - // visible, but no other chat boxes have been created. - expect(_converse.chatboxes.length).toEqual(1); - expect(document.querySelectorAll("#conversejs .chatbox").length).toBe(1); // Controlbox is open - - _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attached. - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length); - // Test that they can be maximized again - const online_contacts = _converse.rosterview.el.querySelectorAll('.roster-group .current-xmpp-contact a.open-chat'); - expect(online_contacts.length).toBe(17); - let i; - for (i=0; i _converse.chatboxes.length == 16); - expect(_converse.chatboxviews.trimChats.calls.count()).toBe(16); - - _converse.api.chatviews.get().forEach(v => spyOn(v, 'onMinimized').and.callThrough()); - for (i=0; i _converse.chatboxviews.keys().length); - var key = _converse.chatboxviews.keys()[1]; - const trimmedview = trimmed_chatboxes.get(key); - const chatbox = trimmedview.model; - spyOn(chatbox, 'maximize').and.callThrough(); - spyOn(trimmedview, 'restore').and.callThrough(); - trimmedview.delegateEvents(); - trimmedview.el.querySelector("a.restore-chat").click(); - - expect(trimmedview.restore).toHaveBeenCalled(); - expect(chatbox.maximize).toHaveBeenCalled(); - expect(_converse.chatboxviews.trimChats.calls.count()).toBe(17); - done(); - })); - - it("can be opened in minimized mode initially", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await _converse.api.chats.create(sender_jid, {'minimized': true}); - await u.waitUntil(() => _converse.chatboxes.length > 1); - const chatBoxView = _converse.chatboxviews.get(sender_jid); - expect(u.isVisible(chatBoxView.el)).toBeFalsy(); - - const minimized_chat = _converse.minimized_chats.get(sender_jid); - expect(minimized_chat).toBeTruthy(); - expect(u.isVisible(minimized_chat.el)).toBeTruthy(); - done(); - })); - - - it("is focused if its already open and you click on its corresponding roster item", - mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - expect(_converse.chatboxes.length).toEqual(1); - - const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const view = await test_utils.openChatBoxFor(_converse, contact_jid); - const el = sizzle('a.open-chat:contains("'+view.model.getDisplayName()+'")', _converse.rosterview.el).pop(); - await u.waitUntil(() => u.isVisible(el)); - const textarea = view.el.querySelector('.chat-textarea'); - await u.waitUntil(() => u.isVisible(textarea)); - textarea.blur(); - spyOn(view.model, 'maybeShow').and.callThrough(); - spyOn(view, 'focus').and.callThrough(); - el.click(); - await u.waitUntil(() => view.model.maybeShow.calls.count(), 1000); - expect(view.model.maybeShow).toHaveBeenCalled(); - expect(view.focus).toHaveBeenCalled(); - expect(_converse.chatboxes.length).toEqual(2); - done(); - })); - - it("can be saved to, and retrieved from, browserStorage", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - spyOn(_converse.ChatBoxViews.prototype, 'trimChats'); - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - spyOn(_converse.api, "trigger").and.callThrough(); - - test_utils.openChatBoxes(_converse, 6); - await u.waitUntil(() => _converse.chatboxes.length == 7); - expect(_converse.chatboxviews.trimChats).toHaveBeenCalled(); - // We instantiate a new ChatBoxes collection, which by default - // will be empty. - const newchatboxes = new _converse.ChatBoxes(); - expect(newchatboxes.length).toEqual(0); - // The chatboxes will then be fetched from browserStorage inside the - // onConnected method - newchatboxes.onConnected(); - await new Promise(resolve => _converse.api.listen.on('chatBoxesFetched', resolve)); - expect(newchatboxes.length).toEqual(7); - // Check that the chatboxes items retrieved from browserStorage - // have the same attributes values as the original ones. - const attrs = ['id', 'box_id', 'visible']; - let new_attrs, old_attrs; - for (var i=0; i _converse.rosterview.el.querySelectorAll('.roster-group').length); - await test_utils.openChatBoxFor(_converse, contact_jid); - const controlview = _converse.chatboxviews.get('controlbox'), // The controlbox is currently open - chatview = _converse.chatboxviews.get(contact_jid); - - spyOn(chatview, 'close').and.callThrough(); - spyOn(controlview, 'close').and.callThrough(); - spyOn(_converse.api, "trigger").and.callThrough(); - - // We need to rebind all events otherwise our spy won't be called - controlview.delegateEvents(); - chatview.delegateEvents(); - - controlview.el.querySelector('.close-chatbox-button').click(); - expect(controlview.close).toHaveBeenCalled(); - await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve)); - expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); - - chatview.el.querySelector('.close-chatbox-button').click(); - expect(chatview.close).toHaveBeenCalled(); - await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve)); - expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); - done(); - })); - - it("can be minimized by clicking a DOM element with class 'toggle-chatbox-button'", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - const contact_jid = mock.cur_names[7].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - await test_utils.openChatBoxFor(_converse, contact_jid); - const trimmed_chatboxes = _converse.minimized_chats; - const chatview = _converse.chatboxviews.get(contact_jid); - spyOn(chatview, 'minimize').and.callThrough(); - spyOn(_converse.api, "trigger").and.callThrough(); - // We need to rebind all events otherwise our spy won't be called - chatview.delegateEvents(); - chatview.el.querySelector('.toggle-chatbox-button').click(); - - expect(chatview.minimize).toHaveBeenCalled(); - expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object)); - expect(_converse.api.trigger.calls.count(), 2); - expect(u.isVisible(chatview.el)).toBeFalsy(); - expect(chatview.model.get('minimized')).toBeTruthy(); - chatview.el.querySelector('.toggle-chatbox-button').click(); - const trimmedview = trimmed_chatboxes.get(chatview.model.get('id')); - spyOn(trimmedview, 'restore').and.callThrough(); - trimmedview.delegateEvents(); - trimmedview.el.querySelector("a.restore-chat").click(); - - expect(trimmedview.restore).toHaveBeenCalled(); - expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object)); - expect(chatview.model.get('minimized')).toBeFalsy(); - done(); - })); - - it("will be removed from browserStorage when closed", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - spyOn(_converse.ChatBoxViews.prototype, 'trimChats'); - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - spyOn(_converse.api, "trigger").and.callThrough(); - - test_utils.closeControlBox(); - await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve)); - expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); - expect(_converse.chatboxes.length).toEqual(1); - expect(_converse.chatboxes.pluck('id')).toEqual(['controlbox']); - test_utils.openChatBoxes(_converse, 6); - await u.waitUntil(() => _converse.chatboxes.length == 7) - expect(_converse.chatboxviews.trimChats).toHaveBeenCalled(); - expect(_converse.chatboxes.length).toEqual(7); - expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxViewInitialized', jasmine.any(Object)); - await test_utils.closeAllChatBoxes(_converse); - - expect(_converse.chatboxes.length).toEqual(1); - expect(_converse.chatboxes.pluck('id')).toEqual(['controlbox']); - expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); - const newchatboxes = new _converse.ChatBoxes(); - expect(newchatboxes.length).toEqual(0); - expect(_converse.chatboxes.pluck('id')).toEqual(['controlbox']); - // onConnected will fetch chatboxes in browserStorage, but - // because there aren't any open chatboxes, there won't be any - // in browserStorage either. XXX except for the controlbox - newchatboxes.onConnected(); - await new Promise(resolve => _converse.api.listen.on('chatBoxesFetched', resolve)); - expect(newchatboxes.length).toEqual(1); - expect(newchatboxes.models[0].id).toBe("controlbox"); - done(); - })); - - describe("A chat toolbar", function () { - - it("can be found on each chat box", + it("is sent when the user opens a chat box", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 3); - await test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const chatbox = _converse.chatboxes.get(contact_jid); + await mock.waitForRoster(_converse, 'current'); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openControlBox(_converse); + u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + spyOn(_converse.connection, 'send'); + await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.chatboxviews.get(contact_jid); - expect(chatbox).toBeDefined(); - expect(view).toBeDefined(); - const toolbar = view.el.querySelector('ul.chat-toolbar'); - expect(_.isElement(toolbar)).toBe(true); - expect(toolbar.querySelectorAll(':scope > li').length).toBe(2); + expect(view.model.get('chat_state')).toBe('active'); + expect(_converse.connection.send).toHaveBeenCalled(); + const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); + expect(stanza.getAttribute('to')).toBe(contact_jid); + expect(stanza.childNodes.length).toBe(3); + expect(stanza.childNodes[0].tagName).toBe('active'); + expect(stanza.childNodes[1].tagName).toBe('no-store'); + expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); done(); })); - it("shows the remaining character count if a message_limit is configured", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 200}, - async function (done, _converse) { + it("is sent when the user maximizes a minimized a chat box", mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 3); - await test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); + await mock.waitForRoster(_converse, 'current', 1); + await mock.openControlBox(_converse); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.chatboxviews.get(contact_jid); - const toolbar = view.el.querySelector('.chat-toolbar'); - const counter = toolbar.querySelector('.message-limit'); - expect(counter.textContent).toBe('200'); - view.insertIntoTextArea('hello world'); - expect(counter.textContent).toBe('188'); - - toolbar.querySelector('a.toggle-smiley').click(); - const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__lists')); - const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a')); - item.click() - expect(counter.textContent).toBe('179'); - - const textarea = view.el.querySelector('.chat-textarea'); - const ev = { - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }; - view.onKeyDown(ev); - await new Promise(resolve => view.once('messageInserted', resolve)); - view.onKeyUp(ev); - expect(counter.textContent).toBe('200'); - - textarea.value = 'hello world'; - view.onKeyUp(ev); - expect(counter.textContent).toBe('189'); - done(); - })); - - - it("does not show a remaining character count if message_limit is zero", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 0}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 3); - await test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - const counter = view.el.querySelector('.chat-toolbar .message-limit'); - expect(counter).toBe(null); - done(); - })); - - - it("can contain a button for starting a call", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - let toolbar, call_button; - const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - spyOn(_converse.api, "trigger").and.callThrough(); - // First check that the button doesn't show if it's not enabled - // via "visible_toolbar_buttons" - _converse.visible_toolbar_buttons.call = false; - await test_utils.openChatBoxFor(_converse, contact_jid); - let view = _converse.chatboxviews.get(contact_jid); - toolbar = view.el.querySelector('ul.chat-toolbar'); - call_button = toolbar.querySelector('.toggle-call'); - expect(call_button === null).toBeTruthy(); - view.close(); - // Now check that it's shown if enabled and that it emits - // callButtonClicked - _converse.visible_toolbar_buttons.call = true; // enable the button - await test_utils.openChatBoxFor(_converse, contact_jid); - view = _converse.chatboxviews.get(contact_jid); - toolbar = view.el.querySelector('ul.chat-toolbar'); - call_button = toolbar.querySelector('.toggle-call'); - call_button.click(); - expect(_converse.api.trigger).toHaveBeenCalledWith('callButtonClicked', jasmine.any(Object)); + view.model.minimize(); + expect(view.model.get('chat_state')).toBe('inactive'); + spyOn(_converse.connection, 'send'); + view.model.maximize(); + await u.waitUntil(() => view.model.get('chat_state') === 'active', 1000); + expect(_converse.connection.send).toHaveBeenCalled(); + const calls = _.filter(_converse.connection.send.calls.all(), function (call) { + return call.args[0] instanceof Strophe.Builder; + }); + expect(calls.length).toBe(1); + const stanza = calls[0].args[0].tree(); + expect(stanza.getAttribute('to')).toBe(contact_jid); + expect(stanza.childNodes.length).toBe(3); + expect(stanza.childNodes[0].tagName).toBe('active'); + expect(stanza.childNodes[1].tagName).toBe('no-store'); + expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); done(); })); }); - describe("A Chat Status Notification", function () { + describe("A composing notification", function () { - it("does not open a new chatbox", + it("is sent as soon as the user starts typing a message which is not a command", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + await mock.openChatBoxFor(_converse, contact_jid); + var view = _converse.chatboxviews.get(contact_jid); + expect(view.model.get('chat_state')).toBe('active'); + spyOn(_converse.connection, 'send'); + spyOn(_converse.api, "trigger").and.callThrough(); + view.onKeyDown({ + target: view.el.querySelector('textarea.chat-textarea'), + keyCode: 1 + }); + expect(view.model.get('chat_state')).toBe('composing'); + expect(_converse.connection.send).toHaveBeenCalled(); + + const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); + expect(stanza.getAttribute('to')).toBe(contact_jid); + expect(stanza.childNodes.length).toBe(3); + expect(stanza.childNodes[0].tagName).toBe('composing'); + expect(stanza.childNodes[1].tagName).toBe('no-store'); + expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); + + // The notification is not sent again + view.onKeyDown({ + target: view.el.querySelector('textarea.chat-textarea'), + keyCode: 1 + }); + expect(view.model.get('chat_state')).toBe('composing'); + expect(_converse.api.trigger.calls.count(), 1); + done(); + })); + + it("is NOT sent out if send_chat_state_notifications doesn't allow it", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'send_chat_state_notifications': []}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + await mock.openChatBoxFor(_converse, contact_jid); + var view = _converse.chatboxviews.get(contact_jid); + expect(view.model.get('chat_state')).toBe('active'); + spyOn(_converse.connection, 'send'); + spyOn(_converse.api, "trigger").and.callThrough(); + view.onKeyDown({ + target: view.el.querySelector('textarea.chat-textarea'), + keyCode: 1 + }); + expect(view.model.get('chat_state')).toBe('composing'); + expect(_converse.connection.send).not.toHaveBeenCalled(); + done(); + })); + + it("will be shown if received", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + await mock.openChatBoxFor(_converse, sender_jid); + // state - const stanza = $msg({ - 'from': sender_jid, + let msg = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + + _converse.connection._dataRecv(mock.createRequest(msg)); + const view = _converse.chatboxviews.get(sender_jid); + let csn = mock.cur_names[1] + ' is typing'; + await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn); + expect(view.model.messages.length).toEqual(0); + + // state + msg = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); + csn = mock.cur_names[1] + ' has stopped typing'; + await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn); + + msg = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('body').t('hello world').tree(); + await _converse.handleMessageStanza(msg); + const msg_el = await u.waitUntil(() => view.content.querySelector('.chat-msg')); + await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === ''); + expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('hello world'); + done(); + })); + + it("is ignored if it's a composing carbon message sent by this user from a different client", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); + await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); + await mock.waitForRoster(_converse, 'current'); + // Send a message from a different resource + const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const view = await mock.openChatBoxFor(_converse, recipient_jid); + + spyOn(u, 'shouldCreateMessage').and.callThrough(); + + const msg = $msg({ + 'from': _converse.bare_jid, + 'id': u.getUniqueId(), 'to': _converse.connection.jid, 'type': 'chat', - 'id': u.getUniqueId() + 'xmlns': 'jabber:client' + }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('message', { + 'xmlns': 'jabber:client', + 'from': _converse.bare_jid+'/another-resource', + 'to': recipient_jid, + 'type': 'chat' }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); - spyOn(_converse.api, "trigger").and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.api.trigger.calls.count()); - expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); - expect(_converse.chatboxviews.keys().length).toBe(1); + await u.waitUntil(() => u.shouldCreateMessage.calls.count()); + expect(view.model.messages.length).toEqual(0); + const el = view.el.querySelector('.chat-content__notifications'); + expect(el.textContent).toBe(''); done(); })); + }); - describe("An active notification", function () { + describe("A paused notification", function () { - it("is sent when the user opens a chat box", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openControlBox(_converse); - u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - spyOn(_converse.connection, 'send'); - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - expect(view.model.get('chat_state')).toBe('active'); - expect(_converse.connection.send).toHaveBeenCalled(); - const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); - expect(stanza.getAttribute('to')).toBe(contact_jid); - expect(stanza.childNodes.length).toBe(3); - expect(stanza.childNodes[0].tagName).toBe('active'); - expect(stanza.childNodes[1].tagName).toBe('no-store'); - expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); - done(); - })); - - it("is sent when the user maximizes a minimized a chat box", mock.initConverse( + it("is sent if the user has stopped typing since 30 seconds", + mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 1); - await test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.waitForRoster(_converse, 'current'); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openControlBox(_converse); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length, 700); + _converse.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + spyOn(_converse.connection, 'send'); + spyOn(view.model, 'setChatState').and.callThrough(); + expect(view.model.get('chat_state')).toBe('active'); + view.onKeyDown({ + target: view.el.querySelector('textarea.chat-textarea'), + keyCode: 1 + }); + expect(view.model.get('chat_state')).toBe('composing'); + expect(_converse.connection.send).toHaveBeenCalled(); + let stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); + expect(stanza.childNodes[0].tagName).toBe('composing'); + await u.waitUntil(() => view.model.get('chat_state') === 'paused', 500); + expect(_converse.connection.send).toHaveBeenCalled(); + var calls = _.filter(_converse.connection.send.calls.all(), function (call) { + return call.args[0] instanceof Strophe.Builder; + }); + expect(calls.length).toBe(2); + stanza = calls[1].args[0].tree(); + expect(stanza.getAttribute('to')).toBe(contact_jid); + expect(stanza.childNodes.length).toBe(3); + expect(stanza.childNodes[0].tagName).toBe('paused'); + expect(stanza.childNodes[1].tagName).toBe('no-store'); + expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - view.model.minimize(); - expect(view.model.get('chat_state')).toBe('inactive'); - spyOn(_converse.connection, 'send'); - view.model.maximize(); - await u.waitUntil(() => view.model.get('chat_state') === 'active', 1000); - expect(_converse.connection.send).toHaveBeenCalled(); - const calls = _.filter(_converse.connection.send.calls.all(), function (call) { - return call.args[0] instanceof Strophe.Builder; - }); - expect(calls.length).toBe(1); - const stanza = calls[0].args[0].tree(); - expect(stanza.getAttribute('to')).toBe(contact_jid); - expect(stanza.childNodes.length).toBe(3); - expect(stanza.childNodes[0].tagName).toBe('active'); - expect(stanza.childNodes[1].tagName).toBe('no-store'); - expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); - done(); - })); - }); + // Test #359. A paused notification should not be sent + // out if the user simply types longer than the + // timeout. + view.onKeyDown({ + target: view.el.querySelector('textarea.chat-textarea'), + keyCode: 1 + }); + expect(view.model.setChatState).toHaveBeenCalled(); + expect(view.model.get('chat_state')).toBe('composing'); - describe("A composing notification", function () { + view.onKeyDown({ + target: view.el.querySelector('textarea.chat-textarea'), + keyCode: 1 + }); + expect(view.model.get('chat_state')).toBe('composing'); + done(); + })); - it("is sent as soon as the user starts typing a message which is not a command", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - await test_utils.openChatBoxFor(_converse, contact_jid); - var view = _converse.chatboxviews.get(contact_jid); - expect(view.model.get('chat_state')).toBe('active'); - spyOn(_converse.connection, 'send'); - spyOn(_converse.api, "trigger").and.callThrough(); - view.onKeyDown({ - target: view.el.querySelector('textarea.chat-textarea'), - keyCode: 1 - }); - expect(view.model.get('chat_state')).toBe('composing'); - expect(_converse.connection.send).toHaveBeenCalled(); - - const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); - expect(stanza.getAttribute('to')).toBe(contact_jid); - expect(stanza.childNodes.length).toBe(3); - expect(stanza.childNodes[0].tagName).toBe('composing'); - expect(stanza.childNodes[1].tagName).toBe('no-store'); - expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); - - // The notification is not sent again - view.onKeyDown({ - target: view.el.querySelector('textarea.chat-textarea'), - keyCode: 1 - }); - expect(view.model.get('chat_state')).toBe('composing'); - expect(_converse.api.trigger.calls.count(), 1); - done(); - })); - - it("is NOT sent out if send_chat_state_notifications doesn't allow it", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'send_chat_state_notifications': []}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - await test_utils.openChatBoxFor(_converse, contact_jid); - var view = _converse.chatboxviews.get(contact_jid); - expect(view.model.get('chat_state')).toBe('active'); - spyOn(_converse.connection, 'send'); - spyOn(_converse.api, "trigger").and.callThrough(); - view.onKeyDown({ - target: view.el.querySelector('textarea.chat-textarea'), - keyCode: 1 - }); - expect(view.model.get('chat_state')).toBe('composing'); - expect(_converse.connection.send).not.toHaveBeenCalled(); - done(); - })); - - it("will be shown if received", + it("will be shown if received", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions - const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - await test_utils.openChatBoxFor(_converse, sender_jid); - - // state - let msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - - _converse.connection._dataRecv(test_utils.createRequest(msg)); - const view = _converse.chatboxviews.get(sender_jid); - let csn = mock.cur_names[1] + ' is typing'; - await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn); - expect(view.model.messages.length).toEqual(0); - - // state - msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - csn = mock.cur_names[1] + ' has stopped typing'; - await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn); - - msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t('hello world').tree(); - await _converse.handleMessageStanza(msg); - const msg_el = await u.waitUntil(() => view.content.querySelector('.chat-msg')); - await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === ''); - expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('hello world'); - done(); - })); - - it("is ignored if it's a composing carbon message sent by this user from a different client", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); - await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); - await test_utils.waitForRoster(_converse, 'current'); - // Send a message from a different resource - const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const view = await test_utils.openChatBoxFor(_converse, recipient_jid); - - spyOn(u, 'shouldCreateMessage').and.callThrough(); - - const msg = $msg({ - 'from': _converse.bare_jid, - 'id': u.getUniqueId(), - 'to': _converse.connection.jid, - 'type': 'chat', - 'xmlns': 'jabber:client' - }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'}) - .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) - .c('message', { - 'xmlns': 'jabber:client', - 'from': _converse.bare_jid+'/another-resource', - 'to': recipient_jid, - 'type': 'chat' - }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - - await u.waitUntil(() => u.shouldCreateMessage.calls.count()); - expect(view.model.messages.length).toEqual(0); - const el = view.el.querySelector('.chat-content__notifications'); - expect(el.textContent).toBe(''); - done(); - })); - }); - - describe("A paused notification", function () { - - it("is sent if the user has stopped typing since 30 seconds", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openControlBox(_converse); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length, 700); - _converse.TIMEOUTS.PAUSED = 200; // Make the timeout shorter so that we can test - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - spyOn(_converse.connection, 'send'); - spyOn(view.model, 'setChatState').and.callThrough(); - expect(view.model.get('chat_state')).toBe('active'); - view.onKeyDown({ - target: view.el.querySelector('textarea.chat-textarea'), - keyCode: 1 - }); - expect(view.model.get('chat_state')).toBe('composing'); - expect(_converse.connection.send).toHaveBeenCalled(); - let stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); - expect(stanza.childNodes[0].tagName).toBe('composing'); - await u.waitUntil(() => view.model.get('chat_state') === 'paused', 500); - expect(_converse.connection.send).toHaveBeenCalled(); - var calls = _.filter(_converse.connection.send.calls.all(), function (call) { - return call.args[0] instanceof Strophe.Builder; - }); - expect(calls.length).toBe(2); - stanza = calls[1].args[0].tree(); - expect(stanza.getAttribute('to')).toBe(contact_jid); - expect(stanza.childNodes.length).toBe(3); - expect(stanza.childNodes[0].tagName).toBe('paused'); - expect(stanza.childNodes[1].tagName).toBe('no-store'); - expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); - - // Test #359. A paused notification should not be sent - // out if the user simply types longer than the - // timeout. - view.onKeyDown({ - target: view.el.querySelector('textarea.chat-textarea'), - keyCode: 1 - }); - expect(view.model.setChatState).toHaveBeenCalled(); - expect(view.model.get('chat_state')).toBe('composing'); - - view.onKeyDown({ - target: view.el.querySelector('textarea.chat-textarea'), - keyCode: 1 - }); - expect(view.model.get('chat_state')).toBe('composing'); - done(); - })); - - it("will be shown if received", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - // TODO: only show paused state if the previous state was composing - // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions - spyOn(_converse.api, "trigger").and.callThrough(); - const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const view = await test_utils.openChatBoxFor(_converse, sender_jid); - // state - const msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - - _converse.connection._dataRecv(test_utils.createRequest(msg)); - const csn = mock.cur_names[1] + ' has stopped typing'; - await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn); - expect(view.model.messages.length).toEqual(0); - done(); - })); - - it("will not be shown if it's a paused carbon message that this user sent from a different client", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); - await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); - await test_utils.waitForRoster(_converse, 'current'); - // Send a message from a different resource - const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - spyOn(u, 'shouldCreateMessage').and.callThrough(); - const view = await test_utils.openChatBoxFor(_converse, recipient_jid); - const msg = $msg({ - 'from': _converse.bare_jid, - 'id': u.getUniqueId(), - 'to': _converse.connection.jid, - 'type': 'chat', - 'xmlns': 'jabber:client' - }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'}) - .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) - .c('message', { - 'xmlns': 'jabber:client', - 'from': _converse.bare_jid+'/another-resource', - 'to': recipient_jid, - 'type': 'chat' - }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - await u.waitUntil(() => u.shouldCreateMessage.calls.count()); - expect(view.model.messages.length).toEqual(0); - const el = view.el.querySelector('.chat-content__notifications'); - expect(el.textContent).toBe(''); - done(); - done(); - })); - }); - - describe("An inactive notifciation", function () { - - it("is sent if the user has stopped typing since 2 minutes", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const sent_stanzas = _converse.connection.sent_stanzas; - // Make the timeouts shorter so that we can test - _converse.TIMEOUTS.PAUSED = 100; - _converse.TIMEOUTS.INACTIVE = 100; - - await test_utils.waitForRoster(_converse, 'current'); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openControlBox(_converse); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 1000); - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - await u.waitUntil(() => view.model.get('chat_state') === 'active'); - let messages = await u.waitUntil(() => sent_stanzas.filter(s => s.matches('message'))); - expect(messages.length).toBe(1); - expect(view.model.get('chat_state')).toBe('active'); - view.onKeyDown({ - target: view.el.querySelector('textarea.chat-textarea'), - keyCode: 1 - }); - await u.waitUntil(() => view.model.get('chat_state') === 'composing', 600); - messages = sent_stanzas.filter(s => s.matches('message')); - expect(messages.length).toBe(2); - - await u.waitUntil(() => view.model.get('chat_state') === 'paused', 600); - messages = sent_stanzas.filter(s => s.matches('message')); - expect(messages.length).toBe(3); - - await u.waitUntil(() => view.model.get('chat_state') === 'inactive', 600); - messages = sent_stanzas.filter(s => s.matches('message')); - expect(messages.length).toBe(4); - - expect(Strophe.serialize(messages[0])).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - expect(Strophe.serialize(messages[1])).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - expect(Strophe.serialize(messages[2])).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - expect(Strophe.serialize(messages[3])).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - done(); - })); - - it("is sent when the user a minimizes a chat box", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - spyOn(_converse.connection, 'send'); - view.minimize(); - expect(view.model.get('chat_state')).toBe('inactive'); - expect(_converse.connection.send).toHaveBeenCalled(); - var stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); - expect(stanza.getAttribute('to')).toBe(contact_jid); - expect(stanza.childNodes[0].tagName).toBe('inactive'); - done(); - })); - - it("is sent if the user closes a chat box", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openControlBox(_converse); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - const view = await test_utils.openChatBoxFor(_converse, contact_jid); - expect(view.model.get('chat_state')).toBe('active'); - spyOn(_converse.connection, 'send'); - view.close(); - expect(view.model.get('chat_state')).toBe('inactive'); - expect(_converse.connection.send).toHaveBeenCalled(); - const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); - expect(stanza.getAttribute('to')).toBe(contact_jid); - expect(stanza.childNodes.length).toBe(3); - expect(stanza.childNodes[0].tagName).toBe('inactive'); - expect(stanza.childNodes[1].tagName).toBe('no-store'); - expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); - done(); - })); - - it("will clear any other chat status notifications", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions - await test_utils.openChatBoxFor(_converse, sender_jid); - const view = _converse.chatboxviews.get(sender_jid); - expect(view.el.querySelectorAll('.chat-event').length).toBe(0); - // Insert message, to also check that - // text messages are inserted correctly with - // temporary chat events in the chat contents. - let msg = $msg({ - 'to': _converse.bare_jid, - 'xmlns': 'jabber:client', - 'from': sender_jid, - 'type': 'chat'}) - .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up() - .tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext).toEqual(mock.cur_names[1] + ' is typing'); - expect(view.model.messages.length).toBe(0); - - msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('inactive', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - - await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent); - done(); - })); - }); - - describe("A gone notifciation", function () { - - it("will be shown if received", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 3); - await test_utils.openControlBox(_converse); - const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, sender_jid); - - const msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').c('gone', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - - const view = _converse.chatboxviews.get(sender_jid); - const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext).toEqual(mock.cur_names[1] + ' has gone away'); - done(); - })); - }); - - describe("On receiving a message correction", function () { - - it("will be removed", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions - const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - await test_utils.openChatBoxFor(_converse, sender_jid); - - // Original message - const original_id = u.getUniqueId(); - const original = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: original_id, - body: "Original message", - }).c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - - spyOn(_converse.api, "trigger").and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(original)); - await u.waitUntil(() => _converse.api.trigger.calls.count()); - expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); - const view = _converse.chatboxviews.get(sender_jid); - expect(view).toBeDefined(); - - // state - const msg = $msg({ + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + // TODO: only show paused state if the previous state was composing + // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions + spyOn(_converse.api, "trigger").and.callThrough(); + const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const view = await mock.openChatBoxFor(_converse, sender_jid); + // state + const msg = $msg({ from: sender_jid, to: _converse.connection.jid, type: 'chat', id: u.getUniqueId() - }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); + }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext).toEqual(mock.cur_names[1] + ' is typing'); + _converse.connection._dataRecv(mock.createRequest(msg)); + const csn = mock.cur_names[1] + ' has stopped typing'; + await u.waitUntil( () => view.el.querySelector('.chat-content__notifications').innerText === csn); + expect(view.model.messages.length).toEqual(0); + done(); + })); - // Edited message - const edited = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId(), - body: "Edited message", - }) - .c('active', {'xmlns': Strophe.NS.CHATSTATES}).up() - .c('replace', {'xmlns': Strophe.NS.MESSAGE_CORRECT, 'id': original_id }).tree(); + it("will not be shown if it's a paused carbon message that this user sent from a different client", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - await _converse.handleMessageStanza(edited); - await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent); - done(); - })); - }); + await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); + await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); + await mock.waitForRoster(_converse, 'current'); + // Send a message from a different resource + const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + spyOn(u, 'shouldCreateMessage').and.callThrough(); + const view = await mock.openChatBoxFor(_converse, recipient_jid); + const msg = $msg({ + 'from': _converse.bare_jid, + 'id': u.getUniqueId(), + 'to': _converse.connection.jid, + 'type': 'chat', + 'xmlns': 'jabber:client' + }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('message', { + 'xmlns': 'jabber:client', + 'from': _converse.bare_jid+'/another-resource', + 'to': recipient_jid, + 'type': 'chat' + }).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); + await u.waitUntil(() => u.shouldCreateMessage.calls.count()); + expect(view.model.messages.length).toEqual(0); + const el = view.el.querySelector('.chat-content__notifications'); + expect(el.textContent).toBe(''); + done(); + done(); + })); }); - }); - describe("Special Messages", function () { + describe("An inactive notifciation", function () { - it("'/clear' can be used to clear messages in a conversation", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("is sent if the user has stopped typing since 2 minutes", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const sent_stanzas = _converse.connection.sent_stanzas; + // Make the timeouts shorter so that we can test + _converse.TIMEOUTS.PAUSED = 100; + _converse.TIMEOUTS.INACTIVE = 100; - spyOn(_converse.api, "trigger").and.callThrough(); - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - let message = 'This message is another sent from this chatbox'; - await test_utils.sendMessage(view, message); + await mock.waitForRoster(_converse, 'current'); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openControlBox(_converse); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 1000); + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + await u.waitUntil(() => view.model.get('chat_state') === 'active'); + let messages = await u.waitUntil(() => sent_stanzas.filter(s => s.matches('message'))); + expect(messages.length).toBe(1); + expect(view.model.get('chat_state')).toBe('active'); + view.onKeyDown({ + target: view.el.querySelector('textarea.chat-textarea'), + keyCode: 1 + }); + await u.waitUntil(() => view.model.get('chat_state') === 'composing', 600); + messages = sent_stanzas.filter(s => s.matches('message')); + expect(messages.length).toBe(2); - expect(view.model.messages.length === 1).toBeTruthy(); - let stored_messages = await view.model.messages.browserStorage.findAll(); - expect(stored_messages.length).toBe(1); - await u.waitUntil(() => view.el.querySelector('.chat-msg')); + await u.waitUntil(() => view.model.get('chat_state') === 'paused', 600); + messages = sent_stanzas.filter(s => s.matches('message')); + expect(messages.length).toBe(3); - message = '/clear'; - spyOn(view, 'clearMessages').and.callThrough(); - spyOn(window, 'confirm').and.callFake(function () { - return true; - }); - view.el.querySelector('.chat-textarea').value = message; - view.onKeyDown({ - target: view.el.querySelector('textarea.chat-textarea'), - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - expect(view.clearMessages.calls.all().length).toBe(1); - await view.clearMessages.calls.all()[0].returnValue; - expect(window.confirm).toHaveBeenCalled(); - expect(view.model.messages.length, 0); // The messages must be removed from the chatbox - stored_messages = await view.model.messages.browserStorage.findAll(); - expect(stored_messages.length).toBe(0); - expect(_converse.api.trigger.calls.count(), 1); - expect(_converse.api.trigger.calls.mostRecent().args, ['messageSend', message]); - done(); - })); - }); + await u.waitUntil(() => view.model.get('chat_state') === 'inactive', 600); + messages = sent_stanzas.filter(s => s.matches('message')); + expect(messages.length).toBe(4); - describe("A Message Counter", function () { + expect(Strophe.serialize(messages[0])).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + expect(Strophe.serialize(messages[1])).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + expect(Strophe.serialize(messages[2])).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + expect(Strophe.serialize(messages[3])).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + done(); + })); - it("is incremented when the message is received and the window is not focused", + it("is sent when the user a minimizes a chat box", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + spyOn(_converse.connection, 'send'); + view.minimize(); + expect(view.model.get('chat_state')).toBe('inactive'); + expect(_converse.connection.send).toHaveBeenCalled(); + var stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); + expect(stanza.getAttribute('to')).toBe(contact_jid); + expect(stanza.childNodes[0].tagName).toBe('inactive'); + done(); + })); + + it("is sent if the user closes a chat box", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openControlBox(_converse); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + const view = await mock.openChatBoxFor(_converse, contact_jid); + expect(view.model.get('chat_state')).toBe('active'); + spyOn(_converse.connection, 'send'); + view.close(); + expect(view.model.get('chat_state')).toBe('inactive'); + expect(_converse.connection.send).toHaveBeenCalled(); + const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); + expect(stanza.getAttribute('to')).toBe(contact_jid); + expect(stanza.childNodes.length).toBe(3); + expect(stanza.childNodes[0].tagName).toBe('inactive'); + expect(stanza.childNodes[1].tagName).toBe('no-store'); + expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); + done(); + })); + + it("will clear any other chat status notifications", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions + await mock.openChatBoxFor(_converse, sender_jid); + const view = _converse.chatboxviews.get(sender_jid); + expect(view.el.querySelectorAll('.chat-event').length).toBe(0); + // Insert message, to also check that + // text messages are inserted correctly with + // temporary chat events in the chat contents. + let msg = $msg({ + 'to': _converse.bare_jid, + 'xmlns': 'jabber:client', + 'from': sender_jid, + 'type': 'chat'}) + .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up() + .tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); + const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext).toEqual(mock.cur_names[1] + ' is typing'); + expect(view.model.messages.length).toBe(0); + + msg = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('inactive', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); + + await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent); + done(); + })); + }); + + describe("A gone notifciation", function () { + + it("will be shown if received", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 3); + await mock.openControlBox(_converse); + const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, sender_jid); - expect(document.title).toBe('Converse Tests'); - - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const view = await test_utils.openChatBoxFor(_converse, sender_jid) - - const previous_state = _converse.windowState; - const message = 'This message will increment the message counter'; - const msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t(message).up() - .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - _converse.windowState = 'hidden'; - - spyOn(_converse.api, "trigger").and.callThrough(); - spyOn(_converse, 'incrementMsgCounter').and.callThrough(); - spyOn(_converse, 'clearMsgCounter').and.callThrough(); - - await _converse.handleMessageStanza(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(_converse.incrementMsgCounter).toHaveBeenCalled(); - expect(_converse.clearMsgCounter).not.toHaveBeenCalled(); - expect(document.title).toBe('Messages (1) Converse Tests'); - expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); - _converse.windowSate = previous_state; - done(); - })); - - it("is cleared when the window is focused", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - _converse.windowState = 'hidden'; - spyOn(_converse, 'clearMsgCounter').and.callThrough(); - _converse.saveWindowState(null, 'focus'); - _converse.saveWindowState(null, 'blur'); - expect(_converse.clearMsgCounter).toHaveBeenCalled(); - done(); - })); - - it("is not incremented when the message is received and the window is focused", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - expect(document.title).toBe('Converse Tests'); - spyOn(_converse, 'incrementMsgCounter').and.callThrough(); - _converse.saveWindowState(null, 'focus'); - const message = 'This message will not increment the message counter'; - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', - msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t(message).up() - .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - await _converse.handleMessageStanza(msg); - expect(_converse.incrementMsgCounter).not.toHaveBeenCalled(); - expect(document.title).toBe('Converse Tests'); - done(); - })); - - it("is incremented from zero when chatbox was closed after viewing previously received messages and the window is not focused now", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - // initial state - expect(document.title).toBe('Converse Tests'); - const message = 'This message will always increment the message counter from zero', - sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', - msgFactory = function () { - return $msg({ + const msg = $msg({ from: sender_jid, to: _converse.connection.jid, type: 'chat', id: u.getUniqueId() + }).c('body').c('gone', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); + + const view = _converse.chatboxviews.get(sender_jid); + const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext).toEqual(mock.cur_names[1] + ' has gone away'); + done(); + })); + }); + + describe("On receiving a message correction", function () { + + it("will be removed", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions + const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + await mock.openChatBoxFor(_converse, sender_jid); + + // Original message + const original_id = u.getUniqueId(); + const original = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: original_id, + body: "Original message", + }).c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + + spyOn(_converse.api, "trigger").and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(original)); + await u.waitUntil(() => _converse.api.trigger.calls.count()); + expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); + const view = _converse.chatboxviews.get(sender_jid); + expect(view).toBeDefined(); + + // state + const msg = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); + + const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext).toEqual(mock.cur_names[1] + ' is typing'); + + // Edited message + const edited = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId(), + body: "Edited message", }) - .c('body').t(message).up() - .c('active', {'xmlns': Strophe.NS.CHATSTATES}) - .tree(); - }; + .c('active', {'xmlns': Strophe.NS.CHATSTATES}).up() + .c('replace', {'xmlns': Strophe.NS.MESSAGE_CORRECT, 'id': original_id }).tree(); - // leave converse-chat page - _converse.windowState = 'hidden'; - await _converse.handleMessageStanza(msgFactory()); - let view = _converse.chatboxviews.get(sender_jid); - expect(document.title).toBe('Messages (1) Converse Tests'); - - // come back to converse-chat page - _converse.saveWindowState(null, 'focus'); - await u.waitUntil(() => u.isVisible(view.el)); - expect(document.title).toBe('Converse Tests'); - - // close chatbox and leave converse-chat page again - view.close(); - _converse.windowState = 'hidden'; - - // check that msg_counter is incremented from zero again - await _converse.handleMessageStanza(msgFactory()); - view = _converse.chatboxviews.get(sender_jid); - await u.waitUntil(() => u.isVisible(view.el)); - expect(document.title).toBe('Messages (1) Converse Tests'); - done(); - })); - }); - - describe("A ChatBox's Unread Message Count", function () { - - it("is incremented when the message is received and ChatBoxView is scrolled up", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', - msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread'); - - const view = await test_utils.openChatBoxFor(_converse, sender_jid) - view.model.save('scrolled', true); - await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.model.messages.length); - expect(view.model.get('num_unread')).toBe(1); - done(); - })); - - it("is not incremented when the message is received and ChatBoxView is scrolled down", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', - msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be read'); - - await test_utils.openChatBoxFor(_converse, sender_jid); - const chatbox = _converse.chatboxes.get(sender_jid); - await _converse.handleMessageStanza(msg); - expect(chatbox.get('num_unread')).toBe(0); - done(); - })); - - it("is incremeted when message is received, chatbox is scrolled down and the window is not focused", - mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const msgFactory = function () { - return test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread'); - }; - await test_utils.openChatBoxFor(_converse, sender_jid); - const chatbox = _converse.chatboxes.get(sender_jid); - _converse.windowState = 'hidden'; - _converse.handleMessageStanza(msgFactory()); - await u.waitUntil(() => chatbox.messages.length); - expect(chatbox.get('num_unread')).toBe(1); - done(); - })); - - it("is incremeted when message is received, chatbox is scrolled up and the window is not focused", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const msgFactory = () => test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread'); - await test_utils.openChatBoxFor(_converse, sender_jid); - const chatbox = _converse.chatboxes.get(sender_jid); - chatbox.save('scrolled', true); - _converse.windowState = 'hidden'; - _converse.handleMessageStanza(msgFactory()); - await u.waitUntil(() => chatbox.messages.length); - expect(chatbox.get('num_unread')).toBe(1); - done(); - })); - - it("is cleared when ChatBoxView was scrolled down and the window become focused", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const msgFactory = () => test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread'); - await test_utils.openChatBoxFor(_converse, sender_jid); - const chatbox = _converse.chatboxes.get(sender_jid); - _converse.windowState = 'hidden'; - _converse.handleMessageStanza(msgFactory()); - await u.waitUntil(() => chatbox.messages.length); - expect(chatbox.get('num_unread')).toBe(1); - _converse.saveWindowState(null, 'focus'); - expect(chatbox.get('num_unread')).toBe(0); - done(); - })); - - it("is not cleared when ChatBoxView was scrolled up and the windows become focused", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const msgFactory = () => test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread'); - await test_utils.openChatBoxFor(_converse, sender_jid); - const chatbox = _converse.chatboxes.get(sender_jid); - chatbox.save('scrolled', true); - _converse.windowState = 'hidden'; - _converse.handleMessageStanza(msgFactory()); - await u.waitUntil(() => chatbox.messages.length); - expect(chatbox.get('num_unread')).toBe(1); - _converse.saveWindowState(null, 'focus'); - expect(chatbox.get('num_unread')).toBe(1); - done(); - })); - }); - - describe("A RosterView's Unread Message Count", function () { - - it("is updated when message is received and chatbox is scrolled up", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - let msg, indicator_el; - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500); - await test_utils.openChatBoxFor(_converse, sender_jid); - const chatbox = _converse.chatboxes.get(sender_jid); - chatbox.save('scrolled', true); - msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread'); - await _converse.handleMessageStanza(msg); - await u.waitUntil(() => chatbox.messages.length); - const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator'; - indicator_el = sizzle(selector, _converse.rosterview.el).pop(); - expect(indicator_el.textContent).toBe('1'); - msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread too'); - await _converse.handleMessageStanza(msg); - await u.waitUntil(() => chatbox.messages.length > 1); - indicator_el = sizzle(selector, _converse.rosterview.el).pop(); - expect(indicator_el.textContent).toBe('2'); - done(); - })); - - it("is updated when message is received and chatbox is minimized", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - - let indicator_el, msg; - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500); - await test_utils.openChatBoxFor(_converse, sender_jid); - const chatbox = _converse.chatboxes.get(sender_jid); - var chatboxview = _converse.chatboxviews.get(sender_jid); - chatboxview.minimize(); - - msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread'); - await _converse.handleMessageStanza(msg); - await u.waitUntil(() => chatbox.messages.length); - const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator'; - indicator_el = sizzle(selector, _converse.rosterview.el).pop(); - expect(indicator_el.textContent).toBe('1'); - - msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread too'); - await _converse.handleMessageStanza(msg); - await u.waitUntil(() => chatbox.messages.length === 2); - indicator_el = sizzle(selector, _converse.rosterview.el).pop(); - expect(indicator_el.textContent).toBe('2'); - done(); - })); - - it("is cleared when chatbox is maximzied after receiving messages in minimized mode", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const msgFactory = () => test_utils.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read'); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500); - await test_utils.openChatBoxFor(_converse, sender_jid); - const chatbox = _converse.chatboxes.get(sender_jid); - const view = _converse.chatboxviews.get(sender_jid); - const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator'; - const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop(); - view.minimize(); - _converse.handleMessageStanza(msgFactory()); - await u.waitUntil(() => chatbox.messages.length); - expect(select_msgs_indicator().textContent).toBe('1'); - _converse.handleMessageStanza(msgFactory()); - await u.waitUntil(() => chatbox.messages.length > 1); - expect(select_msgs_indicator().textContent).toBe('2'); - view.model.maximize(); - u.waitUntil(() => typeof select_msgs_indicator() === 'undefined'); - done(); - })); - - it("is cleared when unread messages are viewed which were received in scrolled-up chatbox", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current', 1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500); - await test_utils.openChatBoxFor(_converse, sender_jid); - const chatbox = _converse.chatboxes.get(sender_jid); - const msgFactory = () => test_utils.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read'); - const selector = `a.open-chat:contains("${chatbox.get('nickname')}") .msgs-indicator`; - const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop(); - chatbox.save('scrolled', true); - _converse.handleMessageStanza(msgFactory()); - const view = _converse.chatboxviews.get(sender_jid); - await u.waitUntil(() => view.model.messages.length); - expect(select_msgs_indicator().textContent).toBe('1'); - view.viewUnreadMessages(); - _converse.rosterview.render(); - expect(select_msgs_indicator()).toBeUndefined(); - done(); - })); - - it("is not cleared after user clicks on roster view when chatbox is already opened and scrolled up", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500); - await test_utils.openChatBoxFor(_converse, sender_jid); - const chatbox = _converse.chatboxes.get(sender_jid); - const view = _converse.chatboxviews.get(sender_jid); - const msg = 'This message will be received as unread, but eventually will be read'; - const msgFactory = () => test_utils.createChatMessage(_converse, sender_jid, msg); - const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator'; - const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop(); - chatbox.save('scrolled', true); - _converse.handleMessageStanza(msgFactory()); - await u.waitUntil(() => view.model.messages.length); - expect(select_msgs_indicator().textContent).toBe('1'); - await test_utils.openChatBoxFor(_converse, sender_jid); - expect(select_msgs_indicator().textContent).toBe('1'); - done(); - })); - }); - - describe("A Minimized ChatBoxView's Unread Message Count", function () { - - it("is displayed when scrolled up chatbox is minimized after receiving unread messages", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, sender_jid); - const msgFactory = function () { - return test_utils.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read'); - }; - const selectUnreadMsgCount = function () { - const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid); - return minimizedChatBoxView.el.querySelector('.message-count'); - }; - const chatbox = _converse.chatboxes.get(sender_jid); - chatbox.save('scrolled', true); - _converse.handleMessageStanza(msgFactory()); - await u.waitUntil(() => chatbox.messages.length); - const chatboxview = _converse.chatboxviews.get(sender_jid); - chatboxview.minimize(); - - const unread_count = selectUnreadMsgCount(); - expect(u.isVisible(unread_count)).toBeTruthy(); - expect(unread_count.innerHTML).toBe('1'); - done(); - })); - - it("is incremented when message is received and windows is not focused", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const view = await test_utils.openChatBoxFor(_converse, sender_jid) - const msgFactory = function () { - return test_utils.createChatMessage(_converse, sender_jid, - 'This message will be received as unread, but eventually will be read'); - }; - const selectUnreadMsgCount = function () { - const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid); - return minimizedChatBoxView.el.querySelector('.message-count'); - }; - view.minimize(); - _converse.handleMessageStanza(msgFactory()); - await u.waitUntil(() => view.model.messages.length); - const unread_count = selectUnreadMsgCount(); - expect(u.isVisible(unread_count)).toBeTruthy(); - expect(unread_count.innerHTML).toBe('1'); - done(); - })); - - it("will render Openstreetmap-URL from geo-URI", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - - const message = "geo:37.786971,-122.399677", - contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - spyOn(view.model, 'sendMessage').and.callThrough(); - test_utils.sendMessage(view, message); - await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length, 1000); - expect(view.model.sendMessage).toHaveBeenCalled(); - const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); - expect(msg.innerHTML).toEqual( - 'https://www.openstreetmap.org/?mlat=37.7869'+ - '71&mlon=-122.399677#map=18/37.786971/-122.399677'); - done(); - })); + await _converse.handleMessageStanza(edited); + await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent); + done(); + })); + }); }); }); + + describe("Special Messages", function () { + + it("'/clear' can be used to clear messages in a conversation", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + + spyOn(_converse.api, "trigger").and.callThrough(); + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + let message = 'This message is another sent from this chatbox'; + await mock.sendMessage(view, message); + + expect(view.model.messages.length === 1).toBeTruthy(); + let stored_messages = await view.model.messages.browserStorage.findAll(); + expect(stored_messages.length).toBe(1); + await u.waitUntil(() => view.el.querySelector('.chat-msg')); + + message = '/clear'; + spyOn(view, 'clearMessages').and.callThrough(); + spyOn(window, 'confirm').and.callFake(function () { + return true; + }); + view.el.querySelector('.chat-textarea').value = message; + view.onKeyDown({ + target: view.el.querySelector('textarea.chat-textarea'), + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + expect(view.clearMessages.calls.all().length).toBe(1); + await view.clearMessages.calls.all()[0].returnValue; + expect(window.confirm).toHaveBeenCalled(); + expect(view.model.messages.length, 0); // The messages must be removed from the chatbox + stored_messages = await view.model.messages.browserStorage.findAll(); + expect(stored_messages.length).toBe(0); + expect(_converse.api.trigger.calls.count(), 1); + expect(_converse.api.trigger.calls.mostRecent().args, ['messageSend', message]); + done(); + })); + }); + + describe("A Message Counter", function () { + + it("is incremented when the message is received and the window is not focused", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + expect(document.title).toBe('Converse Tests'); + + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const view = await mock.openChatBoxFor(_converse, sender_jid) + + const previous_state = _converse.windowState; + const message = 'This message will increment the message counter'; + const msg = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('body').t(message).up() + .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + _converse.windowState = 'hidden'; + + spyOn(_converse.api, "trigger").and.callThrough(); + spyOn(_converse, 'incrementMsgCounter').and.callThrough(); + spyOn(_converse, 'clearMsgCounter').and.callThrough(); + + await _converse.handleMessageStanza(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(_converse.incrementMsgCounter).toHaveBeenCalled(); + expect(_converse.clearMsgCounter).not.toHaveBeenCalled(); + expect(document.title).toBe('Messages (1) Converse Tests'); + expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); + _converse.windowSate = previous_state; + done(); + })); + + it("is cleared when the window is focused", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + _converse.windowState = 'hidden'; + spyOn(_converse, 'clearMsgCounter').and.callThrough(); + _converse.saveWindowState(null, 'focus'); + _converse.saveWindowState(null, 'blur'); + expect(_converse.clearMsgCounter).toHaveBeenCalled(); + done(); + })); + + it("is not incremented when the message is received and the window is focused", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + expect(document.title).toBe('Converse Tests'); + spyOn(_converse, 'incrementMsgCounter').and.callThrough(); + _converse.saveWindowState(null, 'focus'); + const message = 'This message will not increment the message counter'; + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', + msg = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('body').t(message).up() + .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + await _converse.handleMessageStanza(msg); + expect(_converse.incrementMsgCounter).not.toHaveBeenCalled(); + expect(document.title).toBe('Converse Tests'); + done(); + })); + + it("is incremented from zero when chatbox was closed after viewing previously received messages and the window is not focused now", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + // initial state + expect(document.title).toBe('Converse Tests'); + const message = 'This message will always increment the message counter from zero', + sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', + msgFactory = function () { + return $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }) + .c('body').t(message).up() + .c('active', {'xmlns': Strophe.NS.CHATSTATES}) + .tree(); + }; + + // leave converse-chat page + _converse.windowState = 'hidden'; + await _converse.handleMessageStanza(msgFactory()); + let view = _converse.chatboxviews.get(sender_jid); + expect(document.title).toBe('Messages (1) Converse Tests'); + + // come back to converse-chat page + _converse.saveWindowState(null, 'focus'); + await u.waitUntil(() => u.isVisible(view.el)); + expect(document.title).toBe('Converse Tests'); + + // close chatbox and leave converse-chat page again + view.close(); + _converse.windowState = 'hidden'; + + // check that msg_counter is incremented from zero again + await _converse.handleMessageStanza(msgFactory()); + view = _converse.chatboxviews.get(sender_jid); + await u.waitUntil(() => u.isVisible(view.el)); + expect(document.title).toBe('Messages (1) Converse Tests'); + done(); + })); + }); + + describe("A ChatBox's Unread Message Count", function () { + + it("is incremented when the message is received and ChatBoxView is scrolled up", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', + msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); + + const view = await mock.openChatBoxFor(_converse, sender_jid) + view.model.save('scrolled', true); + await _converse.handleMessageStanza(msg); + await u.waitUntil(() => view.model.messages.length); + expect(view.model.get('num_unread')).toBe(1); + done(); + })); + + it("is not incremented when the message is received and ChatBoxView is scrolled down", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', + msg = mock.createChatMessage(_converse, sender_jid, 'This message will be read'); + + await mock.openChatBoxFor(_converse, sender_jid); + const chatbox = _converse.chatboxes.get(sender_jid); + await _converse.handleMessageStanza(msg); + expect(chatbox.get('num_unread')).toBe(0); + done(); + })); + + it("is incremeted when message is received, chatbox is scrolled down and the window is not focused", + mock.initConverse(['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const msgFactory = function () { + return mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); + }; + await mock.openChatBoxFor(_converse, sender_jid); + const chatbox = _converse.chatboxes.get(sender_jid); + _converse.windowState = 'hidden'; + _converse.handleMessageStanza(msgFactory()); + await u.waitUntil(() => chatbox.messages.length); + expect(chatbox.get('num_unread')).toBe(1); + done(); + })); + + it("is incremeted when message is received, chatbox is scrolled up and the window is not focused", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); + await mock.openChatBoxFor(_converse, sender_jid); + const chatbox = _converse.chatboxes.get(sender_jid); + chatbox.save('scrolled', true); + _converse.windowState = 'hidden'; + _converse.handleMessageStanza(msgFactory()); + await u.waitUntil(() => chatbox.messages.length); + expect(chatbox.get('num_unread')).toBe(1); + done(); + })); + + it("is cleared when ChatBoxView was scrolled down and the window become focused", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); + await mock.openChatBoxFor(_converse, sender_jid); + const chatbox = _converse.chatboxes.get(sender_jid); + _converse.windowState = 'hidden'; + _converse.handleMessageStanza(msgFactory()); + await u.waitUntil(() => chatbox.messages.length); + expect(chatbox.get('num_unread')).toBe(1); + _converse.saveWindowState(null, 'focus'); + expect(chatbox.get('num_unread')).toBe(0); + done(); + })); + + it("is not cleared when ChatBoxView was scrolled up and the windows become focused", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); + await mock.openChatBoxFor(_converse, sender_jid); + const chatbox = _converse.chatboxes.get(sender_jid); + chatbox.save('scrolled', true); + _converse.windowState = 'hidden'; + _converse.handleMessageStanza(msgFactory()); + await u.waitUntil(() => chatbox.messages.length); + expect(chatbox.get('num_unread')).toBe(1); + _converse.saveWindowState(null, 'focus'); + expect(chatbox.get('num_unread')).toBe(1); + done(); + })); + }); + + describe("A RosterView's Unread Message Count", function () { + + it("is updated when message is received and chatbox is scrolled up", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + let msg, indicator_el; + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500); + await mock.openChatBoxFor(_converse, sender_jid); + const chatbox = _converse.chatboxes.get(sender_jid); + chatbox.save('scrolled', true); + msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); + await _converse.handleMessageStanza(msg); + await u.waitUntil(() => chatbox.messages.length); + const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator'; + indicator_el = sizzle(selector, _converse.rosterview.el).pop(); + expect(indicator_el.textContent).toBe('1'); + msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread too'); + await _converse.handleMessageStanza(msg); + await u.waitUntil(() => chatbox.messages.length > 1); + indicator_el = sizzle(selector, _converse.rosterview.el).pop(); + expect(indicator_el.textContent).toBe('2'); + done(); + })); + + it("is updated when message is received and chatbox is minimized", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + + let indicator_el, msg; + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500); + await mock.openChatBoxFor(_converse, sender_jid); + const chatbox = _converse.chatboxes.get(sender_jid); + var chatboxview = _converse.chatboxviews.get(sender_jid); + chatboxview.minimize(); + + msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread'); + await _converse.handleMessageStanza(msg); + await u.waitUntil(() => chatbox.messages.length); + const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator'; + indicator_el = sizzle(selector, _converse.rosterview.el).pop(); + expect(indicator_el.textContent).toBe('1'); + + msg = mock.createChatMessage(_converse, sender_jid, 'This message will be unread too'); + await _converse.handleMessageStanza(msg); + await u.waitUntil(() => chatbox.messages.length === 2); + indicator_el = sizzle(selector, _converse.rosterview.el).pop(); + expect(indicator_el.textContent).toBe('2'); + done(); + })); + + it("is cleared when chatbox is maximzied after receiving messages in minimized mode", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read'); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500); + await mock.openChatBoxFor(_converse, sender_jid); + const chatbox = _converse.chatboxes.get(sender_jid); + const view = _converse.chatboxviews.get(sender_jid); + const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator'; + const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop(); + view.minimize(); + _converse.handleMessageStanza(msgFactory()); + await u.waitUntil(() => chatbox.messages.length); + expect(select_msgs_indicator().textContent).toBe('1'); + _converse.handleMessageStanza(msgFactory()); + await u.waitUntil(() => chatbox.messages.length > 1); + expect(select_msgs_indicator().textContent).toBe('2'); + view.model.maximize(); + u.waitUntil(() => typeof select_msgs_indicator() === 'undefined'); + done(); + })); + + it("is cleared when unread messages are viewed which were received in scrolled-up chatbox", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500); + await mock.openChatBoxFor(_converse, sender_jid); + const chatbox = _converse.chatboxes.get(sender_jid); + const msgFactory = () => mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read'); + const selector = `a.open-chat:contains("${chatbox.get('nickname')}") .msgs-indicator`; + const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop(); + chatbox.save('scrolled', true); + _converse.handleMessageStanza(msgFactory()); + const view = _converse.chatboxviews.get(sender_jid); + await u.waitUntil(() => view.model.messages.length); + expect(select_msgs_indicator().textContent).toBe('1'); + view.viewUnreadMessages(); + _converse.rosterview.render(); + expect(select_msgs_indicator()).toBeUndefined(); + done(); + })); + + it("is not cleared after user clicks on roster view when chatbox is already opened and scrolled up", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 500); + await mock.openChatBoxFor(_converse, sender_jid); + const chatbox = _converse.chatboxes.get(sender_jid); + const view = _converse.chatboxviews.get(sender_jid); + const msg = 'This message will be received as unread, but eventually will be read'; + const msgFactory = () => mock.createChatMessage(_converse, sender_jid, msg); + const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator'; + const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop(); + chatbox.save('scrolled', true); + _converse.handleMessageStanza(msgFactory()); + await u.waitUntil(() => view.model.messages.length); + expect(select_msgs_indicator().textContent).toBe('1'); + await mock.openChatBoxFor(_converse, sender_jid); + expect(select_msgs_indicator().textContent).toBe('1'); + done(); + })); + }); + + describe("A Minimized ChatBoxView's Unread Message Count", function () { + + it("is displayed when scrolled up chatbox is minimized after receiving unread messages", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, sender_jid); + const msgFactory = function () { + return mock.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read'); + }; + const selectUnreadMsgCount = function () { + const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid); + return minimizedChatBoxView.el.querySelector('.message-count'); + }; + const chatbox = _converse.chatboxes.get(sender_jid); + chatbox.save('scrolled', true); + _converse.handleMessageStanza(msgFactory()); + await u.waitUntil(() => chatbox.messages.length); + const chatboxview = _converse.chatboxviews.get(sender_jid); + chatboxview.minimize(); + + const unread_count = selectUnreadMsgCount(); + expect(u.isVisible(unread_count)).toBeTruthy(); + expect(unread_count.innerHTML).toBe('1'); + done(); + })); + + it("is incremented when message is received and windows is not focused", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const view = await mock.openChatBoxFor(_converse, sender_jid) + const msgFactory = function () { + return mock.createChatMessage(_converse, sender_jid, + 'This message will be received as unread, but eventually will be read'); + }; + const selectUnreadMsgCount = function () { + const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid); + return minimizedChatBoxView.el.querySelector('.message-count'); + }; + view.minimize(); + _converse.handleMessageStanza(msgFactory()); + await u.waitUntil(() => view.model.messages.length); + const unread_count = selectUnreadMsgCount(); + expect(u.isVisible(unread_count)).toBeTruthy(); + expect(unread_count.innerHTML).toBe('1'); + done(); + })); + + it("will render Openstreetmap-URL from geo-URI", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + + const message = "geo:37.786971,-122.399677", + contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + spyOn(view.model, 'sendMessage').and.callThrough(); + mock.sendMessage(view, message); + await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg').length, 1000); + expect(view.model.sendMessage).toHaveBeenCalled(); + const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); + expect(msg.innerHTML).toEqual( + 'https://www.openstreetmap.org/?mlat=37.7869'+ + '71&mlon=-122.399677#map=18/37.786971/-122.399677'); + done(); + })); + }); }); diff --git a/spec/controlbox.js b/spec/controlbox.js index 6ae4ac5e4..77c25c7aa 100644 --- a/spec/controlbox.js +++ b/spec/controlbox.js @@ -1,389 +1,387 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const _ = converse.env._, - $msg = converse.env.$msg, - u = converse.env.utils, - Strophe = converse.env.Strophe, - sizzle = converse.env.sizzle; +/*global mock */ + +const _ = converse.env._, + $msg = converse.env.$msg, + u = converse.env.utils, + Strophe = converse.env.Strophe, + sizzle = converse.env.sizzle; - describe("The Controlbox", function () { +describe("The Controlbox", function () { - it("can be opened by clicking a DOM element with class 'toggle-controlbox'", + it("can be opened by clicking a DOM element with class 'toggle-controlbox'", + mock.initConverse( + ['rosterGroupsFetched'], {}, + function (done, _converse) { + + // This spec will only pass if the controlbox is not currently + // open yet. + let el = document.querySelector("div#controlbox"); + expect(_.isElement(el)).toBe(true); + expect(u.isVisible(el)).toBe(false); + spyOn(_converse.controlboxtoggle, 'onClick').and.callThrough(); + spyOn(_converse.controlboxtoggle, 'showControlBox').and.callThrough(); + spyOn(_converse.api, "trigger").and.callThrough(); + // Redelegate so that the spies are now registered as the event handlers (specifically for 'onClick') + _converse.controlboxtoggle.delegateEvents(); + document.querySelector('.toggle-controlbox').click(); + expect(_converse.controlboxtoggle.onClick).toHaveBeenCalled(); + expect(_converse.controlboxtoggle.showControlBox).toHaveBeenCalled(); + expect(_converse.api.trigger).toHaveBeenCalledWith('controlBoxOpened', jasmine.any(Object)); + el = document.querySelector("div#controlbox"); + expect(u.isVisible(el)).toBe(true); + done(); + })); + + describe("The \"Contacts\" section", function () { + + it("can be used to add contact and it checks for case-sensivity", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + spyOn(_converse.api, "trigger").and.callThrough(); + spyOn(_converse.rosterview, 'update').and.callThrough(); + await mock.openControlBox(_converse); + // Adding two contacts one with Capital initials and one with small initials of same JID (Case sensitive check) + _converse.roster.create({ + jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', + subscription: 'none', + ask: 'subscribe', + fullname: mock.pend_names[0] + }); + _converse.roster.create({ + jid: mock.pend_names[0].replace(/ /g,'.') + '@montague.lit', + subscription: 'none', + ask: 'subscribe', + fullname: mock.pend_names[0] + }); + await u.waitUntil(() => _.filter(_converse.rosterview.el.querySelectorAll('.roster-group li'), u.isVisible).length, 700); + // Checking that only one entry is created because both JID is same (Case sensitive check) + expect(_.filter(_converse.rosterview.el.querySelectorAll('li'), u.isVisible).length).toBe(1); + expect(_converse.rosterview.update).toHaveBeenCalled(); + done(); + })); + + it("shows the number of unread mentions received", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'all'); + await mock.openControlBox(_converse); + + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, sender_jid); + await u.waitUntil(() => _converse.chatboxes.length); + const chatview = _converse.chatboxviews.get(sender_jid); + chatview.model.set({'minimized': true}); + + expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count') === null).toBeTruthy(); + expect(_converse.rosterview.el.querySelector('.msgs-indicator') === null).toBeTruthy(); + + let msg = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('body').t('hello').up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + _converse.handleMessageStanza(msg); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll(".msgs-indicator").length); + spyOn(chatview.model, 'incrementUnreadMsgCounter').and.callThrough(); + expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count').textContent).toBe('1'); + expect(_converse.rosterview.el.querySelector('.msgs-indicator').textContent).toBe('1'); + + msg = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('body').t('hello again').up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + _converse.handleMessageStanza(msg); + await u.waitUntil(() => chatview.model.incrementUnreadMsgCounter.calls.count()); + expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count').textContent).toBe('2'); + expect(_converse.rosterview.el.querySelector('.msgs-indicator').textContent).toBe('2'); + chatview.model.set({'minimized': false}); + expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count')).toBe(null); + await u.waitUntil(() => _converse.rosterview.el.querySelector('.msgs-indicator') === null); + done(); + })); + }); + + describe("The Status Widget", function () { + + it("shows the user's chat status, which is online by default", mock.initConverse( ['rosterGroupsFetched'], {}, function (done, _converse) { - // This spec will only pass if the controlbox is not currently - // open yet. - let el = document.querySelector("div#controlbox"); - expect(_.isElement(el)).toBe(true); - expect(u.isVisible(el)).toBe(false); - spyOn(_converse.controlboxtoggle, 'onClick').and.callThrough(); - spyOn(_converse.controlboxtoggle, 'showControlBox').and.callThrough(); - spyOn(_converse.api, "trigger").and.callThrough(); - // Redelegate so that the spies are now registered as the event handlers (specifically for 'onClick') - _converse.controlboxtoggle.delegateEvents(); - document.querySelector('.toggle-controlbox').click(); - expect(_converse.controlboxtoggle.onClick).toHaveBeenCalled(); - expect(_converse.controlboxtoggle.showControlBox).toHaveBeenCalled(); - expect(_converse.api.trigger).toHaveBeenCalledWith('controlBoxOpened', jasmine.any(Object)); - el = document.querySelector("div#controlbox"); - expect(u.isVisible(el)).toBe(true); + mock.openControlBox(_converse); + var view = _converse.xmppstatusview; + expect(u.hasClass('online', view.el.querySelector('.xmpp-status span:first-child'))).toBe(true); + expect(view.el.querySelector('.xmpp-status span.online').textContent.trim()).toBe('I am online'); done(); })); - describe("The \"Contacts\" section", function () { - - it("can be used to add contact and it checks for case-sensivity", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - spyOn(_converse.api, "trigger").and.callThrough(); - spyOn(_converse.rosterview, 'update').and.callThrough(); - await test_utils.openControlBox(_converse); - // Adding two contacts one with Capital initials and one with small initials of same JID (Case sensitive check) - _converse.roster.create({ - jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', - subscription: 'none', - ask: 'subscribe', - fullname: mock.pend_names[0] - }); - _converse.roster.create({ - jid: mock.pend_names[0].replace(/ /g,'.') + '@montague.lit', - subscription: 'none', - ask: 'subscribe', - fullname: mock.pend_names[0] - }); - await u.waitUntil(() => _.filter(_converse.rosterview.el.querySelectorAll('.roster-group li'), u.isVisible).length, 700); - // Checking that only one entry is created because both JID is same (Case sensitive check) - expect(_.filter(_converse.rosterview.el.querySelectorAll('li'), u.isVisible).length).toBe(1); - expect(_converse.rosterview.update).toHaveBeenCalled(); - done(); - })); - - it("shows the number of unread mentions received", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'all'); - await test_utils.openControlBox(_converse); - - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, sender_jid); - await u.waitUntil(() => _converse.chatboxes.length); - const chatview = _converse.chatboxviews.get(sender_jid); - chatview.model.set({'minimized': true}); - - expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count') === null).toBeTruthy(); - expect(_converse.rosterview.el.querySelector('.msgs-indicator') === null).toBeTruthy(); - - let msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t('hello').up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); - _converse.handleMessageStanza(msg); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll(".msgs-indicator").length); - spyOn(chatview.model, 'incrementUnreadMsgCounter').and.callThrough(); - expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count').textContent).toBe('1'); - expect(_converse.rosterview.el.querySelector('.msgs-indicator').textContent).toBe('1'); - - msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t('hello again').up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); - _converse.handleMessageStanza(msg); - await u.waitUntil(() => chatview.model.incrementUnreadMsgCounter.calls.count()); - expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count').textContent).toBe('2'); - expect(_converse.rosterview.el.querySelector('.msgs-indicator').textContent).toBe('2'); - chatview.model.set({'minimized': false}); - expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count')).toBe(null); - await u.waitUntil(() => _converse.rosterview.el.querySelector('.msgs-indicator') === null); - done(); - })); - }); - - describe("The Status Widget", function () { - - it("shows the user's chat status, which is online by default", - mock.initConverse( - ['rosterGroupsFetched'], {}, - function (done, _converse) { - - test_utils.openControlBox(_converse); - var view = _converse.xmppstatusview; - expect(u.hasClass('online', view.el.querySelector('.xmpp-status span:first-child'))).toBe(true); - expect(view.el.querySelector('.xmpp-status span.online').textContent.trim()).toBe('I am online'); - done(); - })); - - it("can be used to set the current user's chat status", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - var cbview = _converse.chatboxviews.get('controlbox'); - cbview.el.querySelector('.change-status').click() - var modal = _converse.xmppstatusview.status_modal; - - await u.waitUntil(() => u.isVisible(modal.el), 1000); - const view = _converse.xmppstatusview; - modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd" - modal.el.querySelector('[type="submit"]').click(); - const sent_stanzas = _converse.connection.sent_stanzas; - const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop()); - expect(Strophe.serialize(sent_presence)).toBe( - ``+ - `dnd`+ - `0`+ - ``+ - ``); - const first_child = view.el.querySelector('.xmpp-status span:first-child'); - expect(u.hasClass('online', first_child)).toBe(false); - expect(u.hasClass('dnd', first_child)).toBe(true); - expect(view.el.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe('I am busy'); - done(); - })); - - it("can be used to set a custom status message", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - const cbview = _converse.chatboxviews.get('controlbox'); - cbview.el.querySelector('.change-status').click() - const modal = _converse.xmppstatusview.status_modal; - - await u.waitUntil(() => u.isVisible(modal.el), 1000); - const view = _converse.xmppstatusview; - const msg = 'I am happy'; - modal.el.querySelector('input[name="status_message"]').value = msg; - modal.el.querySelector('[type="submit"]').click(); - const sent_stanzas = _converse.connection.sent_stanzas; - const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop()); - expect(Strophe.serialize(sent_presence)).toBe( - ``+ - `I am happy`+ - `0`+ - ``+ - ``); - - const first_child = view.el.querySelector('.xmpp-status span:first-child'); - expect(u.hasClass('online', first_child)).toBe(true); - expect(view.el.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe(msg); - done(); - })); - }); - }); - - describe("The 'Add Contact' widget", function () { - - it("opens up an add modal when you click on it", + it("can be used to set the current user's chat status", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'all'); - await test_utils.openControlBox(_converse); + await mock.openControlBox(_converse); + var cbview = _converse.chatboxviews.get('controlbox'); + cbview.el.querySelector('.change-status').click() + var modal = _converse.xmppstatusview.status_modal; - const cbview = _converse.chatboxviews.get('controlbox'); - cbview.el.querySelector('.add-contact').click() - const modal = _converse.rosterview.add_contact_modal; await u.waitUntil(() => u.isVisible(modal.el), 1000); - expect(modal.el.querySelector('form.add-xmpp-contact')).not.toBe(null); - - const input_jid = modal.el.querySelector('input[name="jid"]'); - const input_name = modal.el.querySelector('input[name="name"]'); - input_jid.value = 'someone@'; - - const evt = new Event('input'); - input_jid.dispatchEvent(evt); - expect(modal.el.querySelector('.suggestion-box li').textContent).toBe('someone@montague.lit'); - input_jid.value = 'someone@montague.lit'; - input_name.value = 'Someone'; - modal.el.querySelector('button[type="submit"]').click(); - - const sent_IQs = _converse.connection.IQ_stanzas; - const sent_stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`)).pop()); - expect(Strophe.serialize(sent_stanza)).toEqual( - ``+ - ``+ - ``); + const view = _converse.xmppstatusview; + modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd" + modal.el.querySelector('[type="submit"]').click(); + const sent_stanzas = _converse.connection.sent_stanzas; + const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop()); + expect(Strophe.serialize(sent_presence)).toBe( + ``+ + `dnd`+ + `0`+ + ``+ + ``); + const first_child = view.el.querySelector('.xmpp-status span:first-child'); + expect(u.hasClass('online', first_child)).toBe(false); + expect(u.hasClass('dnd', first_child)).toBe(true); + expect(view.el.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe('I am busy'); done(); })); - it("can be configured to not provide search suggestions", + it("can be used to set a custom status message", mock.initConverse( - ['rosterGroupsFetched'], {'autocomplete_add_contact': false}, + ['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'all', 0); - test_utils.openControlBox(_converse); + await mock.openControlBox(_converse); const cbview = _converse.chatboxviews.get('controlbox'); - cbview.el.querySelector('.add-contact').click() - const modal = _converse.rosterview.add_contact_modal; - expect(modal.jid_auto_complete).toBe(undefined); - expect(modal.name_auto_complete).toBe(undefined); + cbview.el.querySelector('.change-status').click() + const modal = _converse.xmppstatusview.status_modal; await u.waitUntil(() => u.isVisible(modal.el), 1000); - expect(modal.el.querySelector('form.add-xmpp-contact')).not.toBe(null); - const input_jid = modal.el.querySelector('input[name="jid"]'); - input_jid.value = 'someone@montague.lit'; - modal.el.querySelector('button[type="submit"]').click(); + const view = _converse.xmppstatusview; + const msg = 'I am happy'; + modal.el.querySelector('input[name="status_message"]').value = msg; + modal.el.querySelector('[type="submit"]').click(); + const sent_stanzas = _converse.connection.sent_stanzas; + const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop()); + expect(Strophe.serialize(sent_presence)).toBe( + ``+ + `I am happy`+ + `0`+ + ``+ + ``); - const IQ_stanzas = _converse.connection.IQ_stanzas; - const sent_stanza = await u.waitUntil( - () => IQ_stanzas.filter(s => sizzle(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`, s).length).pop() - ); - expect(Strophe.serialize(sent_stanza)).toEqual( - ``+ - ``+ - `` - ); - done(); - })); - - - it("integrates with xhr_user_search_url to search for contacts", - mock.initConverse( - ['rosterGroupsFetched'], - { 'xhr_user_search_url': 'http://example.org/?' }, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'all', 0); - - const xhr = { - 'open': function open () {}, - 'send': function () { - xhr.responseText = JSON.stringify([ - {"jid": "marty@mcfly.net", "fullname": "Marty McFly"}, - {"jid": "doc@brown.com", "fullname": "Doc Brown"} - ]); - xhr.onload(); - } - }; - const XMLHttpRequestBackup = window.XMLHttpRequest; - window.XMLHttpRequest = jasmine.createSpy('XMLHttpRequest'); - XMLHttpRequest.and.callFake(() => xhr); - - const cbview = _converse.chatboxviews.get('controlbox'); - cbview.el.querySelector('.add-contact').click() - const modal = _converse.rosterview.add_contact_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - - // We only have autocomplete for the name input - expect(modal.jid_auto_complete).toBe(undefined); - expect(modal.name_auto_complete instanceof _converse.AutoComplete).toBe(true); - - const input_el = modal.el.querySelector('input[name="name"]'); - input_el.value = 'marty'; - input_el.dispatchEvent(new Event('input')); - await u.waitUntil(() => modal.el.querySelector('.suggestion-box li'), 1000); - expect(modal.el.querySelectorAll('.suggestion-box li').length).toBe(1); - const suggestion = modal.el.querySelector('.suggestion-box li'); - expect(suggestion.textContent).toBe('Marty McFly'); - - // Mock selection - modal.name_auto_complete.select(suggestion); - - expect(input_el.value).toBe('Marty McFly'); - expect(modal.el.querySelector('input[name="jid"]').value).toBe('marty@mcfly.net'); - modal.el.querySelector('button[type="submit"]').click(); - - const sent_IQs = _converse.connection.IQ_stanzas; - const sent_stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`)).pop()); - expect(Strophe.serialize(sent_stanza)).toEqual( - ``+ - ``+ - ``); - window.XMLHttpRequest = XMLHttpRequestBackup; - done(); - })); - - it("can be configured to not provide search suggestions for XHR search results", - mock.initConverse( - ['rosterGroupsFetched'], - { 'autocomplete_add_contact': false, - 'xhr_user_search_url': 'http://example.org/?' }, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'all'); - await test_utils.openControlBox(_converse); - var modal; - const xhr = { - 'open': function open () {}, - 'send': function () { - const value = modal.el.querySelector('input[name="name"]').value; - if (value === 'existing') { - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - xhr.responseText = JSON.stringify([{"jid": contact_jid, "fullname": mock.cur_names[0]}]); - } else if (value === 'romeo') { - xhr.responseText = JSON.stringify([{"jid": "romeo@montague.lit", "fullname": "Romeo Montague"}]); - } else if (value === 'ambiguous') { - xhr.responseText = JSON.stringify([ - {"jid": "marty@mcfly.net", "fullname": "Marty McFly"}, - {"jid": "doc@brown.com", "fullname": "Doc Brown"} - ]); - } else if (value === 'insufficient') { - xhr.responseText = JSON.stringify([]); - } else { - xhr.responseText = JSON.stringify([{"jid": "marty@mcfly.net", "fullname": "Marty McFly"}]); - } - xhr.onload(); - } - }; - const XMLHttpRequestBackup = window.XMLHttpRequest; - window.XMLHttpRequest = jasmine.createSpy('XMLHttpRequest'); - XMLHttpRequest.and.callFake(() => xhr); - - const cbview = _converse.chatboxviews.get('controlbox'); - cbview.el.querySelector('.add-contact').click() - modal = _converse.rosterview.add_contact_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - - expect(modal.jid_auto_complete).toBe(undefined); - expect(modal.name_auto_complete).toBe(undefined); - - const input_el = modal.el.querySelector('input[name="name"]'); - input_el.value = 'ambiguous'; - modal.el.querySelector('button[type="submit"]').click(); - let feedback_el = modal.el.querySelector('.invalid-feedback'); - expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name'); - feedback_el.textContent = ''; - - input_el.value = 'insufficient'; - modal.el.querySelector('button[type="submit"]').click(); - feedback_el = modal.el.querySelector('.invalid-feedback'); - expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name'); - feedback_el.textContent = ''; - - input_el.value = 'existing'; - modal.el.querySelector('button[type="submit"]').click(); - feedback_el = modal.el.querySelector('.invalid-feedback'); - expect(feedback_el.textContent).toBe('This contact has already been added'); - - input_el.value = 'Marty McFly'; - modal.el.querySelector('button[type="submit"]').click(); - - const sent_IQs = _converse.connection.IQ_stanzas; - const sent_stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`)).pop()); - expect(Strophe.serialize(sent_stanza)).toEqual( - ``+ - ``+ - ``); - window.XMLHttpRequest = XMLHttpRequestBackup; + const first_child = view.el.querySelector('.xmpp-status span:first-child'); + expect(u.hasClass('online', first_child)).toBe(true); + expect(view.el.querySelector('.xmpp-status span:first-child').textContent.trim()).toBe(msg); done(); })); }); }); + +describe("The 'Add Contact' widget", function () { + + it("opens up an add modal when you click on it", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'all'); + await mock.openControlBox(_converse); + + const cbview = _converse.chatboxviews.get('controlbox'); + cbview.el.querySelector('.add-contact').click() + const modal = _converse.rosterview.add_contact_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + expect(modal.el.querySelector('form.add-xmpp-contact')).not.toBe(null); + + const input_jid = modal.el.querySelector('input[name="jid"]'); + const input_name = modal.el.querySelector('input[name="name"]'); + input_jid.value = 'someone@'; + + const evt = new Event('input'); + input_jid.dispatchEvent(evt); + expect(modal.el.querySelector('.suggestion-box li').textContent).toBe('someone@montague.lit'); + input_jid.value = 'someone@montague.lit'; + input_name.value = 'Someone'; + modal.el.querySelector('button[type="submit"]').click(); + + const sent_IQs = _converse.connection.IQ_stanzas; + const sent_stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`)).pop()); + expect(Strophe.serialize(sent_stanza)).toEqual( + ``+ + ``+ + ``); + done(); + })); + + it("can be configured to not provide search suggestions", + mock.initConverse( + ['rosterGroupsFetched'], {'autocomplete_add_contact': false}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'all', 0); + mock.openControlBox(_converse); + const cbview = _converse.chatboxviews.get('controlbox'); + cbview.el.querySelector('.add-contact').click() + const modal = _converse.rosterview.add_contact_modal; + expect(modal.jid_auto_complete).toBe(undefined); + expect(modal.name_auto_complete).toBe(undefined); + + await u.waitUntil(() => u.isVisible(modal.el), 1000); + expect(modal.el.querySelector('form.add-xmpp-contact')).not.toBe(null); + const input_jid = modal.el.querySelector('input[name="jid"]'); + input_jid.value = 'someone@montague.lit'; + modal.el.querySelector('button[type="submit"]').click(); + + const IQ_stanzas = _converse.connection.IQ_stanzas; + const sent_stanza = await u.waitUntil( + () => IQ_stanzas.filter(s => sizzle(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`, s).length).pop() + ); + expect(Strophe.serialize(sent_stanza)).toEqual( + ``+ + ``+ + `` + ); + done(); + })); + + + it("integrates with xhr_user_search_url to search for contacts", + mock.initConverse( + ['rosterGroupsFetched'], + { 'xhr_user_search_url': 'http://example.org/?' }, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'all', 0); + + const xhr = { + 'open': function open () {}, + 'send': function () { + xhr.responseText = JSON.stringify([ + {"jid": "marty@mcfly.net", "fullname": "Marty McFly"}, + {"jid": "doc@brown.com", "fullname": "Doc Brown"} + ]); + xhr.onload(); + } + }; + const XMLHttpRequestBackup = window.XMLHttpRequest; + window.XMLHttpRequest = jasmine.createSpy('XMLHttpRequest'); + XMLHttpRequest.and.callFake(() => xhr); + + const cbview = _converse.chatboxviews.get('controlbox'); + cbview.el.querySelector('.add-contact').click() + const modal = _converse.rosterview.add_contact_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + + // We only have autocomplete for the name input + expect(modal.jid_auto_complete).toBe(undefined); + expect(modal.name_auto_complete instanceof _converse.AutoComplete).toBe(true); + + const input_el = modal.el.querySelector('input[name="name"]'); + input_el.value = 'marty'; + input_el.dispatchEvent(new Event('input')); + await u.waitUntil(() => modal.el.querySelector('.suggestion-box li'), 1000); + expect(modal.el.querySelectorAll('.suggestion-box li').length).toBe(1); + const suggestion = modal.el.querySelector('.suggestion-box li'); + expect(suggestion.textContent).toBe('Marty McFly'); + + // Mock selection + modal.name_auto_complete.select(suggestion); + + expect(input_el.value).toBe('Marty McFly'); + expect(modal.el.querySelector('input[name="jid"]').value).toBe('marty@mcfly.net'); + modal.el.querySelector('button[type="submit"]').click(); + + const sent_IQs = _converse.connection.IQ_stanzas; + const sent_stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`)).pop()); + expect(Strophe.serialize(sent_stanza)).toEqual( + ``+ + ``+ + ``); + window.XMLHttpRequest = XMLHttpRequestBackup; + done(); + })); + + it("can be configured to not provide search suggestions for XHR search results", + mock.initConverse( + ['rosterGroupsFetched'], + { 'autocomplete_add_contact': false, + 'xhr_user_search_url': 'http://example.org/?' }, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'all'); + await mock.openControlBox(_converse); + var modal; + const xhr = { + 'open': function open () {}, + 'send': function () { + const value = modal.el.querySelector('input[name="name"]').value; + if (value === 'existing') { + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + xhr.responseText = JSON.stringify([{"jid": contact_jid, "fullname": mock.cur_names[0]}]); + } else if (value === 'romeo') { + xhr.responseText = JSON.stringify([{"jid": "romeo@montague.lit", "fullname": "Romeo Montague"}]); + } else if (value === 'ambiguous') { + xhr.responseText = JSON.stringify([ + {"jid": "marty@mcfly.net", "fullname": "Marty McFly"}, + {"jid": "doc@brown.com", "fullname": "Doc Brown"} + ]); + } else if (value === 'insufficient') { + xhr.responseText = JSON.stringify([]); + } else { + xhr.responseText = JSON.stringify([{"jid": "marty@mcfly.net", "fullname": "Marty McFly"}]); + } + xhr.onload(); + } + }; + const XMLHttpRequestBackup = window.XMLHttpRequest; + window.XMLHttpRequest = jasmine.createSpy('XMLHttpRequest'); + XMLHttpRequest.and.callFake(() => xhr); + + const cbview = _converse.chatboxviews.get('controlbox'); + cbview.el.querySelector('.add-contact').click() + modal = _converse.rosterview.add_contact_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + + expect(modal.jid_auto_complete).toBe(undefined); + expect(modal.name_auto_complete).toBe(undefined); + + const input_el = modal.el.querySelector('input[name="name"]'); + input_el.value = 'ambiguous'; + modal.el.querySelector('button[type="submit"]').click(); + let feedback_el = modal.el.querySelector('.invalid-feedback'); + expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name'); + feedback_el.textContent = ''; + + input_el.value = 'insufficient'; + modal.el.querySelector('button[type="submit"]').click(); + feedback_el = modal.el.querySelector('.invalid-feedback'); + expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name'); + feedback_el.textContent = ''; + + input_el.value = 'existing'; + modal.el.querySelector('button[type="submit"]').click(); + feedback_el = modal.el.querySelector('.invalid-feedback'); + expect(feedback_el.textContent).toBe('This contact has already been added'); + + input_el.value = 'Marty McFly'; + modal.el.querySelector('button[type="submit"]').click(); + + const sent_IQs = _converse.connection.IQ_stanzas; + const sent_stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`)).pop()); + expect(Strophe.serialize(sent_stanza)).toEqual( + ``+ + ``+ + ``); + window.XMLHttpRequest = XMLHttpRequestBackup; + done(); + })); +}); diff --git a/spec/converse.js b/spec/converse.js index 1407bdfc2..72490ebbb 100644 --- a/spec/converse.js +++ b/spec/converse.js @@ -1,367 +1,365 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const _ = converse.env._, - u = converse.env.utils; +/* global mock */ - describe("Converse", function() { +describe("Converse", function() { - describe("Authentication", function () { + describe("Authentication", function () { - it("needs either a bosh_service_url a websocket_url or both", mock.initConverse(async (done, _converse) => { - const url = _converse.bosh_service_url; - const connection = _converse.connection; - delete _converse.bosh_service_url; - delete _converse.connection; - try { - await _converse.initConnection(); - } catch (e) { - _converse.bosh_service_url = url; - _converse.connection = connection; - expect(e.message).toBe("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both."); - done(); - } - })); - }); - - describe("A chat state indication", function () { - - it("are sent out when the client becomes or stops being idle", - mock.initConverse(['discoInitialized'], {}, (done, _converse) => { - - spyOn(_converse, 'sendCSI').and.callThrough(); - let sent_stanza; - spyOn(_converse.connection, 'send').and.callFake(function (stanza) { - sent_stanza = stanza; - }); - let i = 0; - _converse.idle_seconds = 0; // Usually initialized by registerIntervalHandler - _converse.disco_entities.get(_converse.domain).features['urn:xmpp:csi:0'] = true; // Mock that the server supports CSI - - _converse.api.settings.set('csi_waiting_time', 3); - while (i <= _converse.api.settings.get("csi_waiting_time")) { - expect(_converse.sendCSI).not.toHaveBeenCalled(); - _converse.onEverySecond(); - i++; - } - expect(_converse.sendCSI).toHaveBeenCalledWith('inactive'); - expect(sent_stanza.toLocaleString()).toBe(''); - _converse.onUserActivity(); - expect(_converse.sendCSI).toHaveBeenCalledWith('active'); - expect(sent_stanza.toLocaleString()).toBe(''); + it("needs either a bosh_service_url a websocket_url or both", mock.initConverse(async (done, _converse) => { + const url = _converse.bosh_service_url; + const connection = _converse.connection; + delete _converse.bosh_service_url; + delete _converse.connection; + try { + await _converse.initConnection(); + } catch (e) { + _converse.bosh_service_url = url; + _converse.connection = connection; + expect(e.message).toBe("initConnection: you must supply a value for either the bosh_service_url or websocket_url or both."); done(); - })); - }); + } + })); + }); - describe("Automatic status change", function () { + describe("A chat state indication", function () { - it("happens when the client is idle for long enough", mock.initConverse((done, _converse) => { - let i = 0; - // Usually initialized by registerIntervalHandler - _converse.idle_seconds = 0; - _converse.auto_changed_status = false; - _converse.api.settings.set('auto_away', 3); - _converse.api.settings.set('auto_xa', 6); + it("are sent out when the client becomes or stops being idle", + mock.initConverse(['discoInitialized'], {}, (done, _converse) => { - expect(_converse.api.user.status.get()).toBe('online'); - while (i <= _converse.api.settings.get("auto_away")) { - _converse.onEverySecond(); i++; - } - expect(_converse.auto_changed_status).toBe(true); - - while (i <= _converse.auto_xa) { - expect(_converse.api.user.status.get()).toBe('away'); - _converse.onEverySecond(); - i++; - } - expect(_converse.api.user.status.get()).toBe('xa'); - expect(_converse.auto_changed_status).toBe(true); - - _converse.onUserActivity(); - expect(_converse.api.user.status.get()).toBe('online'); - expect(_converse.auto_changed_status).toBe(false); - - // Check that it also works for the chat feature - _converse.api.user.status.set('chat') - i = 0; - while (i <= _converse.api.settings.get("auto_away")) { - _converse.onEverySecond(); - i++; - } - expect(_converse.auto_changed_status).toBe(true); - while (i <= _converse.auto_xa) { - expect(_converse.api.user.status.get()).toBe('away'); - _converse.onEverySecond(); - i++; - } - expect(_converse.api.user.status.get()).toBe('xa'); - expect(_converse.auto_changed_status).toBe(true); - - _converse.onUserActivity(); - expect(_converse.api.user.status.get()).toBe('online'); - expect(_converse.auto_changed_status).toBe(false); - - // Check that it doesn't work for 'dnd' - _converse.api.user.status.set('dnd'); - i = 0; - while (i <= _converse.api.settings.get("auto_away")) { - _converse.onEverySecond(); - i++; - } - expect(_converse.api.user.status.get()).toBe('dnd'); - expect(_converse.auto_changed_status).toBe(false); - while (i <= _converse.auto_xa) { - expect(_converse.api.user.status.get()).toBe('dnd'); - _converse.onEverySecond(); - i++; - } - expect(_converse.api.user.status.get()).toBe('dnd'); - expect(_converse.auto_changed_status).toBe(false); - - _converse.onUserActivity(); - expect(_converse.api.user.status.get()).toBe('dnd'); - expect(_converse.auto_changed_status).toBe(false); - done(); - })); - }); - - describe("The \"user\" grouping", function () { - - describe("The \"status\" API", function () { - - it("has a method for getting the user's availability", mock.initConverse((done, _converse) => { - _converse.xmppstatus.set('status', 'online'); - expect(_converse.api.user.status.get()).toBe('online'); - _converse.xmppstatus.set('status', 'dnd'); - expect(_converse.api.user.status.get()).toBe('dnd'); - done(); - })); - - it("has a method for setting the user's availability", mock.initConverse((done, _converse) => { - _converse.api.user.status.set('away'); - expect(_converse.xmppstatus.get('status')).toBe('away'); - _converse.api.user.status.set('dnd'); - expect(_converse.xmppstatus.get('status')).toBe('dnd'); - _converse.api.user.status.set('xa'); - expect(_converse.xmppstatus.get('status')).toBe('xa'); - _converse.api.user.status.set('chat'); - expect(_converse.xmppstatus.get('status')).toBe('chat'); - expect(_.partial(_converse.api.user.status.set, 'invalid')).toThrow( - new Error('Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1') - ); - done(); - })); - - it("allows setting the status message as well", mock.initConverse((done, _converse) => { - _converse.api.user.status.set('away', "I'm in a meeting"); - expect(_converse.xmppstatus.get('status')).toBe('away'); - expect(_converse.xmppstatus.get('status_message')).toBe("I'm in a meeting"); - done(); - })); - - it("has a method for getting the user's status message", mock.initConverse((done, _converse) => { - _converse.xmppstatus.set('status_message', undefined); - expect(_converse.api.user.status.message.get()).toBe(undefined); - _converse.xmppstatus.set('status_message', "I'm in a meeting"); - expect(_converse.api.user.status.message.get()).toBe("I'm in a meeting"); - done(); - })); - - it("has a method for setting the user's status message", mock.initConverse((done, _converse) => { - _converse.xmppstatus.set('status_message', undefined); - _converse.api.user.status.message.set("I'm in a meeting"); - expect(_converse.xmppstatus.get('status_message')).toBe("I'm in a meeting"); - done(); - })); + spyOn(_converse, 'sendCSI').and.callThrough(); + let sent_stanza; + spyOn(_converse.connection, 'send').and.callFake(function (stanza) { + sent_stanza = stanza; }); - }); + let i = 0; + _converse.idle_seconds = 0; // Usually initialized by registerIntervalHandler + _converse.disco_entities.get(_converse.domain).features['urn:xmpp:csi:0'] = true; // Mock that the server supports CSI - describe("The \"tokens\" API", function () { + _converse.api.settings.set('csi_waiting_time', 3); + while (i <= _converse.api.settings.get("csi_waiting_time")) { + expect(_converse.sendCSI).not.toHaveBeenCalled(); + _converse.onEverySecond(); + i++; + } + expect(_converse.sendCSI).toHaveBeenCalledWith('inactive'); + expect(sent_stanza.toLocaleString()).toBe(''); + _converse.onUserActivity(); + expect(_converse.sendCSI).toHaveBeenCalledWith('active'); + expect(sent_stanza.toLocaleString()).toBe(''); + done(); + })); + }); - it("has a method for retrieving the next RID", mock.initConverse((done, _converse) => { - test_utils.createContacts(_converse, 'current'); - const old_connection = _converse.connection; - _converse.connection._proto.rid = '1234'; - expect(_converse.api.tokens.get('rid')).toBe('1234'); - _converse.connection = undefined; - expect(_converse.api.tokens.get('rid')).toBe(null); - // Restore the connection - _converse.connection = old_connection; + describe("Automatic status change", function () { + + it("happens when the client is idle for long enough", mock.initConverse((done, _converse) => { + let i = 0; + // Usually initialized by registerIntervalHandler + _converse.idle_seconds = 0; + _converse.auto_changed_status = false; + _converse.api.settings.set('auto_away', 3); + _converse.api.settings.set('auto_xa', 6); + + expect(_converse.api.user.status.get()).toBe('online'); + while (i <= _converse.api.settings.get("auto_away")) { + _converse.onEverySecond(); i++; + } + expect(_converse.auto_changed_status).toBe(true); + + while (i <= _converse.auto_xa) { + expect(_converse.api.user.status.get()).toBe('away'); + _converse.onEverySecond(); + i++; + } + expect(_converse.api.user.status.get()).toBe('xa'); + expect(_converse.auto_changed_status).toBe(true); + + _converse.onUserActivity(); + expect(_converse.api.user.status.get()).toBe('online'); + expect(_converse.auto_changed_status).toBe(false); + + // Check that it also works for the chat feature + _converse.api.user.status.set('chat') + i = 0; + while (i <= _converse.api.settings.get("auto_away")) { + _converse.onEverySecond(); + i++; + } + expect(_converse.auto_changed_status).toBe(true); + while (i <= _converse.auto_xa) { + expect(_converse.api.user.status.get()).toBe('away'); + _converse.onEverySecond(); + i++; + } + expect(_converse.api.user.status.get()).toBe('xa'); + expect(_converse.auto_changed_status).toBe(true); + + _converse.onUserActivity(); + expect(_converse.api.user.status.get()).toBe('online'); + expect(_converse.auto_changed_status).toBe(false); + + // Check that it doesn't work for 'dnd' + _converse.api.user.status.set('dnd'); + i = 0; + while (i <= _converse.api.settings.get("auto_away")) { + _converse.onEverySecond(); + i++; + } + expect(_converse.api.user.status.get()).toBe('dnd'); + expect(_converse.auto_changed_status).toBe(false); + while (i <= _converse.auto_xa) { + expect(_converse.api.user.status.get()).toBe('dnd'); + _converse.onEverySecond(); + i++; + } + expect(_converse.api.user.status.get()).toBe('dnd'); + expect(_converse.auto_changed_status).toBe(false); + + _converse.onUserActivity(); + expect(_converse.api.user.status.get()).toBe('dnd'); + expect(_converse.auto_changed_status).toBe(false); + done(); + })); + }); + + describe("The \"user\" grouping", function () { + + describe("The \"status\" API", function () { + + it("has a method for getting the user's availability", mock.initConverse((done, _converse) => { + _converse.xmppstatus.set('status', 'online'); + expect(_converse.api.user.status.get()).toBe('online'); + _converse.xmppstatus.set('status', 'dnd'); + expect(_converse.api.user.status.get()).toBe('dnd'); done(); })); - it("has a method for retrieving the SID", mock.initConverse((done, _converse) => { - test_utils.createContacts(_converse, 'current'); - const old_connection = _converse.connection; - _converse.connection._proto.sid = '1234'; - expect(_converse.api.tokens.get('sid')).toBe('1234'); - _converse.connection = undefined; - expect(_converse.api.tokens.get('sid')).toBe(null); - // Restore the connection - _converse.connection = old_connection; + it("has a method for setting the user's availability", mock.initConverse((done, _converse) => { + _converse.api.user.status.set('away'); + expect(_converse.xmppstatus.get('status')).toBe('away'); + _converse.api.user.status.set('dnd'); + expect(_converse.xmppstatus.get('status')).toBe('dnd'); + _converse.api.user.status.set('xa'); + expect(_converse.xmppstatus.get('status')).toBe('xa'); + _converse.api.user.status.set('chat'); + expect(_converse.xmppstatus.get('status')).toBe('chat'); + expect(() => _converse.api.user.status.set('invalid')).toThrow( + new Error('Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1') + ); + done(); + })); + + it("allows setting the status message as well", mock.initConverse((done, _converse) => { + _converse.api.user.status.set('away', "I'm in a meeting"); + expect(_converse.xmppstatus.get('status')).toBe('away'); + expect(_converse.xmppstatus.get('status_message')).toBe("I'm in a meeting"); + done(); + })); + + it("has a method for getting the user's status message", mock.initConverse((done, _converse) => { + _converse.xmppstatus.set('status_message', undefined); + expect(_converse.api.user.status.message.get()).toBe(undefined); + _converse.xmppstatus.set('status_message', "I'm in a meeting"); + expect(_converse.api.user.status.message.get()).toBe("I'm in a meeting"); + done(); + })); + + it("has a method for setting the user's status message", mock.initConverse((done, _converse) => { + _converse.xmppstatus.set('status_message', undefined); + _converse.api.user.status.message.set("I'm in a meeting"); + expect(_converse.xmppstatus.get('status_message')).toBe("I'm in a meeting"); done(); })); }); + }); - describe("The \"contacts\" API", function () { + describe("The \"tokens\" API", function () { - it("has a method 'get' which returns wrapped contacts", - mock.initConverse([], {}, async function (done, _converse) { + it("has a method for retrieving the next RID", mock.initConverse((done, _converse) => { + mock.createContacts(_converse, 'current'); + const old_connection = _converse.connection; + _converse.connection._proto.rid = '1234'; + expect(_converse.api.tokens.get('rid')).toBe('1234'); + _converse.connection = undefined; + expect(_converse.api.tokens.get('rid')).toBe(null); + // Restore the connection + _converse.connection = old_connection; + done(); + })); - await test_utils.waitForRoster(_converse, 'current'); - let contact = await _converse.api.contacts.get('non-existing@jabber.org'); - expect(contact).toBeFalsy(); - // Check when a single jid is given - const jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - contact = await _converse.api.contacts.get(jid); - expect(contact.getDisplayName()).toBe(mock.cur_names[0]); - expect(contact.get('jid')).toBe(jid); - // You can retrieve multiple contacts by passing in an array - const jid2 = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - let list = await _converse.api.contacts.get([jid, jid2]); - expect(Array.isArray(list)).toBeTruthy(); - expect(list[0].getDisplayName()).toBe(mock.cur_names[0]); - expect(list[1].getDisplayName()).toBe(mock.cur_names[1]); - // Check that all JIDs are returned if you call without any parameters - list = await _converse.api.contacts.get(); - expect(list.length).toBe(mock.cur_names.length); - done(); - })); + it("has a method for retrieving the SID", mock.initConverse((done, _converse) => { + mock.createContacts(_converse, 'current'); + const old_connection = _converse.connection; + _converse.connection._proto.sid = '1234'; + expect(_converse.api.tokens.get('sid')).toBe('1234'); + _converse.connection = undefined; + expect(_converse.api.tokens.get('sid')).toBe(null); + // Restore the connection + _converse.connection = old_connection; + done(); + })); + }); - it("has a method 'add' with which contacts can be added", - mock.initConverse(['rosterInitialized'], {}, async (done, _converse) => { + describe("The \"contacts\" API", function () { - await test_utils.waitForRoster(_converse, 'current', 0); - try { - await _converse.api.contacts.add(); - throw new Error('Call should have failed'); - } catch (e) { - expect(e.message).toBe('contacts.add: invalid jid'); + it("has a method 'get' which returns wrapped contacts", + mock.initConverse([], {}, async function (done, _converse) { - } - try { - await _converse.api.contacts.add("invalid jid"); - throw new Error('Call should have failed'); - } catch (e) { - expect(e.message).toBe('contacts.add: invalid jid'); - } - spyOn(_converse.roster, 'addAndSubscribe'); - await _converse.api.contacts.add("newcontact@example.org"); - expect(_converse.roster.addAndSubscribe).toHaveBeenCalled(); - done(); - })); - }); + await mock.waitForRoster(_converse, 'current'); + let contact = await _converse.api.contacts.get('non-existing@jabber.org'); + expect(contact).toBeFalsy(); + // Check when a single jid is given + const jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + contact = await _converse.api.contacts.get(jid); + expect(contact.getDisplayName()).toBe(mock.cur_names[0]); + expect(contact.get('jid')).toBe(jid); + // You can retrieve multiple contacts by passing in an array + const jid2 = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + let list = await _converse.api.contacts.get([jid, jid2]); + expect(Array.isArray(list)).toBeTruthy(); + expect(list[0].getDisplayName()).toBe(mock.cur_names[0]); + expect(list[1].getDisplayName()).toBe(mock.cur_names[1]); + // Check that all JIDs are returned if you call without any parameters + list = await _converse.api.contacts.get(); + expect(list.length).toBe(mock.cur_names.length); + done(); + })); - describe("The \"chats\" API", function() { + it("has a method 'add' with which contacts can be added", + mock.initConverse(['rosterInitialized'], {}, async (done, _converse) => { - it("has a method 'get' which returns the promise that resolves to a chat model", mock.initConverse( + await mock.waitForRoster(_converse, 'current', 0); + try { + await _converse.api.contacts.add(); + throw new Error('Call should have failed'); + } catch (e) { + expect(e.message).toBe('contacts.add: invalid jid'); + + } + try { + await _converse.api.contacts.add("invalid jid"); + throw new Error('Call should have failed'); + } catch (e) { + expect(e.message).toBe('contacts.add: invalid jid'); + } + spyOn(_converse.roster, 'addAndSubscribe'); + await _converse.api.contacts.add("newcontact@example.org"); + expect(_converse.roster.addAndSubscribe).toHaveBeenCalled(); + done(); + })); + }); + + describe("The \"chats\" API", function() { + + it("has a method 'get' which returns the promise that resolves to a chat model", mock.initConverse( ['rosterInitialized', 'chatBoxesInitialized'], {}, async (done, _converse) => { - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current', 2); + const u = converse.env.utils; - // Test on chat that doesn't exist. - let chat = await _converse.api.chats.get('non-existing@jabber.org'); - expect(chat).toBeFalsy(); - const jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const jid2 = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 2); - // Test on chat that's not open - chat = await _converse.api.chats.get(jid); - expect(chat === null).toBeTruthy(); - expect(_converse.chatboxes.length).toBe(1); + // Test on chat that doesn't exist. + let chat = await _converse.api.chats.get('non-existing@jabber.org'); + expect(chat).toBeFalsy(); + const jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const jid2 = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - // Test for one JID - chat = await _converse.api.chats.open(jid); - expect(chat instanceof Object).toBeTruthy(); - expect(chat.get('box_id')).toBe(`box-${btoa(jid)}`); + // Test on chat that's not open + chat = await _converse.api.chats.get(jid); + expect(chat === null).toBeTruthy(); + expect(_converse.chatboxes.length).toBe(1); - const view = _converse.chatboxviews.get(jid); - await u.waitUntil(() => u.isVisible(view.el)); - // Test for multiple JIDs - test_utils.openChatBoxFor(_converse, jid2); - await u.waitUntil(() => _converse.chatboxes.length == 3); - const list = await _converse.api.chats.get([jid, jid2]); - expect(Array.isArray(list)).toBeTruthy(); - expect(list[0].get('box_id')).toBe(`box-${btoa(jid)}`); - expect(list[1].get('box_id')).toBe(`box-${btoa(jid2)}`); - done(); - })); + // Test for one JID + chat = await _converse.api.chats.open(jid); + expect(chat instanceof Object).toBeTruthy(); + expect(chat.get('box_id')).toBe(`box-${btoa(jid)}`); - it("has a method 'open' which opens and returns a promise that resolves to a chat model", mock.initConverse( + const view = _converse.chatboxviews.get(jid); + await u.waitUntil(() => u.isVisible(view.el)); + // Test for multiple JIDs + mock.openChatBoxFor(_converse, jid2); + await u.waitUntil(() => _converse.chatboxes.length == 3); + const list = await _converse.api.chats.get([jid, jid2]); + expect(Array.isArray(list)).toBeTruthy(); + expect(list[0].get('box_id')).toBe(`box-${btoa(jid)}`); + expect(list[1].get('box_id')).toBe(`box-${btoa(jid2)}`); + done(); + })); + + it("has a method 'open' which opens and returns a promise that resolves to a chat model", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesInitialized'], {}, async (done, _converse) => { - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current', 2); + const u = converse.env.utils; + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 2); - const jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const jid2 = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - // Test on chat that doesn't exist. - let chat = await _converse.api.chats.get('non-existing@jabber.org'); - expect(chat).toBeFalsy(); + const jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const jid2 = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + // Test on chat that doesn't exist. + let chat = await _converse.api.chats.get('non-existing@jabber.org'); + expect(chat).toBeFalsy(); - chat = await _converse.api.chats.open(jid); - expect(chat instanceof Object).toBeTruthy(); - expect(chat.get('box_id')).toBe(`box-${btoa(jid)}`); - expect( - Object.keys(chat), - ['close', 'endOTR', 'focus', 'get', 'initiateOTR', 'is_chatroom', 'maximize', 'minimize', 'open', 'set'] - ); - const view = _converse.chatboxviews.get(jid); - await u.waitUntil(() => u.isVisible(view.el)); - // Test for multiple JIDs - const list = await _converse.api.chats.open([jid, jid2]); - expect(Array.isArray(list)).toBeTruthy(); - expect(list[0].get('box_id')).toBe(`box-${btoa(jid)}`); - expect(list[1].get('box_id')).toBe(`box-${btoa(jid2)}`); + chat = await _converse.api.chats.open(jid); + expect(chat instanceof Object).toBeTruthy(); + expect(chat.get('box_id')).toBe(`box-${btoa(jid)}`); + expect( + Object.keys(chat), + ['close', 'endOTR', 'focus', 'get', 'initiateOTR', 'is_chatroom', 'maximize', 'minimize', 'open', 'set'] + ); + const view = _converse.chatboxviews.get(jid); + await u.waitUntil(() => u.isVisible(view.el)); + // Test for multiple JIDs + const list = await _converse.api.chats.open([jid, jid2]); + expect(Array.isArray(list)).toBeTruthy(); + expect(list[0].get('box_id')).toBe(`box-${btoa(jid)}`); + expect(list[1].get('box_id')).toBe(`box-${btoa(jid2)}`); + done(); + })); + }); + + describe("The \"settings\" API", function() { + it("has methods 'get' and 'set' to set configuration settings", + mock.initConverse(null, {'play_sounds': true}, (done, _converse) => { + + expect(Object.keys(_converse.api.settings)).toEqual(["update", "get", "set"]); + expect(_converse.api.settings.get("play_sounds")).toBe(true); + _converse.api.settings.set("play_sounds", false); + expect(_converse.api.settings.get("play_sounds")).toBe(false); + _converse.api.settings.set({"play_sounds": true}); + expect(_converse.api.settings.get("play_sounds")).toBe(true); + // Only whitelisted settings allowed. + expect(typeof _converse.api.settings.get("non_existing")).toBe("undefined"); + _converse.api.settings.set("non_existing", true); + expect(typeof _converse.api.settings.get("non_existing")).toBe("undefined"); + done(); + })); + }); + + describe("The \"plugins\" API", function() { + it("only has a method 'add' for registering plugins", mock.initConverse((done, _converse) => { + expect(Object.keys(converse.plugins)).toEqual(["add"]); + // Cheating a little bit. We clear the plugins to test more easily. + const _old_plugins = _converse.pluggable.plugins; + _converse.pluggable.plugins = []; + converse.plugins.add('plugin1', {}); + expect(Object.keys(_converse.pluggable.plugins)).toEqual(['plugin1']); + converse.plugins.add('plugin2', {}); + expect(Object.keys(_converse.pluggable.plugins)).toEqual(['plugin1', 'plugin2']); + _converse.pluggable.plugins = _old_plugins; + done(); + })); + + describe("The \"plugins.add\" method", function() { + it("throws an error when multiple plugins attempt to register with the same name", + mock.initConverse((done, _converse) => { // eslint-disable-line no-unused-vars + + converse.plugins.add('myplugin', {}); + const error = new TypeError('Error: plugin with name "myplugin" has already been registered!'); + expect(() => converse.plugins.add('myplugin', {})).toThrow(error); done(); })); }); - - describe("The \"settings\" API", function() { - it("has methods 'get' and 'set' to set configuration settings", - mock.initConverse(null, {'play_sounds': true}, (done, _converse) => { - - expect(_.keys(_converse.api.settings)).toEqual(["update", "get", "set"]); - expect(_converse.api.settings.get("play_sounds")).toBe(true); - _converse.api.settings.set("play_sounds", false); - expect(_converse.api.settings.get("play_sounds")).toBe(false); - _converse.api.settings.set({"play_sounds": true}); - expect(_converse.api.settings.get("play_sounds")).toBe(true); - // Only whitelisted settings allowed. - expect(typeof _converse.api.settings.get("non_existing")).toBe("undefined"); - _converse.api.settings.set("non_existing", true); - expect(typeof _converse.api.settings.get("non_existing")).toBe("undefined"); - done(); - })); - }); - - describe("The \"plugins\" API", function() { - it("only has a method 'add' for registering plugins", mock.initConverse((done, _converse) => { - expect(_.keys(converse.plugins)).toEqual(["add"]); - // Cheating a little bit. We clear the plugins to test more easily. - const _old_plugins = _converse.pluggable.plugins; - _converse.pluggable.plugins = []; - converse.plugins.add('plugin1', {}); - expect(_.keys(_converse.pluggable.plugins)).toEqual(['plugin1']); - converse.plugins.add('plugin2', {}); - expect(_.keys(_converse.pluggable.plugins)).toEqual(['plugin1', 'plugin2']); - _converse.pluggable.plugins = _old_plugins; - done(); - })); - - describe("The \"plugins.add\" method", function() { - it("throws an error when multiple plugins attempt to register with the same name", - mock.initConverse((done, _converse) => { // eslint-disable-line no-unused-vars - - converse.plugins.add('myplugin', {}); - const error = new TypeError('Error: plugin with name "myplugin" has already been registered!'); - expect(_.partial(converse.plugins.add, 'myplugin', {})).toThrow(error); - done(); - })); - }); - }); }); }); diff --git a/spec/disco.js b/spec/disco.js index ea76bae59..573d87f7d 100644 --- a/spec/disco.js +++ b/spec/disco.js @@ -1,190 +1,183 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const Strophe = converse.env.Strophe; - const $iq = converse.env.$iq; - const _ = converse.env._; - const u = converse.env.utils; +/*global mock */ - describe("Service Discovery", function () { +describe("Service Discovery", function () { - describe("Whenever converse.js queries a server for its features", function () { + describe("Whenever converse.js queries a server for its features", function () { - it("stores the features it receives", - mock.initConverse( - ['discoInitialized'], {}, - async function (done, _converse) { + it("stores the features it receives", + mock.initConverse( + ['discoInitialized'], {}, + async function (done, _converse) { - const IQ_stanzas = _converse.connection.IQ_stanzas; - const IQ_ids = _converse.connection.IQ_ids; - await u.waitUntil(function () { - return _.filter(IQ_stanzas, function (iq) { - return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'); - }).length > 0; - }); - /* - * - * - * - * - * - * - * - * - * - * - * - * - * - */ - let stanza = _.find(IQ_stanzas, function (iq) { + const { u, $iq } = converse.env; + const IQ_stanzas = _converse.connection.IQ_stanzas; + const IQ_ids = _converse.connection.IQ_ids; + await u.waitUntil(function () { + return IQ_stanzas.filter(function (iq) { return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'); + }).length > 0; + }); + /* + * + * + * + * + * + * + * + * + * + * + * + * + * + */ + let stanza = IQ_stanzas.find(function (iq) { + return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'); + }); + const info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; + stanza = $iq({ + 'type': 'result', + 'from': 'montague.lit', + 'to': 'romeo@montague.lit/orchard', + 'id': info_IQ_id + }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', { + 'category': 'server', + 'type': 'im'}).up() + .c('identity', { + 'category': 'conference', + 'type': 'text', + 'name': 'Play-Specific Chatrooms'}).up() + .c('identity', { + 'category': 'directory', + 'type': 'chatroom', + 'name': 'Play-Specific Chatrooms'}).up() + .c('feature', { + 'var': 'http://jabber.org/protocol/disco#info'}).up() + .c('feature', { + 'var': 'http://jabber.org/protocol/disco#items'}).up() + .c('feature', { + 'var': 'jabber:iq:register'}).up() + .c('feature', { + 'var': 'jabber:iq:time'}).up() + .c('feature', { + 'var': 'jabber:iq:version'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + let entities = await _converse.api.disco.entities.get() + expect(entities.length).toBe(2); // We have an extra entity, which is the user's JID + expect(entities.get(_converse.domain).features.length).toBe(5); + expect(entities.get(_converse.domain).identities.length).toBe(3); + expect(entities.get('montague.lit').features.where({'var': 'jabber:iq:version'}).length).toBe(1); + expect(entities.get('montague.lit').features.where({'var': 'jabber:iq:time'}).length).toBe(1); + expect(entities.get('montague.lit').features.where({'var': 'jabber:iq:register'}).length).toBe(1); + expect(entities.get('montague.lit').features.where( + {'var': 'http://jabber.org/protocol/disco#items'}).length).toBe(1); + expect(entities.get('montague.lit').features.where( + {'var': 'http://jabber.org/protocol/disco#info'}).length).toBe(1); + + await u.waitUntil(function () { + // Converse.js sees that the entity has a disco#items feature, + // so it will make a query for it. + return IQ_stanzas.filter(iq => iq.querySelector('query[xmlns="http://jabber.org/protocol/disco#items"]')).length > 0; + }); + /* + * + * + * + * + * + * + * + * + * + * + * + */ + stanza = IQ_stanzas.find(function (iq) { + return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]'); + }); + const items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; + stanza = $iq({ + 'type': 'result', + 'from': 'montague.lit', + 'to': 'romeo@montague.lit/orchard', + 'id': items_IQ_id + }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'}) + .c('item', { + 'jid': 'people.shakespeare.lit', + 'name': 'Directory of Characters'}).up() + .c('item', { + 'jid': 'plays.shakespeare.lit', + 'name': 'Play-Specific Chatrooms'}).up() + .c('item', { + 'jid': 'words.shakespeare.lit', + 'name': 'Gateway to Marlowe IM'}).up() + + .c('item', { + 'jid': 'montague.lit', + 'node': 'books', + 'name': 'Books by and about Shakespeare'}).up() + .c('item', { + 'node': 'montague.lit', + 'name': 'Wear your literary taste with pride'}).up() + .c('item', { + 'jid': 'montague.lit', + 'node': 'music', + 'name': 'Music from the time of Shakespeare' }); - const info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; - stanza = $iq({ - 'type': 'result', - 'from': 'montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'id': info_IQ_id - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', { - 'category': 'server', - 'type': 'im'}).up() - .c('identity', { - 'category': 'conference', - 'type': 'text', - 'name': 'Play-Specific Chatrooms'}).up() - .c('identity', { - 'category': 'directory', - 'type': 'chatroom', - 'name': 'Play-Specific Chatrooms'}).up() - .c('feature', { - 'var': 'http://jabber.org/protocol/disco#info'}).up() - .c('feature', { - 'var': 'http://jabber.org/protocol/disco#items'}).up() - .c('feature', { - 'var': 'jabber:iq:register'}).up() - .c('feature', { - 'var': 'jabber:iq:time'}).up() - .c('feature', { - 'var': 'jabber:iq:version'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.disco_entities); + entities = _converse.disco_entities; + expect(entities.length).toBe(2); // We have an extra entity, which is the user's JID + expect(entities.get(_converse.domain).items.length).toBe(3); + expect(entities.get(_converse.domain).items.pluck('jid').includes('people.shakespeare.lit')).toBeTruthy(); + expect(entities.get(_converse.domain).items.pluck('jid').includes('plays.shakespeare.lit')).toBeTruthy(); + expect(entities.get(_converse.domain).items.pluck('jid').includes('words.shakespeare.lit')).toBeTruthy(); + expect(entities.get(_converse.domain).identities.where({'category': 'conference'}).length).toBe(1); + expect(entities.get(_converse.domain).identities.where({'category': 'directory'}).length).toBe(1); + done(); + })); + }); - let entities = await _converse.api.disco.entities.get() - expect(entities.length).toBe(2); // We have an extra entity, which is the user's JID - expect(entities.get(_converse.domain).features.length).toBe(5); - expect(entities.get(_converse.domain).identities.length).toBe(3); - expect(entities.get('montague.lit').features.where({'var': 'jabber:iq:version'}).length).toBe(1); - expect(entities.get('montague.lit').features.where({'var': 'jabber:iq:time'}).length).toBe(1); - expect(entities.get('montague.lit').features.where({'var': 'jabber:iq:register'}).length).toBe(1); - expect(entities.get('montague.lit').features.where( - {'var': 'http://jabber.org/protocol/disco#items'}).length).toBe(1); - expect(entities.get('montague.lit').features.where( - {'var': 'http://jabber.org/protocol/disco#info'}).length).toBe(1); + describe("Whenever converse.js discovers a new server feature", function () { + it("emits the serviceDiscovered event", + mock.initConverse( + ['discoInitialized'], {}, + function (done, _converse) { - await u.waitUntil(function () { - // Converse.js sees that the entity has a disco#items feature, - // so it will make a query for it. - return _.filter(IQ_stanzas, function (iq) { - return iq.querySelector('query[xmlns="http://jabber.org/protocol/disco#items"]'); - }).length > 0; - }); - /* - * - * - * - * - * - * - * - * - * - * - * - */ - stanza = _.find(IQ_stanzas, function (iq) { - return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]'); - }); - var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; - stanza = $iq({ - 'type': 'result', - 'from': 'montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'id': items_IQ_id - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'}) - .c('item', { - 'jid': 'people.shakespeare.lit', - 'name': 'Directory of Characters'}).up() - .c('item', { - 'jid': 'plays.shakespeare.lit', - 'name': 'Play-Specific Chatrooms'}).up() - .c('item', { - 'jid': 'words.shakespeare.lit', - 'name': 'Gateway to Marlowe IM'}).up() - - .c('item', { - 'jid': 'montague.lit', - 'node': 'books', - 'name': 'Books by and about Shakespeare'}).up() - .c('item', { - 'node': 'montague.lit', - 'name': 'Wear your literary taste with pride'}).up() - .c('item', { - 'jid': 'montague.lit', - 'node': 'music', - 'name': 'Music from the time of Shakespeare' - }); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.disco_entities); - entities = _converse.disco_entities; - expect(entities.length).toBe(2); // We have an extra entity, which is the user's JID - expect(entities.get(_converse.domain).items.length).toBe(3); - expect(_.includes(entities.get(_converse.domain).items.pluck('jid'), 'people.shakespeare.lit')).toBeTruthy(); - expect(_.includes(entities.get(_converse.domain).items.pluck('jid'), 'plays.shakespeare.lit')).toBeTruthy(); - expect(_.includes(entities.get(_converse.domain).items.pluck('jid'), 'words.shakespeare.lit')).toBeTruthy(); - expect(entities.get(_converse.domain).identities.where({'category': 'conference'}).length).toBe(1); - expect(entities.get(_converse.domain).identities.where({'category': 'directory'}).length).toBe(1); - done(); - })); - }); - - describe("Whenever converse.js discovers a new server feature", function () { - it("emits the serviceDiscovered event", - mock.initConverse( - ['discoInitialized'], {}, - function (done, _converse) { - - sinon.spy(_converse.api, "trigger"); - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); - expect(_converse.api.trigger.called).toBe(true); - expect(_converse.api.trigger.args[0][0]).toBe('serviceDiscovered'); - expect(_converse.api.trigger.args[0][1].get('var')).toBe(Strophe.NS.MAM); - done(); - })); - }); + const { Strophe } = converse.env; + sinon.spy(_converse.api, "trigger"); + _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); + expect(_converse.api.trigger.called).toBe(true); + expect(_converse.api.trigger.args[0][0]).toBe('serviceDiscovered'); + expect(_converse.api.trigger.args[0][1].get('var')).toBe(Strophe.NS.MAM); + done(); + })); }); }); diff --git a/spec/emojis.js b/spec/emojis.js index 6163cc7e0..85f50afca 100644 --- a/spec/emojis.js +++ b/spec/emojis.js @@ -1,238 +1,236 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const { Promise, $msg, $pres, sizzle } = converse.env; - const u = converse.env.utils; +/*global mock */ - describe("Emojis", function () { - describe("The emoji picker", function () { +const { Promise, $msg, $pres, sizzle } = converse.env; +const u = converse.env.utils; - it("can be opened by clicking a button in the chat toolbar", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { +describe("Emojis", function () { + describe("The emoji picker", function () { - const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - const toolbar = await u.waitUntil(() => view.el.querySelector('ul.chat-toolbar')); - expect(toolbar.querySelectorAll('li.toggle-smiley__container').length).toBe(1); - toolbar.querySelector('a.toggle-smiley').click(); - await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')), 1000); - const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container'), 1000); - const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a'), 1000); - item.click() - expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':smiley: '); - toolbar.querySelector('a.toggle-smiley').click(); // Close the panel again - done(); - })); + it("can be opened by clicking a button in the chat toolbar", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - it("is opened to autocomplete emojis in the textarea", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + const toolbar = await u.waitUntil(() => view.el.querySelector('ul.chat-toolbar')); + expect(toolbar.querySelectorAll('li.toggle-smiley__container').length).toBe(1); + toolbar.querySelector('a.toggle-smiley').click(); + await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')), 1000); + const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container'), 1000); + const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a'), 1000); + item.click() + expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':smiley: '); + toolbar.querySelector('a.toggle-smiley').click(); // Close the panel again + done(); + })); - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.chatboxviews.get(muc_jid); + it("is opened to autocomplete emojis in the textarea", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - const textarea = view.el.querySelector('textarea.chat-textarea'); - textarea.value = ':gri'; + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.chatboxviews.get(muc_jid); - // Press tab - const tab_event = { - 'target': textarea, - 'preventDefault': function preventDefault () {}, - 'stopPropagation': function stopPropagation () {}, - 'keyCode': 9, - 'key': 'Tab' - } - view.onKeyDown(tab_event); - await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists'))); - let picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container')); - const input = picker.querySelector('.emoji-search'); - expect(input.value).toBe(':gri'); - let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker); - expect(visible_emojis.length).toBe(3); - expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':grimacing:'); - expect(visible_emojis[1].getAttribute('data-emoji')).toBe(':grin:'); - expect(visible_emojis[2].getAttribute('data-emoji')).toBe(':grinning:'); + const textarea = view.el.querySelector('textarea.chat-textarea'); + textarea.value = ':gri'; - // Test that TAB autocompletes the to first match - view.emoji_picker_view.onKeyDown(tab_event); - visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker); - expect(visible_emojis.length).toBe(1); - expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':grimacing:'); - expect(input.value).toBe(':grimacing:'); + // Press tab + const tab_event = { + 'target': textarea, + 'preventDefault': function preventDefault () {}, + 'stopPropagation': function stopPropagation () {}, + 'keyCode': 9, + 'key': 'Tab' + } + view.onKeyDown(tab_event); + await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists'))); + let picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container')); + const input = picker.querySelector('.emoji-search'); + expect(input.value).toBe(':gri'); + let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker); + expect(visible_emojis.length).toBe(3); + expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':grimacing:'); + expect(visible_emojis[1].getAttribute('data-emoji')).toBe(':grin:'); + expect(visible_emojis[2].getAttribute('data-emoji')).toBe(':grinning:'); - // Check that ENTER now inserts the match - const enter_event = Object.assign({}, tab_event, {'keyCode': 13, 'key': 'Enter', 'target': input}); - view.emoji_picker_view.onKeyDown(enter_event); - expect(input.value).toBe(''); - expect(textarea.value).toBe(':grimacing: '); + // Test that TAB autocompletes the to first match + view.emoji_picker_view.onKeyDown(tab_event); + visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker); + expect(visible_emojis.length).toBe(1); + expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':grimacing:'); + expect(input.value).toBe(':grimacing:'); - // Test that username starting with : doesn't cause issues - const presence = $pres({ - 'from': `${muc_jid}/:username`, - 'id': '27C55F89-1C6A-459A-9EB5-77690145D624', - 'to': _converse.jid - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'some1@montague.lit', - 'affiliation': 'member', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); + // Check that ENTER now inserts the match + const enter_event = Object.assign({}, tab_event, {'keyCode': 13, 'key': 'Enter', 'target': input}); + view.emoji_picker_view.onKeyDown(enter_event); + expect(input.value).toBe(''); + expect(textarea.value).toBe(':grimacing: '); - textarea.value = ':use'; - view.onKeyDown(tab_event); - await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists'))); - picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container')); - expect(input.value).toBe(':use'); - visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker); - expect(visible_emojis.length).toBe(0); - done(); - })); + // Test that username starting with : doesn't cause issues + const presence = $pres({ + 'from': `${muc_jid}/:username`, + 'id': '27C55F89-1C6A-459A-9EB5-77690145D624', + 'to': _converse.jid + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'some1@montague.lit', + 'affiliation': 'member', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + + textarea.value = ':use'; + view.onKeyDown(tab_event); + await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists'))); + picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container')); + expect(input.value).toBe(':use'); + visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker); + expect(visible_emojis.length).toBe(0); + done(); + })); - it("allows you to search for particular emojis", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("allows you to search for particular emojis", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.chatboxviews.get(muc_jid); - const toolbar = view.el.querySelector('ul.chat-toolbar'); - expect(toolbar.querySelectorAll('.toggle-smiley__container').length).toBe(1); - toolbar.querySelector('.toggle-smiley').click(); - await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists'))); - const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container')); - const input = picker.querySelector('.emoji-search'); - expect(sizzle('.insert-emoji:not(.hidden)', picker).length).toBe(1589); + const view = _converse.chatboxviews.get(muc_jid); + const toolbar = view.el.querySelector('ul.chat-toolbar'); + expect(toolbar.querySelectorAll('.toggle-smiley__container').length).toBe(1); + toolbar.querySelector('.toggle-smiley').click(); + await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists'))); + const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container')); + const input = picker.querySelector('.emoji-search'); + expect(sizzle('.insert-emoji:not(.hidden)', picker).length).toBe(1589); - expect(view.emoji_picker_view.model.get('query')).toBeUndefined(); - input.value = 'smiley'; - const event = { - 'target': input, - 'preventDefault': function preventDefault () {}, - 'stopPropagation': function stopPropagation () {} - }; - view.emoji_picker_view.onKeyDown(event); - await u.waitUntil(() => view.emoji_picker_view.model.get('query') === 'smiley'); - let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker); - expect(visible_emojis.length).toBe(2); - expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':smiley:'); - expect(visible_emojis[1].getAttribute('data-emoji')).toBe(':smiley_cat:'); + expect(view.emoji_picker_view.model.get('query')).toBeUndefined(); + input.value = 'smiley'; + const event = { + 'target': input, + 'preventDefault': function preventDefault () {}, + 'stopPropagation': function stopPropagation () {} + }; + view.emoji_picker_view.onKeyDown(event); + await u.waitUntil(() => view.emoji_picker_view.model.get('query') === 'smiley'); + let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker); + expect(visible_emojis.length).toBe(2); + expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':smiley:'); + expect(visible_emojis[1].getAttribute('data-emoji')).toBe(':smiley_cat:'); - // Check that pressing enter without an unambiguous match does nothing - const enter_event = Object.assign({}, event, {'keyCode': 13}); - view.emoji_picker_view.onKeyDown(enter_event); - expect(input.value).toBe('smiley'); + // Check that pressing enter without an unambiguous match does nothing + const enter_event = Object.assign({}, event, {'keyCode': 13}); + view.emoji_picker_view.onKeyDown(enter_event); + expect(input.value).toBe('smiley'); - // Test that TAB autocompletes the to first match - const tab_event = Object.assign({}, event, {'keyCode': 9, 'key': 'Tab'}); - view.emoji_picker_view.onKeyDown(tab_event); - expect(input.value).toBe(':smiley:'); - visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker); - expect(visible_emojis.length).toBe(1); - expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':smiley:'); + // Test that TAB autocompletes the to first match + const tab_event = Object.assign({}, event, {'keyCode': 9, 'key': 'Tab'}); + view.emoji_picker_view.onKeyDown(tab_event); + expect(input.value).toBe(':smiley:'); + visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker); + expect(visible_emojis.length).toBe(1); + expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':smiley:'); - // Check that ENTER now inserts the match - view.emoji_picker_view.onKeyDown(enter_event); - expect(input.value).toBe(''); - expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':smiley: '); - done(); - })); - }); + // Check that ENTER now inserts the match + view.emoji_picker_view.onKeyDown(enter_event); + expect(input.value).toBe(''); + expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':smiley: '); + done(); + })); + }); - describe("A Chat Message", function () { - it("will display larger if it's only emojis", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'use_system_emojis': true}, - async function (done, _converse) { + describe("A Chat Message", function () { + it("will display larger if it's only emojis", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'use_system_emojis': true}, + async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); - const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - _converse.handleMessageStanza($msg({ - 'from': sender_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': _converse.connection.getUniqueId() - }).c('body').t('😇').up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); - await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve)); - const view = _converse.api.chatviews.get(sender_jid); - await new Promise(resolve => view.once('messageInserted', resolve)); - let message = view.content.querySelector('.chat-msg__text'); - expect(u.hasClass('chat-msg__text--larger', message)).toBe(true); + await mock.waitForRoster(_converse, 'current'); + const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + _converse.handleMessageStanza($msg({ + 'from': sender_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': _converse.connection.getUniqueId() + }).c('body').t('😇').up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve)); + const view = _converse.api.chatviews.get(sender_jid); + await new Promise(resolve => view.once('messageInserted', resolve)); + let message = view.content.querySelector('.chat-msg__text'); + expect(u.hasClass('chat-msg__text--larger', message)).toBe(true); - _converse.handleMessageStanza($msg({ - 'from': sender_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': _converse.connection.getUniqueId() - }).c('body').t('😇 Hello world! 😇 😇').up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); - await new Promise(resolve => view.once('messageInserted', resolve)); - message = view.content.querySelector('.message:last-child .chat-msg__text'); - expect(u.hasClass('chat-msg__text--larger', message)).toBe(false); + _converse.handleMessageStanza($msg({ + 'from': sender_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': _converse.connection.getUniqueId() + }).c('body').t('😇 Hello world! 😇 😇').up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + await new Promise(resolve => view.once('messageInserted', resolve)); + message = view.content.querySelector('.message:last-child .chat-msg__text'); + expect(u.hasClass('chat-msg__text--larger', message)).toBe(false); - // Test that a modified message that no longer contains only - // emojis now renders normally again. - const textarea = view.el.querySelector('textarea.chat-textarea'); - textarea.value = ':poop: :innocent:'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(3); - expect(view.content.querySelector('.message:last-child .chat-msg__text').textContent).toBe('💩 😇'); - expect(textarea.value).toBe(''); - view.onKeyDown({ - target: textarea, - keyCode: 38 // Up arrow - }); - expect(textarea.value).toBe('💩 😇'); - expect(view.model.messages.at(2).get('correcting')).toBe(true); - await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg:last-child')), 500); - textarea.value = textarea.value += 'This is no longer an emoji-only message'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); - expect(view.model.messages.models.length).toBe(3); - message = view.content.querySelector('.message:last-child .chat-msg__text'); - expect(u.hasClass('chat-msg__text--larger', message)).toBe(false); + // Test that a modified message that no longer contains only + // emojis now renders normally again. + const textarea = view.el.querySelector('textarea.chat-textarea'); + textarea.value = ':poop: :innocent:'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(3); + expect(view.content.querySelector('.message:last-child .chat-msg__text').textContent).toBe('💩 😇'); + expect(textarea.value).toBe(''); + view.onKeyDown({ + target: textarea, + keyCode: 38 // Up arrow + }); + expect(textarea.value).toBe('💩 😇'); + expect(view.model.messages.at(2).get('correcting')).toBe(true); + await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg:last-child')), 500); + textarea.value = textarea.value += 'This is no longer an emoji-only message'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); + expect(view.model.messages.models.length).toBe(3); + message = view.content.querySelector('.message:last-child .chat-msg__text'); + expect(u.hasClass('chat-msg__text--larger', message)).toBe(false); - textarea.value = ':smile: Hello world!'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - await new Promise(resolve => view.once('messageInserted', resolve)); + textarea.value = ':smile: Hello world!'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + await new Promise(resolve => view.once('messageInserted', resolve)); - textarea.value = ':smile: :smiley: :imp:'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - await new Promise(resolve => view.once('messageInserted', resolve)); + textarea.value = ':smile: :smiley: :imp:'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + await new Promise(resolve => view.once('messageInserted', resolve)); - message = view.content.querySelector('.message:last-child .chat-msg__text'); - expect(u.hasClass('chat-msg__text--larger', message)).toBe(true); - done() - })); - }); + message = view.content.querySelector('.message:last-child .chat-msg__text'); + expect(u.hasClass('chat-msg__text--larger', message)).toBe(true); + done() + })); }); }); diff --git a/spec/eventemitter.js b/spec/eventemitter.js index 5107cd52b..fa4873242 100644 --- a/spec/eventemitter.js +++ b/spec/eventemitter.js @@ -1,63 +1,61 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; +/*global mock */ - return describe("The _converse Event Emitter", function() { +describe("The _converse Event Emitter", function() { - it("allows you to subscribe to emitted events", mock.initConverse((done, _converse) => { - this.callback = function () {}; - spyOn(this, 'callback'); - _converse.on('connected', this.callback); - _converse.api.trigger('connected'); - expect(this.callback).toHaveBeenCalled(); - _converse.api.trigger('connected'); - expect(this.callback.calls.count(), 2); - _converse.api.trigger('connected'); - expect(this.callback.calls.count(), 3); - done(); - })); + it("allows you to subscribe to emitted events", mock.initConverse((done, _converse) => { + this.callback = function () {}; + spyOn(this, 'callback'); + _converse.on('connected', this.callback); + _converse.api.trigger('connected'); + expect(this.callback).toHaveBeenCalled(); + _converse.api.trigger('connected'); + expect(this.callback.calls.count(), 2); + _converse.api.trigger('connected'); + expect(this.callback.calls.count(), 3); + done(); + })); - it("allows you to listen once for an emitted event", mock.initConverse((done, _converse) => { - this.callback = function () {}; - spyOn(this, 'callback'); - _converse.once('connected', this.callback); - _converse.api.trigger('connected'); - expect(this.callback).toHaveBeenCalled(); - _converse.api.trigger('connected'); - expect(this.callback.calls.count(), 1); - _converse.api.trigger('connected'); - expect(this.callback.calls.count(), 1); - done(); - })); + it("allows you to listen once for an emitted event", mock.initConverse((done, _converse) => { + this.callback = function () {}; + spyOn(this, 'callback'); + _converse.once('connected', this.callback); + _converse.api.trigger('connected'); + expect(this.callback).toHaveBeenCalled(); + _converse.api.trigger('connected'); + expect(this.callback.calls.count(), 1); + _converse.api.trigger('connected'); + expect(this.callback.calls.count(), 1); + done(); + })); - it("allows you to stop listening or subscribing to an event", mock.initConverse((done, _converse) => { - this.callback = function () {}; - this.anotherCallback = function () {}; - this.neverCalled = function () {}; + it("allows you to stop listening or subscribing to an event", mock.initConverse((done, _converse) => { + this.callback = function () {}; + this.anotherCallback = function () {}; + this.neverCalled = function () {}; - spyOn(this, 'callback'); - spyOn(this, 'anotherCallback'); - spyOn(this, 'neverCalled'); - _converse.on('connected', this.callback); - _converse.on('connected', this.anotherCallback); + spyOn(this, 'callback'); + spyOn(this, 'anotherCallback'); + spyOn(this, 'neverCalled'); + _converse.on('connected', this.callback); + _converse.on('connected', this.anotherCallback); - _converse.api.trigger('connected'); - expect(this.callback).toHaveBeenCalled(); - expect(this.anotherCallback).toHaveBeenCalled(); + _converse.api.trigger('connected'); + expect(this.callback).toHaveBeenCalled(); + expect(this.anotherCallback).toHaveBeenCalled(); - _converse.off('connected', this.callback); + _converse.off('connected', this.callback); - _converse.api.trigger('connected'); - expect(this.callback.calls.count(), 1); - expect(this.anotherCallback.calls.count(), 2); + _converse.api.trigger('connected'); + expect(this.callback.calls.count(), 1); + expect(this.anotherCallback.calls.count(), 2); - _converse.once('connected', this.neverCalled); - _converse.off('connected', this.neverCalled); + _converse.once('connected', this.neverCalled); + _converse.off('connected', this.neverCalled); - _converse.api.trigger('connected'); - expect(this.callback.calls.count(), 1); - expect(this.anotherCallback.calls.count(), 3); - expect(this.neverCalled).not.toHaveBeenCalled(); - done(); - })); - }); + _converse.api.trigger('connected'); + expect(this.callback.calls.count(), 1); + expect(this.anotherCallback.calls.count(), 3); + expect(this.neverCalled).not.toHaveBeenCalled(); + done(); + })); }); diff --git a/spec/hats.js b/spec/hats.js index ee3b97c35..17130294f 100644 --- a/spec/hats.js +++ b/spec/hats.js @@ -1,80 +1,78 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const u = converse.env.utils; +/*global mock */ - describe("A XEP-0317 MUC Hat", function () { +const u = converse.env.utils; - it("can be included in a presence stanza", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { +describe("A XEP-0317 MUC Hat", function () { - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.chatboxviews.get(muc_jid); - const hat1_id = u.getUniqueId(); - const hat2_id = u.getUniqueId(); - _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(` - - - - - - - - - - `))); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "romeo and Terry have entered the groupchat"); + it("can be included in a presence stanza", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - let hats = view.model.getOccupant("Terry").get('hats'); - expect(hats.length).toBe(2); - expect(hats.map(h => h.title).join(' ')).toBe("Teacher's Assistant Dark Mage"); + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.chatboxviews.get(muc_jid); + const hat1_id = u.getUniqueId(); + const hat2_id = u.getUniqueId(); + _converse.connection._dataRecv(mock.createRequest(u.toStanza(` + + + + + + + + + + `))); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === + "romeo and Terry have entered the groupchat"); - _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(` - - Hello world - - `))); + let hats = view.model.getOccupant("Terry").get('hats'); + expect(hats.length).toBe(2); + expect(hats.map(h => h.title).join(' ')).toBe("Teacher's Assistant Dark Mage"); - const msg_el = await u.waitUntil(() => view.el.querySelector('.chat-msg')); - let badges = Array.from(msg_el.querySelectorAll('.badge')); - expect(badges.length).toBe(2); - expect(badges.map(b => b.textContent.trim()).join(' ' )).toBe("Teacher's Assistant Dark Mage"); + _converse.connection._dataRecv(mock.createRequest(u.toStanza(` + + Hello world + + `))); - const hat3_id = u.getUniqueId(); - _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(` - - - - - - - - - - - `))); + const msg_el = await u.waitUntil(() => view.el.querySelector('.chat-msg')); + let badges = Array.from(msg_el.querySelectorAll('.badge')); + expect(badges.length).toBe(2); + expect(badges.map(b => b.textContent.trim()).join(' ' )).toBe("Teacher's Assistant Dark Mage"); - await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 3); - hats = view.model.getOccupant("Terry").get('hats'); - expect(hats.map(h => h.title).join(' ')).toBe("Teacher's Assistant Dark Mage Mad hatter"); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg .badge').length === 3); - badges = Array.from(view.el.querySelectorAll('.chat-msg .badge')); - expect(badges.map(b => b.textContent.trim()).join(' ' )).toBe("Teacher's Assistant Dark Mage Mad hatter"); + const hat3_id = u.getUniqueId(); + _converse.connection._dataRecv(mock.createRequest(u.toStanza(` + + + + + + + + + + + `))); - _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(` - - - - - - `))); - await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 0); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg .badge').length === 0); - done(); - })); - }) -}); + await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 3); + hats = view.model.getOccupant("Terry").get('hats'); + expect(hats.map(h => h.title).join(' ')).toBe("Teacher's Assistant Dark Mage Mad hatter"); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg .badge').length === 3); + badges = Array.from(view.el.querySelectorAll('.chat-msg .badge')); + expect(badges.map(b => b.textContent.trim()).join(' ' )).toBe("Teacher's Assistant Dark Mage Mad hatter"); + + _converse.connection._dataRecv(mock.createRequest(u.toStanza(` + + + + + + `))); + await u.waitUntil(() => view.model.getOccupant("Terry").get('hats').length === 0); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg .badge').length === 0); + done(); + })); +}) diff --git a/spec/headline.js b/spec/headline.js index dc1481fa2..57600b5a0 100644 --- a/spec/headline.js +++ b/spec/headline.js @@ -1,177 +1,176 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const $msg = converse.env.$msg, - _ = converse.env._, - u = converse.env.utils; +/*global mock */ - describe("A headlines box", function () { +describe("A headlines box", function () { - it("will not open nor display non-headline messages", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) { + it("will not open nor display non-headline messages", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) { - /* XMPP spam message: - * - * - * -wwdmz - * SORRY FOR THIS ADVERT + * -wwdmz + * SORRY FOR THIS ADVERT - * SIEVE - * <juliet@example.com> You got mail. - * - * - * imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18 - * - * - * - */ - sinon.spy(u, 'isHeadlineMessage'); - const stanza = $msg({ - 'type': 'headline', - 'from': 'notify.example.com', - 'to': 'romeo@montague.lit', - 'xml:lang': 'en' - }) - .c('subject').t('SIEVE').up() - .c('body').t('<juliet@example.com> You got mail.').up() - .c('x', {'xmlns': 'jabber:x:oob'}) - .c('url').t('imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18'); - - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.chatboxviews.keys().includes('notify.example.com')); - expect(u.isHeadlineMessage.called).toBeTruthy(); - expect(u.isHeadlineMessage.returned(true)).toBeTruthy(); - u.isHeadlineMessage.restore(); // unwraps - const view = _converse.chatboxviews.get('notify.example.com'); - expect(view.model.get('show_avatar')).toBeFalsy(); - expect(view.el.querySelector('img.avatar')).toBe(null); - done(); - })); - - it("will show headline messages in the controlbox", mock.initConverse( + it("will open and display headline messages", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { - /* - * SIEVE - * <juliet@example.com> You got mail. - * - * - * imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18 - * - * - * - */ - const stanza = $msg({ - 'type': 'headline', - 'from': 'notify.example.com', - 'to': 'romeo@montague.lit', - 'xml:lang': 'en' - }) - .c('subject').t('SIEVE').up() - .c('body').t('<juliet@example.com> You got mail.').up() - .c('x', {'xmlns': 'jabber:x:oob'}) - .c('url').t('imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18'); + const { u, $msg} = converse.env; + /* + * SIEVE + * <juliet@example.com> You got mail. + * + * + * imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18 + * + * + * + */ + sinon.spy(u, 'isHeadlineMessage'); + const stanza = $msg({ + 'type': 'headline', + 'from': 'notify.example.com', + 'to': 'romeo@montague.lit', + 'xml:lang': 'en' + }) + .c('subject').t('SIEVE').up() + .c('body').t('<juliet@example.com> You got mail.').up() + .c('x', {'xmlns': 'jabber:x:oob'}) + .c('url').t('imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - const view = _converse.chatboxviews.get('controlbox'); - await u.waitUntil(() => view.el.querySelectorAll(".open-headline").length); - expect(view.el.querySelectorAll('.open-headline').length).toBe(1); - expect(view.el.querySelector('.open-headline').text).toBe('notify.example.com'); - done(); - })); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.chatboxviews.keys().includes('notify.example.com')); + expect(u.isHeadlineMessage.called).toBeTruthy(); + expect(u.isHeadlineMessage.returned(true)).toBeTruthy(); + u.isHeadlineMessage.restore(); // unwraps + const view = _converse.chatboxviews.get('notify.example.com'); + expect(view.model.get('show_avatar')).toBeFalsy(); + expect(view.el.querySelector('img.avatar')).toBe(null); + done(); + })); - it("will remove headline messages from the controlbox if closed", mock.initConverse( - ['rosterGroupsFetched'], {}, async function (done, _converse) { + it("will show headline messages in the controlbox", mock.initConverse( + ['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.openControlBox(_converse); - /* - * SIEVE - * <juliet@example.com> You got mail. - * - * - * imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18 - * - * - * - */ - const stanza = $msg({ - 'type': 'headline', - 'from': 'notify.example.com', - 'to': 'romeo@montague.lit', - 'xml:lang': 'en' - }) - .c('subject').t('SIEVE').up() - .c('body').t('<juliet@example.com> You got mail.').up() - .c('x', {'xmlns': 'jabber:x:oob'}) - .c('url').t('imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18'); + const { u, $msg} = converse.env; + /* + * SIEVE + * <juliet@example.com> You got mail. + * + * + * imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18 + * + * + * + */ + const stanza = $msg({ + 'type': 'headline', + 'from': 'notify.example.com', + 'to': 'romeo@montague.lit', + 'xml:lang': 'en' + }) + .c('subject').t('SIEVE').up() + .c('body').t('<juliet@example.com> You got mail.').up() + .c('x', {'xmlns': 'jabber:x:oob'}) + .c('url').t('imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - const cbview = _converse.chatboxviews.get('controlbox'); - await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length); - const hlview = _converse.chatboxviews.get('notify.example.com'); - await u.isVisible(hlview.el); - const close_el = await u.waitUntil(() => hlview.el.querySelector('.close-chatbox-button')); - close_el.click(); - await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length === 0); - expect(cbview.el.querySelectorAll('.open-headline').length).toBe(0); - done(); - })); + _converse.connection._dataRecv(mock.createRequest(stanza)); + const view = _converse.chatboxviews.get('controlbox'); + await u.waitUntil(() => view.el.querySelectorAll(".open-headline").length); + expect(view.el.querySelectorAll('.open-headline').length).toBe(1); + expect(view.el.querySelector('.open-headline').text).toBe('notify.example.com'); + done(); + })); - it("will not show a headline messages from a full JID if allow_non_roster_messaging is false", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) { + it("will remove headline messages from the controlbox if closed", mock.initConverse( + ['rosterGroupsFetched'], {}, async function (done, _converse) { - _converse.allow_non_roster_messaging = false; - sinon.spy(u, 'isHeadlineMessage'); - const stanza = $msg({ - 'type': 'headline', - 'from': 'andre5114@jabber.snc.ru/Spark', - 'to': 'romeo@montague.lit', - 'xml:lang': 'en' - }) - .c('nick').t('gpocy').up() - .c('body').t('Здравствуйте друзья'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_.without('controlbox', _converse.chatboxviews.keys()).length).toBe(0); - expect(u.isHeadlineMessage.called).toBeTruthy(); - expect(u.isHeadlineMessage.returned(true)).toBeTruthy(); - u.isHeadlineMessage.restore(); // unwraps - done(); - })); - }); + const { u, $msg} = converse.env; + await mock.openControlBox(_converse); + /* + * SIEVE + * <juliet@example.com> You got mail. + * + * + * imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18 + * + * + * + */ + const stanza = $msg({ + 'type': 'headline', + 'from': 'notify.example.com', + 'to': 'romeo@montague.lit', + 'xml:lang': 'en' + }) + .c('subject').t('SIEVE').up() + .c('body').t('<juliet@example.com> You got mail.').up() + .c('x', {'xmlns': 'jabber:x:oob'}) + .c('url').t('imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18'); + + _converse.connection._dataRecv(mock.createRequest(stanza)); + const cbview = _converse.chatboxviews.get('controlbox'); + await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length); + const hlview = _converse.chatboxviews.get('notify.example.com'); + await u.isVisible(hlview.el); + const close_el = await u.waitUntil(() => hlview.el.querySelector('.close-chatbox-button')); + close_el.click(); + await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length === 0); + expect(cbview.el.querySelectorAll('.open-headline').length).toBe(0); + done(); + })); + + it("will not show a headline messages from a full JID if allow_non_roster_messaging is false", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) { + + const { u, $msg, _ } = converse.env; + _converse.allow_non_roster_messaging = false; + sinon.spy(u, 'isHeadlineMessage'); + const stanza = $msg({ + 'type': 'headline', + 'from': 'andre5114@jabber.snc.ru/Spark', + 'to': 'romeo@montague.lit', + 'xml:lang': 'en' + }) + .c('nick').t('gpocy').up() + .c('body').t('Здравствуйте друзья'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_.without('controlbox', _converse.chatboxviews.keys()).length).toBe(0); + expect(u.isHeadlineMessage.called).toBeTruthy(); + expect(u.isHeadlineMessage.returned(true)).toBeTruthy(); + u.isHeadlineMessage.restore(); // unwraps + done(); + })); }); diff --git a/spec/http-file-upload.js b/spec/http-file-upload.js index 1ad70c97a..5a43fb9ba 100644 --- a/spec/http-file-upload.js +++ b/spec/http-file-upload.js @@ -1,573 +1,244 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const Strophe = converse.env.Strophe; - const $iq = converse.env.$iq; - const _ = converse.env._; - const sizzle = converse.env.sizzle; - const u = converse.env.utils; +/*global mock */ - describe("XEP-0363: HTTP File Upload", function () { +const Strophe = converse.env.Strophe; +const $iq = converse.env.$iq; +const _ = converse.env._; +const sizzle = converse.env.sizzle; +const u = converse.env.utils; - describe("Discovering support", function () { +describe("XEP-0363: HTTP File Upload", function () { - it("is done automatically", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - const IQ_stanzas = _converse.connection.IQ_stanzas; - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []); - let selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'; - let stanza = await u.waitUntil(() => IQ_stanzas.find(iq => iq.querySelector(selector)), 1000); + describe("Discovering support", function () { - /* - * - * - * - * - * - * - */ - stanza = $iq({ - 'type': 'result', - 'from': 'montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'id': stanza.getAttribute('id'), - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', { - 'category': 'server', - 'type': 'im'}).up() - .c('feature', { - 'var': 'http://jabber.org/protocol/disco#info'}).up() - .c('feature', { - 'var': 'http://jabber.org/protocol/disco#items'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); + it("is done automatically", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + const IQ_stanzas = _converse.connection.IQ_stanzas; + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []); + let selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'; + let stanza = await u.waitUntil(() => IQ_stanzas.find(iq => iq.querySelector(selector)), 1000); - let entities = await _converse.api.disco.entities.get(); + /* + * + * + * + * + * + * + */ + stanza = $iq({ + 'type': 'result', + 'from': 'montague.lit', + 'to': 'romeo@montague.lit/orchard', + 'id': stanza.getAttribute('id'), + }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', { + 'category': 'server', + 'type': 'im'}).up() + .c('feature', { + 'var': 'http://jabber.org/protocol/disco#info'}).up() + .c('feature', { + 'var': 'http://jabber.org/protocol/disco#items'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + let entities = await _converse.api.disco.entities.get(); + expect(entities.length).toBe(2); + expect(entities.pluck('jid').includes('montague.lit')).toBe(true); + expect(entities.pluck('jid').includes('romeo@montague.lit')).toBe(true); + + expect(entities.get(_converse.domain).features.length).toBe(2); + expect(entities.get(_converse.domain).identities.length).toBe(1); + + // Converse.js sees that the entity has a disco#items feature, + // so it will make a query for it. + selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]'; + await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).length, 1000); + /* + * + * + * + * + * + */ + selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]'; + stanza = IQ_stanzas.find(iq => iq.querySelector(selector), 500); + stanza = $iq({ + 'type': 'result', + 'from': 'montague.lit', + 'to': 'romeo@montague.lit/orchard', + 'id': stanza.getAttribute('id'), + }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'}) + .c('item', { + 'jid': 'upload.montague.lit', + 'name': 'HTTP File Upload'}); + + _converse.connection._dataRecv(mock.createRequest(stanza)); + + _converse.api.disco.entities.get().then(entities => { expect(entities.length).toBe(2); - expect(entities.pluck('jid').includes('montague.lit')).toBe(true); - expect(entities.pluck('jid').includes('romeo@montague.lit')).toBe(true); + expect(entities.get('montague.lit').items.length).toBe(1); + // Converse.js sees that the entity has a disco#info feature, so it will make a query for it. + const selector = 'iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'; + return u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).length > 0); + }); - expect(entities.get(_converse.domain).features.length).toBe(2); - expect(entities.get(_converse.domain).identities.length).toBe(1); + selector = 'iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'; + stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).pop(), 1000); + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``); - // Converse.js sees that the entity has a disco#items feature, - // so it will make a query for it. - selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]'; - await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).length, 1000); - /* - * - * - * - * - * - */ - selector = 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]'; - stanza = IQ_stanzas.find(iq => iq.querySelector(selector), 500); - stanza = $iq({ - 'type': 'result', - 'from': 'montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'id': stanza.getAttribute('id'), - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'}) - .c('item', { - 'jid': 'upload.montague.lit', - 'name': 'HTTP File Upload'}); + // Upload service responds and reports a maximum file size of 5MiB + /* + * + * + * + * + * + * urn:xmpp:http:upload:0 + * + * + * 5242880 + * + * + * + * + */ + stanza = $iq({'type': 'result', 'to': 'romeo@montague.lit/orchard', 'id': stanza.getAttribute('id'), 'from': 'upload.montague.lit'}) + .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up() + .c('feature', {'var':'urn:xmpp:http:upload:0'}).up() + .c('x', {'type':'result', 'xmlns':'jabber:x:data'}) + .c('field', {'var':'FORM_TYPE', 'type':'hidden'}) + .c('value').t('urn:xmpp:http:upload:0').up().up() + .c('field', {'var':'max-file-size'}) + .c('value').t('5242880'); + _converse.connection._dataRecv(mock.createRequest(stanza)); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); + entities = await _converse.api.disco.entities.get(); + expect(entities.get('montague.lit').items.get('upload.montague.lit').identities.where({'category': 'store'}).length).toBe(1); + const supported = await _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain); + expect(supported).toBe(true); + const features = await _converse.api.disco.features.get(Strophe.NS.HTTPUPLOAD, _converse.domain); + expect(features.length).toBe(1); + expect(features[0].get('jid')).toBe('upload.montague.lit'); + expect(features[0].dataforms.where({'FORM_TYPE': {value: "urn:xmpp:http:upload:0", type: "hidden"}}).length).toBe(1); + done(); + })); + }); - _converse.api.disco.entities.get().then(entities => { - expect(entities.length).toBe(2); - expect(entities.get('montague.lit').items.length).toBe(1); - // Converse.js sees that the entity has a disco#info feature, so it will make a query for it. - const selector = 'iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'; - return u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).length > 0); - }); + describe("When not supported", function () { + describe("A file upload toolbar button", function () { - selector = 'iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'; - stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).pop(), 1000); - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``); + it("does not appear in private chats", + mock.initConverse([], {}, async function (done, _converse) { - // Upload service responds and reports a maximum file size of 5MiB - /* - * - * - * - * - * - * urn:xmpp:http:upload:0 - * - * - * 5242880 - * - * - * - * - */ - stanza = $iq({'type': 'result', 'to': 'romeo@montague.lit/orchard', 'id': stanza.getAttribute('id'), 'from': 'upload.montague.lit'}) - .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up() - .c('feature', {'var':'urn:xmpp:http:upload:0'}).up() - .c('x', {'type':'result', 'xmlns':'jabber:x:data'}) - .c('field', {'var':'FORM_TYPE', 'type':'hidden'}) - .c('value').t('urn:xmpp:http:upload:0').up().up() - .c('field', {'var':'max-file-size'}) - .c('value').t('5242880'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); + await mock.waitForRoster(_converse, 'current', 3); + mock.openControlBox(_converse); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + await mock.waitUntilDiscoConfirmed( + _converse, _converse.domain, + [{'category': 'server', 'type':'IM'}], + ['http://jabber.org/protocol/disco#items'], [], 'info'); - entities = await _converse.api.disco.entities.get(); - expect(entities.get('montague.lit').items.get('upload.montague.lit').identities.where({'category': 'store'}).length).toBe(1); - const supported = await _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain); - expect(supported).toBe(true); - const features = await _converse.api.disco.features.get(Strophe.NS.HTTPUPLOAD, _converse.domain); - expect(features.length).toBe(1); - expect(features[0].get('jid')).toBe('upload.montague.lit'); - expect(features[0].dataforms.where({'FORM_TYPE': {value: "urn:xmpp:http:upload:0", type: "hidden"}}).length).toBe(1); + await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], [], 'items'); + const view = _converse.chatboxviews.get(contact_jid); + expect(view.el.querySelector('.chat-toolbar .upload-file')).toBe(null); done(); })); + + it("does not appear in MUC chats", mock.initConverse( + ['rosterGroupsFetched'], {}, + async (done, _converse) => { + + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + mock.waitUntilDiscoConfirmed( + _converse, _converse.domain, + [{'category': 'server', 'type':'IM'}], + ['http://jabber.org/protocol/disco#items'], [], 'info'); + + await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.lit'], 'items'); + await mock.waitUntilDiscoConfirmed(_converse, 'upload.montague.lit', [], [Strophe.NS.HTTPUPLOAD], []); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + expect(view.el.querySelector('.chat-toolbar .upload-file')).toBe(null); + done(); + })); + }); + }); - describe("When not supported", function () { - describe("A file upload toolbar button", function () { + describe("When supported", function () { - it("does not appear in private chats", - mock.initConverse([], {}, async function (done, _converse) { + describe("A file upload toolbar button", function () { - await test_utils.waitForRoster(_converse, 'current', 3); - test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.domain, - [{'category': 'server', 'type':'IM'}], - ['http://jabber.org/protocol/disco#items'], [], 'info'); + it("appears in private chats", mock.initConverse(async (done, _converse) => { + await mock.waitUntilDiscoConfirmed( + _converse, _converse.domain, + [{'category': 'server', 'type':'IM'}], + ['http://jabber.org/protocol/disco#items'], [], 'info'); - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], [], 'items'); - const view = _converse.chatboxviews.get(contact_jid); - expect(view.el.querySelector('.chat-toolbar .upload-file')).toBe(null); - done(); - })); + await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.lit'], 'items') + await mock.waitUntilDiscoConfirmed(_converse, 'upload.montague.lit', [], [Strophe.NS.HTTPUPLOAD], []); + await mock.waitForRoster(_converse, 'current', 3); + const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + u.waitUntil(() => view.el.querySelector('.upload-file')); + expect(view.el.querySelector('.chat-toolbar .upload-file')).not.toBe(null); + done(); + })); - it("does not appear in MUC chats", mock.initConverse( - ['rosterGroupsFetched'], {}, - async (done, _converse) => { - - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - test_utils.waitUntilDiscoConfirmed( - _converse, _converse.domain, - [{'category': 'server', 'type':'IM'}], - ['http://jabber.org/protocol/disco#items'], [], 'info'); - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.lit'], 'items'); - await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.lit', [], [Strophe.NS.HTTPUPLOAD], []); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - expect(view.el.querySelector('.chat-toolbar .upload-file')).toBe(null); - done(); - })); - - }); - }); - - describe("When supported", function () { - - describe("A file upload toolbar button", function () { - - it("appears in private chats", mock.initConverse(async (done, _converse) => { - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.domain, - [{'category': 'server', 'type':'IM'}], - ['http://jabber.org/protocol/disco#items'], [], 'info'); - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.lit'], 'items') - await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.lit', [], [Strophe.NS.HTTPUPLOAD], []); - await test_utils.waitForRoster(_converse, 'current', 3); - const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - u.waitUntil(() => view.el.querySelector('.upload-file')); - expect(view.el.querySelector('.chat-toolbar .upload-file')).not.toBe(null); - done(); - })); - - it("appears in MUC chats", mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async (done, _converse) => { - - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.domain, - [{'category': 'server', 'type':'IM'}], - ['http://jabber.org/protocol/disco#items'], [], 'info'); - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.lit'], 'items'); - await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.lit', [], [Strophe.NS.HTTPUPLOAD], []); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - await u.waitUntil(() => _converse.chatboxviews.get('lounge@montague.lit').el.querySelector('.upload-file')); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - expect(view.el.querySelector('.chat-toolbar .upload-file')).not.toBe(null); - done(); - })); - - describe("when clicked and a file chosen", function () { - - it("is uploaded and sent out", mock.initConverse(async (done, _converse) => { - const base_url = 'https://conversejs.org'; - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.domain, - [{'category': 'server', 'type':'IM'}], - ['http://jabber.org/protocol/disco#items'], [], 'info'); - - const send_backup = XMLHttpRequest.prototype.send; - const IQ_stanzas = _converse.connection.IQ_stanzas; - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items'); - await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []); - await test_utils.waitForRoster(_converse, 'current'); - const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - const file = { - 'type': 'image/jpeg', - 'size': '23456' , - 'lastModifiedDate': "", - 'name': "my-juliet.jpg" - }; - view.model.sendFiles([file]); - await new Promise(resolve => view.once('messageInserted', resolve)); - - await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length); - const iq = IQ_stanzas.pop(); - expect(Strophe.serialize(iq)).toBe( - ``+ - ``+ - ``); - - const message = base_url+"/logo/conversejs-filled.svg"; - - const stanza = u.toStanza(` - - - - Basic Base64String== - foo=bar; user=romeo - - - - `); - - spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () { - const message = view.model.messages.at(0); - expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0'); - message.set('progress', 0.5); - u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5') - .then(() => { - message.set('progress', 1); - u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1') - }).then(() => { - message.save({ - 'upload': _converse.SUCCESS, - 'oob_url': message.get('get'), - 'message': message.get('get') - }); - return new Promise(resolve => view.model.messages.once('rendered', resolve)); - }); - }); - let sent_stanza; - spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza)); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - await u.waitUntil(() => sent_stanza, 1000); - expect(sent_stanza.toLocaleString()).toBe( - ``+ - `${message}`+ - ``+ - ``+ - ``+ - `${message}`+ - ``+ - ``+ - ``); - await u.waitUntil(() => view.el.querySelector('.chat-image'), 1000); - // Check that the image renders - expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual( - ``+ - ``); - XMLHttpRequest.prototype.send = send_backup; - done(); - })); - - it("is uploaded and sent out from a groupchat", mock.initConverse(async (done, _converse) => { - - const base_url = 'https://conversejs.org'; - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.domain, - [{'category': 'server', 'type':'IM'}], - ['http://jabber.org/protocol/disco#items'], [], 'info'); - - const send_backup = XMLHttpRequest.prototype.send; - const IQ_stanzas = _converse.connection.IQ_stanzas; - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items'); - await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - - // Wait until MAM query has been sent out - const sent_stanzas = _converse.connection.sent_stanzas; - await u.waitUntil(() => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.MAM}"]`, s).length).pop()); - - const view = _converse.chatboxviews.get('lounge@montague.lit'); - const file = { - 'type': 'image/jpeg', - 'size': '23456' , - 'lastModifiedDate': "", - 'name': "my-juliet.jpg" - }; - view.model.sendFiles([file]); - await new Promise(resolve => view.once('messageInserted', resolve)); - - await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length); - const iq = IQ_stanzas.pop(); - expect(Strophe.serialize(iq)).toBe( - ``+ - ``+ - ``); - - const message = base_url+"/logo/conversejs-filled.svg"; - const stanza = u.toStanza(` - - - - Basic Base64String== - foo=bar; user=romeo - - - - `); - - spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () { - const message = view.model.messages.at(0); - expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0'); - message.set('progress', 0.5); - u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5') - .then(() => { - message.set('progress', 1); - u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1') - }).then(() => { - message.save({ - 'upload': _converse.SUCCESS, - 'oob_url': message.get('get'), - 'message': message.get('get') - }); - return new Promise(resolve => view.model.messages.once('rendered', resolve)); - }); - }); - let sent_stanza; - spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza)); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - await u.waitUntil(() => sent_stanza, 1000); - expect(sent_stanza.toLocaleString()).toBe( - ``+ - `${message}`+ - ``+ - ``+ - `${message}`+ - ``+ - ``+ - ``); - await u.waitUntil(() => view.el.querySelector('.chat-image'), 1000); - // Check that the image renders - expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual( - ``+ - ``); - - XMLHttpRequest.prototype.send = send_backup; - done(); - })); - - it("shows an error message if the file is too large", - mock.initConverse([], {}, async function (done, _converse) { - - const IQ_stanzas = _converse.connection.IQ_stanzas; - const IQ_ids = _converse.connection.IQ_ids; - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []); - await u.waitUntil(() => _.filter( - IQ_stanzas, - iq => iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]')).length - ); - - let stanza = _.find(IQ_stanzas, function (iq) { - return iq.querySelector( - 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'); - }); - const info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; - stanza = $iq({ - 'type': 'result', - 'from': 'montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'id': info_IQ_id - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', { - 'category': 'server', - 'type': 'im'}).up() - .c('feature', { - 'var': 'http://jabber.org/protocol/disco#info'}).up() - .c('feature', { - 'var': 'http://jabber.org/protocol/disco#items'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - let entities = await _converse.api.disco.entities.get(); - - expect(entities.length).toBe(2); - expect(_.includes(entities.pluck('jid'), 'montague.lit')).toBe(true); - expect(_.includes(entities.pluck('jid'), 'romeo@montague.lit')).toBe(true); - - expect(entities.get(_converse.domain).features.length).toBe(2); - expect(entities.get(_converse.domain).identities.length).toBe(1); - - await u.waitUntil(function () { - // Converse.js sees that the entity has a disco#items feature, - // so it will make a query for it. - return _.filter(IQ_stanzas, function (iq) { - return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]'); - }).length > 0; - }, 300); - - stanza = _.find(IQ_stanzas, function (iq) { - return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]'); - }); - var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; - stanza = $iq({ - 'type': 'result', - 'from': 'montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'id': items_IQ_id - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'}) - .c('item', { - 'jid': 'upload.montague.lit', - 'name': 'HTTP File Upload'}); - - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - entities = await _converse.api.disco.entities.get() - - expect(entities.length).toBe(2); - expect(entities.get('montague.lit').items.length).toBe(1); - await u.waitUntil(function () { - // Converse.js sees that the entity has a disco#info feature, - // so it will make a query for it. - return _.filter(IQ_stanzas, function (iq) { - return iq.querySelector('iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'); - }).length > 0; - }, 300); - - stanza = _.find(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]')); - const IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``); - - // Upload service responds and reports a maximum file size of 5MiB - stanza = $iq({'type': 'result', 'to': 'romeo@montague.lit/orchard', 'id': IQ_id, 'from': 'upload.montague.lit'}) - .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up() - .c('feature', {'var':'urn:xmpp:http:upload:0'}).up() - .c('x', {'type':'result', 'xmlns':'jabber:x:data'}) - .c('field', {'var':'FORM_TYPE', 'type':'hidden'}) - .c('value').t('urn:xmpp:http:upload:0').up().up() - .c('field', {'var':'max-file-size'}) - .c('value').t('5242880'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - entities = await _converse.api.disco.entities.get(); - expect(entities.get('montague.lit').items.get('upload.montague.lit').identities.where({'category': 'store'}).length).toBe(1); - await _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain); - await test_utils.waitForRoster(_converse, 'current'); - - const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - var file = { - 'type': 'image/jpeg', - 'size': '5242881', - 'lastModifiedDate': "", - 'name': "my-juliet.jpg" - }; - view.model.sendFiles([file]); - await u.waitUntil(() => view.el.querySelectorAll('.message').length) - const messages = view.el.querySelectorAll('.message.chat-error'); - expect(messages.length).toBe(1); - expect(messages[0].textContent.trim()).toBe( - 'The size of your file, my-juliet.jpg, exceeds the maximum allowed by your server, which is 5 MB.'); - done(); - })); - }); - }); - - describe("While a file is being uploaded", function () { - - it("shows a progress bar", mock.initConverse( + it("appears in MUC chats", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + async (done, _converse) => { - await test_utils.waitUntilDiscoConfirmed( + await mock.waitUntilDiscoConfirmed( + _converse, _converse.domain, + [{'category': 'server', 'type':'IM'}], + ['http://jabber.org/protocol/disco#items'], [], 'info'); + + await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.lit'], 'items'); + await mock.waitUntilDiscoConfirmed(_converse, 'upload.montague.lit', [], [Strophe.NS.HTTPUPLOAD], []); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + await u.waitUntil(() => _converse.chatboxviews.get('lounge@montague.lit').el.querySelector('.upload-file')); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + expect(view.el.querySelector('.chat-toolbar .upload-file')).not.toBe(null); + done(); + })); + + describe("when clicked and a file chosen", function () { + + it("is uploaded and sent out", mock.initConverse(async (done, _converse) => { + const base_url = 'https://conversejs.org'; + await mock.waitUntilDiscoConfirmed( _converse, _converse.domain, [{'category': 'server', 'type':'IM'}], ['http://jabber.org/protocol/disco#items'], [], 'info'); + const send_backup = XMLHttpRequest.prototype.send; const IQ_stanzas = _converse.connection.IQ_stanzas; - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items'); - await test_utils.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []); - await test_utils.waitForRoster(_converse, 'current'); + await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items'); + await mock.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []); + await mock.waitForRoster(_converse, 'current'); const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); + await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.chatboxviews.get(contact_jid); const file = { 'type': 'image/jpeg', @@ -577,7 +248,8 @@ window.addEventListener('converse-loaded', () => { }; view.model.sendFiles([file]); await new Promise(resolve => view.once('messageInserted', resolve)); - await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length) + + await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length); const iq = IQ_stanzas.pop(); expect(Strophe.serialize(iq)).toBe( ` { `xmlns="urn:xmpp:http:upload:0"/>`+ ``); - const base_url = 'https://conversejs.org'; const message = base_url+"/logo/conversejs-filled.svg"; + const stanza = u.toStanza(` { type="result"> - Basic Base64String== - foo=bar; user=romeo + Basic Base64String== + foo=bar; user=romeo `); + spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () { const message = view.model.messages.at(0); expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0'); @@ -616,13 +289,338 @@ window.addEventListener('converse-loaded', () => { message.set('progress', 1); u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1') }).then(() => { - expect(view.el.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB'); - done(); + message.save({ + 'upload': _converse.SUCCESS, + 'oob_url': message.get('get'), + 'message': message.get('get') + }); + return new Promise(resolve => view.model.messages.once('rendered', resolve)); }); }); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); + let sent_stanza; + spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza)); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + await u.waitUntil(() => sent_stanza, 1000); + expect(sent_stanza.toLocaleString()).toBe( + ``+ + `${message}`+ + ``+ + ``+ + ``+ + `${message}`+ + ``+ + ``+ + ``); + await u.waitUntil(() => view.el.querySelector('.chat-image'), 1000); + // Check that the image renders + expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual( + ``+ + ``); + XMLHttpRequest.prototype.send = send_backup; + done(); + })); + + it("is uploaded and sent out from a groupchat", mock.initConverse(async (done, _converse) => { + + const base_url = 'https://conversejs.org'; + await mock.waitUntilDiscoConfirmed( + _converse, _converse.domain, + [{'category': 'server', 'type':'IM'}], + ['http://jabber.org/protocol/disco#items'], [], 'info'); + + const send_backup = XMLHttpRequest.prototype.send; + const IQ_stanzas = _converse.connection.IQ_stanzas; + + await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items'); + await mock.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + + // Wait until MAM query has been sent out + const sent_stanzas = _converse.connection.sent_stanzas; + await u.waitUntil(() => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.MAM}"]`, s).length).pop()); + + const view = _converse.chatboxviews.get('lounge@montague.lit'); + const file = { + 'type': 'image/jpeg', + 'size': '23456' , + 'lastModifiedDate': "", + 'name': "my-juliet.jpg" + }; + view.model.sendFiles([file]); + await new Promise(resolve => view.once('messageInserted', resolve)); + + await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length); + const iq = IQ_stanzas.pop(); + expect(Strophe.serialize(iq)).toBe( + ``+ + ``+ + ``); + + const message = base_url+"/logo/conversejs-filled.svg"; + const stanza = u.toStanza(` + + + + Basic Base64String== + foo=bar; user=romeo + + + + `); + + spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () { + const message = view.model.messages.at(0); + expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0'); + message.set('progress', 0.5); + u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5') + .then(() => { + message.set('progress', 1); + u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1') + }).then(() => { + message.save({ + 'upload': _converse.SUCCESS, + 'oob_url': message.get('get'), + 'message': message.get('get') + }); + return new Promise(resolve => view.model.messages.once('rendered', resolve)); + }); + }); + let sent_stanza; + spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza)); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + await u.waitUntil(() => sent_stanza, 1000); + expect(sent_stanza.toLocaleString()).toBe( + ``+ + `${message}`+ + ``+ + ``+ + `${message}`+ + ``+ + ``+ + ``); + await u.waitUntil(() => view.el.querySelector('.chat-image'), 1000); + // Check that the image renders + expect(view.el.querySelector('.chat-msg .chat-msg__media').innerHTML.trim()).toEqual( + ``+ + ``); + + XMLHttpRequest.prototype.send = send_backup; + done(); + })); + + it("shows an error message if the file is too large", + mock.initConverse([], {}, async function (done, _converse) { + + const IQ_stanzas = _converse.connection.IQ_stanzas; + const IQ_ids = _converse.connection.IQ_ids; + + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []); + await u.waitUntil(() => _.filter( + IQ_stanzas, + iq => iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]')).length + ); + + let stanza = _.find(IQ_stanzas, function (iq) { + return iq.querySelector( + 'iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'); + }); + const info_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; + stanza = $iq({ + 'type': 'result', + 'from': 'montague.lit', + 'to': 'romeo@montague.lit/orchard', + 'id': info_IQ_id + }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', { + 'category': 'server', + 'type': 'im'}).up() + .c('feature', { + 'var': 'http://jabber.org/protocol/disco#info'}).up() + .c('feature', { + 'var': 'http://jabber.org/protocol/disco#items'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + let entities = await _converse.api.disco.entities.get(); + + expect(entities.length).toBe(2); + expect(_.includes(entities.pluck('jid'), 'montague.lit')).toBe(true); + expect(_.includes(entities.pluck('jid'), 'romeo@montague.lit')).toBe(true); + + expect(entities.get(_converse.domain).features.length).toBe(2); + expect(entities.get(_converse.domain).identities.length).toBe(1); + + await u.waitUntil(function () { + // Converse.js sees that the entity has a disco#items feature, + // so it will make a query for it. + return _.filter(IQ_stanzas, function (iq) { + return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]'); + }).length > 0; + }, 300); + + stanza = _.find(IQ_stanzas, function (iq) { + return iq.querySelector('iq[to="montague.lit"] query[xmlns="http://jabber.org/protocol/disco#items"]'); + }); + var items_IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; + stanza = $iq({ + 'type': 'result', + 'from': 'montague.lit', + 'to': 'romeo@montague.lit/orchard', + 'id': items_IQ_id + }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#items'}) + .c('item', { + 'jid': 'upload.montague.lit', + 'name': 'HTTP File Upload'}); + + _converse.connection._dataRecv(mock.createRequest(stanza)); + + entities = await _converse.api.disco.entities.get() + + expect(entities.length).toBe(2); + expect(entities.get('montague.lit').items.length).toBe(1); + await u.waitUntil(function () { + // Converse.js sees that the entity has a disco#info feature, + // so it will make a query for it. + return _.filter(IQ_stanzas, function (iq) { + return iq.querySelector('iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]'); + }).length > 0; + }, 300); + + stanza = _.find(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.lit"] query[xmlns="http://jabber.org/protocol/disco#info"]')); + const IQ_id = IQ_ids[IQ_stanzas.indexOf(stanza)]; + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``); + + // Upload service responds and reports a maximum file size of 5MiB + stanza = $iq({'type': 'result', 'to': 'romeo@montague.lit/orchard', 'id': IQ_id, 'from': 'upload.montague.lit'}) + .c('query', {'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', {'category':'store', 'type':'file', 'name':'HTTP File Upload'}).up() + .c('feature', {'var':'urn:xmpp:http:upload:0'}).up() + .c('x', {'type':'result', 'xmlns':'jabber:x:data'}) + .c('field', {'var':'FORM_TYPE', 'type':'hidden'}) + .c('value').t('urn:xmpp:http:upload:0').up().up() + .c('field', {'var':'max-file-size'}) + .c('value').t('5242880'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + entities = await _converse.api.disco.entities.get(); + expect(entities.get('montague.lit').items.get('upload.montague.lit').identities.where({'category': 'store'}).length).toBe(1); + await _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain); + await mock.waitForRoster(_converse, 'current'); + + const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + var file = { + 'type': 'image/jpeg', + 'size': '5242881', + 'lastModifiedDate': "", + 'name': "my-juliet.jpg" + }; + view.model.sendFiles([file]); + await u.waitUntil(() => view.el.querySelectorAll('.message').length) + const messages = view.el.querySelectorAll('.message.chat-error'); + expect(messages.length).toBe(1); + expect(messages[0].textContent.trim()).toBe( + 'The size of your file, my-juliet.jpg, exceeds the maximum allowed by your server, which is 5 MB.'); + done(); })); }); }); + + describe("While a file is being uploaded", function () { + + it("shows a progress bar", mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed( + _converse, _converse.domain, + [{'category': 'server', 'type':'IM'}], + ['http://jabber.org/protocol/disco#items'], [], 'info'); + + const IQ_stanzas = _converse.connection.IQ_stanzas; + + await mock.waitUntilDiscoConfirmed(_converse, _converse.domain, [], [], ['upload.montague.tld'], 'items'); + await mock.waitUntilDiscoConfirmed(_converse, 'upload.montague.tld', [], [Strophe.NS.HTTPUPLOAD], []); + await mock.waitForRoster(_converse, 'current'); + const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + const file = { + 'type': 'image/jpeg', + 'size': '23456' , + 'lastModifiedDate': "", + 'name': "my-juliet.jpg" + }; + view.model.sendFiles([file]); + await new Promise(resolve => view.once('messageInserted', resolve)); + await u.waitUntil(() => _.filter(IQ_stanzas, iq => iq.querySelector('iq[to="upload.montague.tld"] request')).length) + const iq = IQ_stanzas.pop(); + expect(Strophe.serialize(iq)).toBe( + ``+ + ``+ + ``); + + const base_url = 'https://conversejs.org'; + const message = base_url+"/logo/conversejs-filled.svg"; + const stanza = u.toStanza(` + + + + Basic Base64String== + foo=bar; user=romeo + + + + `); + spyOn(XMLHttpRequest.prototype, 'send').and.callFake(function () { + const message = view.model.messages.at(0); + expect(view.el.querySelector('.chat-content progress').getAttribute('value')).toBe('0'); + message.set('progress', 0.5); + u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '0.5') + .then(() => { + message.set('progress', 1); + u.waitUntil(() => view.el.querySelector('.chat-content progress').getAttribute('value') === '1') + }).then(() => { + expect(view.el.querySelector('.chat-content .chat-msg__text').textContent).toBe('Uploading file: my-juliet.jpg, 22.91 KB'); + done(); + }); + }); + _converse.connection._dataRecv(mock.createRequest(stanza)); + })); + }); }); }); diff --git a/spec/login.js b/spec/login.js index 52174910d..3ee07b9e1 100644 --- a/spec/login.js +++ b/spec/login.js @@ -1,79 +1,77 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const u = converse.env.utils; +/*global mock */ - describe("The Login Form", function () { +const u = converse.env.utils; - it("contains a checkbox to indicate whether the computer is trusted or not", - mock.initConverse( - ['chatBoxesInitialized'], - { auto_login: false, - allow_registration: false }, - async function (done, _converse) { +describe("The Login Form", function () { - test_utils.openControlBox(_converse); - const cbview = await u.waitUntil(() => _converse.chatboxviews.get('controlbox')); + it("contains a checkbox to indicate whether the computer is trusted or not", + mock.initConverse( + ['chatBoxesInitialized'], + { auto_login: false, + allow_registration: false }, + async function (done, _converse) { + + mock.openControlBox(_converse); + const cbview = await u.waitUntil(() => _converse.chatboxviews.get('controlbox')); + const checkboxes = cbview.el.querySelectorAll('input[type="checkbox"]'); + expect(checkboxes.length).toBe(1); + + const checkbox = checkboxes[0]; + const label = cbview.el.querySelector(`label[for="${checkbox.getAttribute('id')}"]`); + expect(label.textContent).toBe('This is a trusted device'); + expect(checkbox.checked).toBe(true); + + cbview.el.querySelector('input[name="jid"]').value = 'romeo@montague.lit'; + cbview.el.querySelector('input[name="password"]').value = 'secret'; + + spyOn(cbview.loginpanel, 'connect'); + cbview.delegateEvents(); + + expect(_converse.config.get('storage')).toBe('persistent'); + cbview.el.querySelector('input[type="submit"]').click(); + expect(_converse.config.get('storage')).toBe('persistent'); + expect(cbview.loginpanel.connect).toHaveBeenCalled(); + + checkbox.click(); + cbview.el.querySelector('input[type="submit"]').click(); + expect(_converse.config.get('storage')).toBe('session'); + done(); + })); + + it("checkbox can be set to false by default", + mock.initConverse( + ['chatBoxesInitialized'], + { auto_login: false, + trusted: false, + allow_registration: false }, + function (done, _converse) { + + u.waitUntil(() => _converse.chatboxviews.get('controlbox')) + .then(() => { + var cbview = _converse.chatboxviews.get('controlbox'); + mock.openControlBox(_converse); const checkboxes = cbview.el.querySelectorAll('input[type="checkbox"]'); expect(checkboxes.length).toBe(1); const checkbox = checkboxes[0]; const label = cbview.el.querySelector(`label[for="${checkbox.getAttribute('id')}"]`); expect(label.textContent).toBe('This is a trusted device'); - expect(checkbox.checked).toBe(true); + expect(checkbox.checked).toBe(false); cbview.el.querySelector('input[name="jid"]').value = 'romeo@montague.lit'; cbview.el.querySelector('input[name="password"]').value = 'secret'; spyOn(cbview.loginpanel, 'connect'); - cbview.delegateEvents(); - expect(_converse.config.get('storage')).toBe('persistent'); + expect(_converse.config.get('storage')).toBe('session'); cbview.el.querySelector('input[type="submit"]').click(); - expect(_converse.config.get('storage')).toBe('persistent'); + expect(_converse.config.get('storage')).toBe('session'); expect(cbview.loginpanel.connect).toHaveBeenCalled(); checkbox.click(); cbview.el.querySelector('input[type="submit"]').click(); - expect(_converse.config.get('storage')).toBe('session'); + expect(_converse.config.get('storage')).toBe('persistent'); done(); - })); - - it("checkbox can be set to false by default", - mock.initConverse( - ['chatBoxesInitialized'], - { auto_login: false, - trusted: false, - allow_registration: false }, - function (done, _converse) { - - u.waitUntil(() => _converse.chatboxviews.get('controlbox')) - .then(() => { - var cbview = _converse.chatboxviews.get('controlbox'); - test_utils.openControlBox(_converse); - const checkboxes = cbview.el.querySelectorAll('input[type="checkbox"]'); - expect(checkboxes.length).toBe(1); - - const checkbox = checkboxes[0]; - const label = cbview.el.querySelector(`label[for="${checkbox.getAttribute('id')}"]`); - expect(label.textContent).toBe('This is a trusted device'); - expect(checkbox.checked).toBe(false); - - cbview.el.querySelector('input[name="jid"]').value = 'romeo@montague.lit'; - cbview.el.querySelector('input[name="password"]').value = 'secret'; - - spyOn(cbview.loginpanel, 'connect'); - - expect(_converse.config.get('storage')).toBe('session'); - cbview.el.querySelector('input[type="submit"]').click(); - expect(_converse.config.get('storage')).toBe('session'); - expect(cbview.loginpanel.connect).toHaveBeenCalled(); - - checkbox.click(); - cbview.el.querySelector('input[type="submit"]').click(); - expect(_converse.config.get('storage')).toBe('persistent'); - done(); - }); - })); - }); + }); + })); }); diff --git a/spec/mam.js b/spec/mam.js index 3e5f69f70..29f268791 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -1,1100 +1,1098 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const Model = converse.env.Model; - const Strophe = converse.env.Strophe; - const $iq = converse.env.$iq; - const $msg = converse.env.$msg; - const dayjs = converse.env.dayjs; - const u = converse.env.utils; - const sizzle = converse.env.sizzle; - // See: https://xmpp.org/rfcs/rfc3921.html +/*global mock */ - // Implements the protocol defined in https://xmpp.org/extensions/xep-0313.html#config - describe("Message Archive Management", function () { +const Model = converse.env.Model; +const Strophe = converse.env.Strophe; +const $iq = converse.env.$iq; +const $msg = converse.env.$msg; +const dayjs = converse.env.dayjs; +const u = converse.env.utils; +const sizzle = converse.env.sizzle; +// See: https://xmpp.org/rfcs/rfc3921.html - describe("The XEP-0313 Archive", function () { +// Implements the protocol defined in https://xmpp.org/extensions/xep-0313.html#config +describe("Message Archive Management", function () { - it("is queried when the user enters a new MUC", - mock.initConverse(['discoInitialized'], {'archived_messages_page_size': 2}, async function (done, _converse) { + describe("The XEP-0313 Archive", function () { + it("is queried when the user enters a new MUC", + mock.initConverse(['discoInitialized'], {'archived_messages_page_size': 2}, async function (done, _converse) { + + const sent_IQs = _converse.connection.IQ_stanzas; + const muc_jid = 'orchard@chat.shakespeare.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + let view = _converse.chatboxviews.get(muc_jid); + let iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop()); + expect(Strophe.serialize(iq_get)).toBe( + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + ``+ + `2`+ + ``+ + ``); + + let first_msg_id = _converse.connection.getUniqueId(); + let last_msg_id = _converse.connection.getUniqueId(); + let message = u.toStanza( + ` + + + + + 2nd Message + + + + `); + _converse.connection._dataRecv(mock.createRequest(message)); + + message = u.toStanza( + ` + + + + + 3rd Message + + + + `); + _converse.connection._dataRecv(mock.createRequest(message)); + + // Clear so that we don't match the older query + while (sent_IQs.length) { sent_IQs.pop(); } + + // XXX: Even though the count is 3, when fetching messages for + // the first time, we don't paginate, so that message + // is not fetched. The user needs to manually load older + // messages for it to be fetched. + // TODO: we need to add a clickable link to load older messages + let result = u.toStanza( + ` + + + ${first_msg_id} + ${last_msg_id} + 3 + + + `); + _converse.connection._dataRecv(mock.createRequest(result)); + await u.waitUntil(() => view.model.messages.length === 2); + view.close(); + // Clear so that we don't match the older query + while (sent_IQs.length) { sent_IQs.pop(); } + + await u.waitUntil(() => _converse.chatboxes.length === 1); + + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => view.model.messages.length); + + iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop()); + expect(Strophe.serialize(iq_get)).toBe( + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + ``+ + `2${message.querySelector('result').getAttribute('id')}`+ + ``+ + ``); + + first_msg_id = _converse.connection.getUniqueId(); + last_msg_id = _converse.connection.getUniqueId(); + message = u.toStanza( + ` + + + + + 4th Message + + + + `); + _converse.connection._dataRecv(mock.createRequest(message)); + + message = u.toStanza( + ` + + + + + 5th Message + + + + `); + _converse.connection._dataRecv(mock.createRequest(message)); + + // Clear so that we don't match the older query + while (sent_IQs.length) { sent_IQs.pop(); } + + result = u.toStanza( + ` + + + ${first_msg_id} + ${last_msg_id} + 5 + + + `); + _converse.connection._dataRecv(mock.createRequest(result)); + await u.waitUntil(() => view.model.messages.length === 4); + + iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop()); + expect(Strophe.serialize(iq_get)).toBe( + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + ``+ + ``+ + `2${last_msg_id}`+ + ``+ + ``+ + ``); + + const msg_id = _converse.connection.getUniqueId(); + message = u.toStanza( + ` + + + + + 6th Message + + + + `); + _converse.connection._dataRecv(mock.createRequest(message)); + + result = u.toStanza( + ` + + + ${msg_id} + ${msg_id} + 6 + + + `); + _converse.connection._dataRecv(mock.createRequest(result)); + await u.waitUntil(() => view.model.messages.length === 5); + const msg_els = view.content.querySelectorAll('.chat-msg__text'); + expect(Array.from(msg_els).map(e => e.textContent).join(' ')).toBe("2nd Message 3rd Message 4th Message 5th Message 6th Message"); + done(); + })); + }); + + describe("An archived message", function () { + + describe("when received", function () { + + it("is discarded if it doesn't come from the right sender", + mock.initConverse( + ['discoInitialized'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); const sent_IQs = _converse.connection.IQ_stanzas; - const muc_jid = 'orchard@chat.shakespeare.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - let view = _converse.chatboxviews.get(muc_jid); - let iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop()); - expect(Strophe.serialize(iq_get)).toBe( - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - ``+ - `2`+ - ``+ - ``); + const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop()); + const queryid = stanza.querySelector('query').getAttribute('queryid'); + let msg = $msg({'id': _converse.connection.getUniqueId(), 'from': 'impersonator@capulet.lit', 'to': _converse.bare_jid}) + .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()}) + .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) + .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() + .c('message', { + 'xmlns':'jabber:client', + 'to': _converse.bare_jid, + 'id': _converse.connection.getUniqueId(), + 'from': contact_jid, + 'type':'chat' + }).c('body').t("Meet me at the dance"); + spyOn(converse.env.log, 'warn'); + _converse.connection._dataRecv(mock.createRequest(msg)); + expect(converse.env.log.warn).toHaveBeenCalledWith(`Ignoring alleged MAM message from ${msg.nodeTree.getAttribute('from')}`); - let first_msg_id = _converse.connection.getUniqueId(); - let last_msg_id = _converse.connection.getUniqueId(); - let message = u.toStanza( - ` - - - - - 2nd Message - - - + msg = $msg({'id': _converse.connection.getUniqueId(), 'to': _converse.bare_jid}) + .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()}) + .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) + .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() + .c('message', { + 'xmlns':'jabber:client', + 'to': _converse.bare_jid, + 'id': _converse.connection.getUniqueId(), + 'from': contact_jid, + 'type':'chat' + }).c('body').t("Thrice the brinded cat hath mew'd."); + _converse.connection._dataRecv(mock.createRequest(msg)); + + const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')}) + .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) + .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) + .c('first', {'index': '0'}).t('23452-4534-1').up() + .c('last').t('09af3-cc343-b409f').up() + .c('count').t('16'); + _converse.connection._dataRecv(mock.createRequest(iq_result)); + + const view = _converse.chatboxviews.get(contact_jid); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('message')).toBe("Thrice the brinded cat hath mew'd."); + done(); + })); + + + it("updates the is_archived value of an already cached version", + mock.initConverse( + ['discoInitialized'], {}, + async function (done, _converse) { + + await mock.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'romeo'); + + const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org'); + let stanza = u.toStanza( + ` + Hello + `); - _converse.connection._dataRecv(test_utils.createRequest(message)); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('is_archived')).toBe(false); + expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621'); - message = u.toStanza( + stanza = u.toStanza( ` - - - - - 3rd Message - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(message)); - - // Clear so that we don't match the older query - while (sent_IQs.length) { sent_IQs.pop(); } - - // XXX: Even though the count is 3, when fetching messages for - // the first time, we don't paginate, so that message - // is not fetched. The user needs to manually load older - // messages for it to be fetched. - // TODO: we need to add a clickable link to load older messages - let result = u.toStanza( - ` - - - ${first_msg_id} - ${last_msg_id} - 3 - - - `); - _converse.connection._dataRecv(test_utils.createRequest(result)); - await u.waitUntil(() => view.model.messages.length === 2); - view.close(); - // Clear so that we don't match the older query - while (sent_IQs.length) { sent_IQs.pop(); } - - await u.waitUntil(() => _converse.chatboxes.length === 1); - - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => view.model.messages.length); - - iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop()); - expect(Strophe.serialize(iq_get)).toBe( - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - ``+ - `2${message.querySelector('result').getAttribute('id')}`+ - ``+ - ``); - - first_msg_id = _converse.connection.getUniqueId(); - last_msg_id = _converse.connection.getUniqueId(); - message = u.toStanza( - ` - + from="trek-radio@conference.lightwitch.org"> + - - 4th Message + + Hello `); - _converse.connection._dataRecv(test_utils.createRequest(message)); + spyOn(view.model, 'getDuplicateMessage').and.callThrough(); + spyOn(view.model, 'updateMessage').and.callThrough(); + view.model.queueMessage(stanza); + await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); + expect(view.model.getDuplicateMessage.calls.count()).toBe(1); + const result = view.model.getDuplicateMessage.calls.all()[0].returnValue + expect(result instanceof _converse.Message).toBe(true); + expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); - message = u.toStanza( - ` - - - - - 5th Message - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(message)); - - // Clear so that we don't match the older query - while (sent_IQs.length) { sent_IQs.pop(); } - - result = u.toStanza( - ` - - - ${first_msg_id} - ${last_msg_id} - 5 - - - `); - _converse.connection._dataRecv(test_utils.createRequest(result)); - await u.waitUntil(() => view.model.messages.length === 4); - - iq_get = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq query[xmlns="${Strophe.NS.MAM}"]`)).pop()); - expect(Strophe.serialize(iq_get)).toBe( - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - ``+ - ``+ - `2${last_msg_id}`+ - ``+ - ``+ - ``); - - const msg_id = _converse.connection.getUniqueId(); - message = u.toStanza( - ` - - - - - 6th Message - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(message)); - - result = u.toStanza( - ` - - - ${msg_id} - ${msg_id} - 6 - - - `); - _converse.connection._dataRecv(test_utils.createRequest(result)); - await u.waitUntil(() => view.model.messages.length === 5); - const msg_els = view.content.querySelectorAll('.chat-msg__text'); - expect(Array.from(msg_els).map(e => e.textContent).join(' ')).toBe("2nd Message 3rd Message 4th Message 5th Message 6th Message"); + await u.waitUntil(() => view.model.updateMessage.calls.count()); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('is_archived')).toBe(true); + expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621'); done(); })); + + it("isn't shown as duplicate by comparing its stanza id or archive id", + mock.initConverse( + ['discoInitialized'], {}, + async function (done, _converse) { + + await mock.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'jcbrand'); + const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org'); + let stanza = u.toStanza( + ` + negan + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); + // Not sure whether such a race-condition might pose a problem + // in "real-world" situations. + stanza = u.toStanza( + ` + + + + + negan + + + + `); + spyOn(view.model, 'getDuplicateMessage').and.callThrough(); + view.model.queueMessage(stanza); + await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); + expect(view.model.getDuplicateMessage.calls.count()).toBe(1); + const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue + expect(result instanceof _converse.Message).toBe(true); + expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); + done(); + })); + + it("isn't shown as duplicate by comparing only the archive id", + mock.initConverse( + ['discoInitialized'], {}, + async function (done, _converse) { + + await mock.openAndEnterChatRoom(_converse, 'discuss@conference.conversejs.org', 'romeo'); + const view = _converse.chatboxviews.get('discuss@conference.conversejs.org'); + let stanza = u.toStanza( + ` + + + + + looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published + + + + + + + `); + view.model.queueMessage(stanza); + await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); + expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); + + stanza = u.toStanza( + ` + + + + + looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published + + + + + + + `); + + spyOn(view.model, 'getDuplicateMessage').and.callThrough(); + view.model.queueMessage(stanza); + await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); + expect(view.model.getDuplicateMessage.calls.count()).toBe(1); + const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue + expect(result instanceof _converse.Message).toBe(true); + expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); + done(); + })) }); + }); - describe("An archived message", function () { + describe("The archive.query API", function () { - describe("when received", function () { + it("can be used to query for all archived messages", + mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { - it("is discarded if it doesn't come from the right sender", - mock.initConverse( - ['discoInitialized'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); - const sent_IQs = _converse.connection.IQ_stanzas; - const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.MAM}"]`)).pop()); - const queryid = stanza.querySelector('query').getAttribute('queryid'); - let msg = $msg({'id': _converse.connection.getUniqueId(), 'from': 'impersonator@capulet.lit', 'to': _converse.bare_jid}) - .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()}) - .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) - .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() - .c('message', { - 'xmlns':'jabber:client', - 'to': _converse.bare_jid, - 'id': _converse.connection.getUniqueId(), - 'from': contact_jid, - 'type':'chat' - }).c('body').t("Meet me at the dance"); - spyOn(converse.env.log, 'warn'); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - expect(converse.env.log.warn).toHaveBeenCalledWith(`Ignoring alleged MAM message from ${msg.nodeTree.getAttribute('from')}`); - - msg = $msg({'id': _converse.connection.getUniqueId(), 'to': _converse.bare_jid}) - .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id': _converse.connection.getUniqueId()}) - .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) - .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() - .c('message', { - 'xmlns':'jabber:client', - 'to': _converse.bare_jid, - 'id': _converse.connection.getUniqueId(), - 'from': contact_jid, - 'type':'chat' - }).c('body').t("Thrice the brinded cat hath mew'd."); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - - const iq_result = $iq({'type': 'result', 'id': stanza.getAttribute('id')}) - .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) - .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) - .c('first', {'index': '0'}).t('23452-4534-1').up() - .c('last').t('09af3-cc343-b409f').up() - .c('count').t('16'); - _converse.connection._dataRecv(test_utils.createRequest(iq_result)); - - const view = _converse.chatboxviews.get(contact_jid); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('message')).toBe("Thrice the brinded cat hath mew'd."); - done(); - })); - - - it("updates the is_archived value of an already cached version", - mock.initConverse( - ['discoInitialized'], {}, - async function (done, _converse) { - - await test_utils.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'romeo'); - - const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org'); - let stanza = u.toStanza( - ` - Hello - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('is_archived')).toBe(false); - expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621'); - - stanza = u.toStanza( - ` - - - - - Hello - - - - `); - spyOn(view.model, 'getDuplicateMessage').and.callThrough(); - spyOn(view.model, 'updateMessage').and.callThrough(); - view.model.queueMessage(stanza); - await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); - expect(view.model.getDuplicateMessage.calls.count()).toBe(1); - const result = view.model.getDuplicateMessage.calls.all()[0].returnValue - expect(result instanceof _converse.Message).toBe(true); - expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); - - await u.waitUntil(() => view.model.updateMessage.calls.count()); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('is_archived')).toBe(true); - expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621'); - done(); - })); - - it("isn't shown as duplicate by comparing its stanza id or archive id", - mock.initConverse( - ['discoInitialized'], {}, - async function (done, _converse) { - - await test_utils.openAndEnterChatRoom(_converse, 'trek-radio@conference.lightwitch.org', 'jcbrand'); - const view = _converse.chatboxviews.get('trek-radio@conference.lightwitch.org'); - let stanza = u.toStanza( - ` - negan - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); - // Not sure whether such a race-condition might pose a problem - // in "real-world" situations. - stanza = u.toStanza( - ` - - - - - negan - - - - `); - spyOn(view.model, 'getDuplicateMessage').and.callThrough(); - view.model.queueMessage(stanza); - await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); - expect(view.model.getDuplicateMessage.calls.count()).toBe(1); - const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue - expect(result instanceof _converse.Message).toBe(true); - expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); - done(); - })); - - it("isn't shown as duplicate by comparing only the archive id", - mock.initConverse( - ['discoInitialized'], {}, - async function (done, _converse) { - - await test_utils.openAndEnterChatRoom(_converse, 'discuss@conference.conversejs.org', 'romeo'); - const view = _converse.chatboxviews.get('discuss@conference.conversejs.org'); - let stanza = u.toStanza( - ` - - - - - looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published - - - - - - - `); - view.model.queueMessage(stanza); - await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length); - expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); - - stanza = u.toStanza( - ` - - - - - looks like omemo fails completely with "bundle is undefined" when there is a device in the devicelist that has no keys published - - - - - - - `); - - spyOn(view.model, 'getDuplicateMessage').and.callThrough(); - view.model.queueMessage(stanza); - await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); - expect(view.model.getDuplicateMessage.calls.count()).toBe(1); - const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue - expect(result instanceof _converse.Message).toBe(true); - expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); - done(); - })) + const sendIQ = _converse.connection.sendIQ; + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + let sent_stanza, IQ_id; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); }); - }); + _converse.api.archive.query(); + await u.waitUntil(() => sent_stanza); + const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); + expect(sent_stanza.toString()).toBe( + ``); + done(); + })); - describe("The archive.query API", function () { + it("can be used to query for all messages to/from a particular JID", + mock.initConverse([], {}, async function (done, _converse) { - it("can be used to query for all archived messages", - mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + let sent_stanza, IQ_id; + const sendIQ = _converse.connection.sendIQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + _converse.api.archive.query({'with':'juliet@capulet.lit'}); + await u.waitUntil(() => sent_stanza); + const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); + expect(sent_stanza.toString()).toBe( + ``+ + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + ``+ + ``+ + `juliet@capulet.lit`+ + ``+ + ``+ + ``+ + ``); + done(); + })); - const sendIQ = _converse.connection.sendIQ; - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); - let sent_stanza, IQ_id; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_stanza = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); - _converse.api.archive.query(); - await u.waitUntil(() => sent_stanza); - const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); - expect(sent_stanza.toString()).toBe( - ``); - done(); - })); + it("can be used to query for archived messages from a chat room", + mock.initConverse([], {}, async function (done, _converse) { - it("can be used to query for all messages to/from a particular JID", - mock.initConverse([], {}, async function (done, _converse) { + const room_jid = 'coven@chat.shakespeare.lit'; + _converse.api.archive.query({'with': room_jid, 'groupchat': true}); + await mock.waitUntilDiscoConfirmed(_converse, room_jid, null, [Strophe.NS.MAM]); - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); - let sent_stanza, IQ_id; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_stanza = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); - _converse.api.archive.query({'with':'juliet@capulet.lit'}); - await u.waitUntil(() => sent_stanza); - const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); - expect(sent_stanza.toString()).toBe( - ``+ - ``+ - ``+ + const sent_stanzas = _converse.connection.sent_stanzas; + const stanza = await u.waitUntil( + () => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.MAM}"]`, s).length).pop()); + + const queryid = stanza.querySelector('query').getAttribute('queryid'); + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + ``+ + ``+ + ``+ + ``); + done(); + })); + + it("checks whether returned MAM messages from a MUC room are from the right JID", + mock.initConverse([], {}, async function (done, _converse) { + + const room_jid = 'coven@chat.shakespeare.lit'; + const promise = _converse.api.archive.query({'with': room_jid, 'groupchat': true, 'max':'10'}); + + await mock.waitUntilDiscoConfirmed(_converse, room_jid, null, [Strophe.NS.MAM]); + + const sent_stanzas = _converse.connection.sent_stanzas; + const sent_stanza = await u.waitUntil( + () => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.MAM}"]`, s).length).pop()); + const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); + + /* + * + * + * + * + * Thrice the brinded cat hath mew'd. + * + * + * + * + * + * + * + */ + const msg1 = $msg({'id':'iasd207', 'from': 'other@chat.shakespear.lit', 'to': 'romeo@montague.lit'}) + .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'34482-21985-73620'}) + .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) + .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() + .c('message', { + 'xmlns':'jabber:client', + 'to':'romeo@montague.lit', + 'id':'162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2', + 'from':'coven@chat.shakespeare.lit/firstwitch', + 'type':'groupchat' }) + .c('body').t("Thrice the brinded cat hath mew'd."); + _converse.connection._dataRecv(mock.createRequest(msg1)); + + /* Send an stanza to indicate the end of the result set. + * + * + * + * + * 28482-98726-73623 + * 09af3-cc343-b409f + * 20 + * + * + */ + const stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) + .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) + .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) + .c('first', {'index': '0'}).t('23452-4534-1').up() + .c('last').t('09af3-cc343-b409f').up() + .c('count').t('16'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + const result = await promise; + expect(result.messages.length).toBe(0); + done(); + })); + + it("can be used to query for all messages in a certain timespan", + mock.initConverse([], {}, async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + let sent_stanza, IQ_id; + const sendIQ = _converse.connection.sendIQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + const start = '2010-06-07T00:00:00Z'; + const end = '2010-07-07T13:23:54Z'; + _converse.api.archive.query({ + 'start': start, + 'end': end + }); + await u.waitUntil(() => sent_stanza); + const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); + expect(sent_stanza.toString()).toBe( + ``+ + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + ``+ + ``+ + `${dayjs(start).toISOString()}`+ + ``+ + ``+ + `${dayjs(end).toISOString()}`+ + ``+ + ``+ + ``+ + `` + ); + done(); + })); + + it("throws a TypeError if an invalid date is provided", + mock.initConverse([], {}, async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + try { + await _converse.api.archive.query({'start': 'not a real date'}); + } catch (e) { + expect(() => {throw e}).toThrow(new TypeError('archive.query: invalid date provided for: start')); + } + done(); + })); + + it("can be used to query for all messages after a certain time", + mock.initConverse([], {}, async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + let sent_stanza, IQ_id; + const sendIQ = _converse.connection.sendIQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) { + _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); + } + const start = '2010-06-07T00:00:00Z'; + _converse.api.archive.query({'start': start}); + await u.waitUntil(() => sent_stanza); + const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); + expect(sent_stanza.toString()).toBe( + ``+ + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + ``+ + ``+ + `${dayjs(start).toISOString()}`+ + ``+ + ``+ + ``+ + `` + ); + done(); + })); + + it("can be used to query for a limited set of results", + mock.initConverse([], {}, async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + let sent_stanza, IQ_id; + const sendIQ = _converse.connection.sendIQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + const start = '2010-06-07T00:00:00Z'; + _converse.api.archive.query({'start': start, 'max':10}); + await u.waitUntil(() => sent_stanza); + const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); + expect(sent_stanza.toString()).toBe( + ``+ + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + ``+ + ``+ + `${dayjs(start).toISOString()}`+ + ``+ + ``+ + ``+ + `10`+ + ``+ + ``+ + `` + ); + done(); + })); + + it("can be used to page through results", + mock.initConverse([], {}, async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + let sent_stanza, IQ_id; + const sendIQ = _converse.connection.sendIQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + const start = '2010-06-07T00:00:00Z'; + _converse.api.archive.query({ + 'start': start, + 'after': '09af3-cc343-b409f', + 'max':10 + }); + await u.waitUntil(() => sent_stanza); + const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); + expect(sent_stanza.toString()).toBe( + ``+ + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + ``+ + ``+ + `${dayjs(start).toISOString()}`+ + ``+ + ``+ + ``+ + `10`+ + `09af3-cc343-b409f`+ + ``+ + ``+ + ``); + done(); + })); + + it("accepts \"before\" with an empty string as value to reverse the order", + mock.initConverse([], {}, async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + let sent_stanza, IQ_id; + const sendIQ = _converse.connection.sendIQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + _converse.api.archive.query({'before': '', 'max':10}); + await u.waitUntil(() => sent_stanza); + const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); + expect(sent_stanza.toString()).toBe( + ``+ + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + ``+ + ``+ + ``+ + `10`+ + ``+ + ``+ + ``+ + ``); + done(); + })); + + it("accepts a _converse.RSM object for the query options", + mock.initConverse([], {}, async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + let sent_stanza, IQ_id; + const sendIQ = _converse.connection.sendIQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + // Normally the user wouldn't manually make a _converse.RSM object + // and pass it in. However, in the callback method an RSM object is + // returned which can be reused for easy paging. This test is + // more for that usecase. + const rsm = new _converse.RSM({'max': '10'}); + rsm['with'] = 'romeo@montague.lit'; // eslint-disable-line dot-notation + rsm.start = '2010-06-07T00:00:00Z'; + _converse.api.archive.query(rsm); + await u.waitUntil(() => sent_stanza); + const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); + expect(sent_stanza.toString()).toBe( + ``+ + ``+ + ``+ ``+ `urn:xmpp:mam:2`+ ``+ ``+ - `juliet@capulet.lit`+ - ``+ - ``+ - ``+ - ``); - done(); - })); - - it("can be used to query for archived messages from a chat room", - mock.initConverse([], {}, async function (done, _converse) { - - const room_jid = 'coven@chat.shakespeare.lit'; - _converse.api.archive.query({'with': room_jid, 'groupchat': true}); - await test_utils.waitUntilDiscoConfirmed(_converse, room_jid, null, [Strophe.NS.MAM]); - - const sent_stanzas = _converse.connection.sent_stanzas; - const stanza = await u.waitUntil( - () => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.MAM}"]`, s).length).pop()); - - const queryid = stanza.querySelector('query').getAttribute('queryid'); - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - ``+ - ``+ - ``+ - ``); - done(); - })); - - it("checks whether returned MAM messages from a MUC room are from the right JID", - mock.initConverse([], {}, async function (done, _converse) { - - const room_jid = 'coven@chat.shakespeare.lit'; - const promise = _converse.api.archive.query({'with': room_jid, 'groupchat': true, 'max':'10'}); - - await test_utils.waitUntilDiscoConfirmed(_converse, room_jid, null, [Strophe.NS.MAM]); - - const sent_stanzas = _converse.connection.sent_stanzas; - const sent_stanza = await u.waitUntil( - () => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.MAM}"]`, s).length).pop()); - const queryid = sent_stanza.querySelector('query').getAttribute('queryid'); - - /* - * - * - * - * - * Thrice the brinded cat hath mew'd. - * - * - * - * - * - * - * - */ - const msg1 = $msg({'id':'iasd207', 'from': 'other@chat.shakespear.lit', 'to': 'romeo@montague.lit'}) - .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'34482-21985-73620'}) - .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) - .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() - .c('message', { - 'xmlns':'jabber:client', - 'to':'romeo@montague.lit', - 'id':'162BEBB1-F6DB-4D9A-9BD8-CFDCC801A0B2', - 'from':'coven@chat.shakespeare.lit/firstwitch', - 'type':'groupchat' }) - .c('body').t("Thrice the brinded cat hath mew'd."); - _converse.connection._dataRecv(test_utils.createRequest(msg1)); - - /* Send an stanza to indicate the end of the result set. - * - * - * - * - * 28482-98726-73623 - * 09af3-cc343-b409f - * 20 - * - * - */ - const stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) - .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) - .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) - .c('first', {'index': '0'}).t('23452-4534-1').up() - .c('last').t('09af3-cc343-b409f').up() - .c('count').t('16'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - const result = await promise; - expect(result.messages.length).toBe(0); - done(); - })); - - it("can be used to query for all messages in a certain timespan", - mock.initConverse([], {}, async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); - let sent_stanza, IQ_id; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_stanza = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); - const start = '2010-06-07T00:00:00Z'; - const end = '2010-07-07T13:23:54Z'; - _converse.api.archive.query({ - 'start': start, - 'end': end - }); - await u.waitUntil(() => sent_stanza); - const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); - expect(sent_stanza.toString()).toBe( - ``+ - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ + `romeo@montague.lit`+ ``+ ``+ - `${dayjs(start).toISOString()}`+ + `${dayjs(rsm.start).toISOString()}`+ ``+ - ``+ - `${dayjs(end).toISOString()}`+ - ``+ - ``+ - ``+ - `` - ); - done(); - })); + ``+ + ``+ + `10`+ + ``+ + ``+ + ``); + done(); + })); - it("throws a TypeError if an invalid date is provided", - mock.initConverse([], {}, async function (done, _converse) { + it("returns an object which includes the messages and a _converse.RSM object", + mock.initConverse([], {}, async function (done, _converse) { - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); - try { - await _converse.api.archive.query({'start': 'not a real date'}); - } catch (e) { - expect(() => {throw e}).toThrow(new TypeError('archive.query: invalid date provided for: start')); - } - done(); - })); + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + let sent_stanza, IQ_id; + const sendIQ = _converse.connection.sendIQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + const promise = _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}); + await u.waitUntil(() => sent_stanza); + const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); - it("can be used to query for all messages after a certain time", - mock.initConverse([], {}, async function (done, _converse) { + /* + * + * + * + * + * Call me but love, and I'll be new baptized; Henceforth I never will be Romeo. + * + * + * + * + */ + const msg1 = $msg({'id':'aeb212', 'to':'juliet@capulet.lit/chamber'}) + .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'}) + .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) + .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() + .c('message', { + 'xmlns':'jabber:client', + 'to':'juliet@capulet.lit/balcony', + 'from':'romeo@montague.lit/orchard', + 'type':'chat' }) + .c('body').t("Call me but love, and I'll be new baptized;"); + _converse.connection._dataRecv(mock.createRequest(msg1)); - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); - let sent_stanza, IQ_id; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_stanza = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); - if (!_converse.disco_entities.get(_converse.domain).features.findWhere({'var': Strophe.NS.MAM})) { - _converse.disco_entities.get(_converse.domain).features.create({'var': Strophe.NS.MAM}); - } - const start = '2010-06-07T00:00:00Z'; - _converse.api.archive.query({'start': start}); - await u.waitUntil(() => sent_stanza); - const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); - expect(sent_stanza.toString()).toBe( - ``+ - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - ``+ - ``+ - `${dayjs(start).toISOString()}`+ - ``+ - ``+ - ``+ - `` - ); - done(); - })); + const msg2 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'}) + .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'}) + .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) + .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() + .c('message', { + 'xmlns':'jabber:client', + 'to':'juliet@capulet.lit/balcony', + 'from':'romeo@montague.lit/orchard', + 'type':'chat' }) + .c('body').t("Henceforth I never will be Romeo."); + _converse.connection._dataRecv(mock.createRequest(msg2)); - it("can be used to query for a limited set of results", - mock.initConverse([], {}, async function (done, _converse) { + /* Send an stanza to indicate the end of the result set. + * + * + * + * + * 28482-98726-73623 + * 09af3-cc343-b409f + * 20 + * + * + */ + const stanza = $iq({'type': 'result', 'id': IQ_id}) + .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) + .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) + .c('first', {'index': '0'}).t('23452-4534-1').up() + .c('last').t('09af3-cc343-b409f').up() + .c('count').t('16'); + _converse.connection._dataRecv(mock.createRequest(stanza)); - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); - let sent_stanza, IQ_id; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_stanza = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); - const start = '2010-06-07T00:00:00Z'; - _converse.api.archive.query({'start': start, 'max':10}); - await u.waitUntil(() => sent_stanza); - const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); - expect(sent_stanza.toString()).toBe( - ``+ - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - ``+ - ``+ - `${dayjs(start).toISOString()}`+ - ``+ - ``+ - ``+ - `10`+ - ``+ - ``+ - `` - ); - done(); - })); - - it("can be used to page through results", - mock.initConverse([], {}, async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); - let sent_stanza, IQ_id; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_stanza = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); - const start = '2010-06-07T00:00:00Z'; - _converse.api.archive.query({ - 'start': start, - 'after': '09af3-cc343-b409f', - 'max':10 - }); - await u.waitUntil(() => sent_stanza); - const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); - expect(sent_stanza.toString()).toBe( - ``+ - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - ``+ - ``+ - `${dayjs(start).toISOString()}`+ - ``+ - ``+ - ``+ - `10`+ - `09af3-cc343-b409f`+ - ``+ - ``+ - ``); - done(); - })); - - it("accepts \"before\" with an empty string as value to reverse the order", - mock.initConverse([], {}, async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); - let sent_stanza, IQ_id; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_stanza = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); - _converse.api.archive.query({'before': '', 'max':10}); - await u.waitUntil(() => sent_stanza); - const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); - expect(sent_stanza.toString()).toBe( - ``+ - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - ``+ - ``+ - ``+ - `10`+ - ``+ - ``+ - ``+ - ``); - done(); - })); - - it("accepts a _converse.RSM object for the query options", - mock.initConverse([], {}, async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); - let sent_stanza, IQ_id; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_stanza = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); - // Normally the user wouldn't manually make a _converse.RSM object - // and pass it in. However, in the callback method an RSM object is - // returned which can be reused for easy paging. This test is - // more for that usecase. - const rsm = new _converse.RSM({'max': '10'}); - rsm['with'] = 'romeo@montague.lit'; // eslint-disable-line dot-notation - rsm.start = '2010-06-07T00:00:00Z'; - _converse.api.archive.query(rsm); - await u.waitUntil(() => sent_stanza); - const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); - expect(sent_stanza.toString()).toBe( - ``+ - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - ``+ - ``+ - `romeo@montague.lit`+ - ``+ - ``+ - `${dayjs(rsm.start).toISOString()}`+ - ``+ - ``+ - ``+ - `10`+ - ``+ - ``+ - ``); - done(); - })); - - it("returns an object which includes the messages and a _converse.RSM object", - mock.initConverse([], {}, async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); - let sent_stanza, IQ_id; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_stanza = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); - const promise = _converse.api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}); - await u.waitUntil(() => sent_stanza); - const queryid = sent_stanza.nodeTree.querySelector('query').getAttribute('queryid'); - - /* - * - * - * - * - * Call me but love, and I'll be new baptized; Henceforth I never will be Romeo. - * - * - * - * - */ - const msg1 = $msg({'id':'aeb212', 'to':'juliet@capulet.lit/chamber'}) - .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'}) - .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) - .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() - .c('message', { - 'xmlns':'jabber:client', - 'to':'juliet@capulet.lit/balcony', - 'from':'romeo@montague.lit/orchard', - 'type':'chat' }) - .c('body').t("Call me but love, and I'll be new baptized;"); - _converse.connection._dataRecv(test_utils.createRequest(msg1)); - - const msg2 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'}) - .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'}) - .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) - .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() - .c('message', { - 'xmlns':'jabber:client', - 'to':'juliet@capulet.lit/balcony', - 'from':'romeo@montague.lit/orchard', - 'type':'chat' }) - .c('body').t("Henceforth I never will be Romeo."); - _converse.connection._dataRecv(test_utils.createRequest(msg2)); - - /* Send an stanza to indicate the end of the result set. - * - * - * - * - * 28482-98726-73623 - * 09af3-cc343-b409f - * 20 - * - * - */ - const stanza = $iq({'type': 'result', 'id': IQ_id}) - .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) - .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) - .c('first', {'index': '0'}).t('23452-4534-1').up() - .c('last').t('09af3-cc343-b409f').up() - .c('count').t('16'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - const result = await promise; - expect(result.messages.length).toBe(2); - expect(result.messages[0].outerHTML).toBe(msg1.nodeTree.outerHTML); - expect(result.messages[1].outerHTML).toBe(msg2.nodeTree.outerHTML); - expect(result.rsm['with']).toBe('romeo@capulet.lit'); // eslint-disable-line dot-notation - expect(result.rsm.max).toBe('10'); - expect(result.rsm.count).toBe('16'); - expect(result.rsm.first).toBe('23452-4534-1'); - expect(result.rsm.last).toBe('09af3-cc343-b409f'); - done() - })); - }); - - describe("The default preference", function () { - - it("is set once server support for MAM has been confirmed", - mock.initConverse([], {}, async function (done, _converse) { - - const entity = await _converse.api.disco.entities.get(_converse.domain); - spyOn(_converse, 'onMAMPreferences').and.callThrough(); - _converse.message_archiving = 'never'; - - const feature = new Model({ - 'var': Strophe.NS.MAM - }); - spyOn(feature, 'save').and.callFake(feature.set); // Save will complain about a url not being set - - entity.onFeatureAdded(feature); - - const IQ_stanzas = _converse.connection.IQ_stanzas; - let sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(s => sizzle('iq[type="get"] prefs[xmlns="urn:xmpp:mam:2"]', s).length).pop()); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ``+ - ``); - - /* Example 20. Server responds with current preferences - * - * - * - * - * - * - * - */ - let stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) - .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'roster'}) - .c('always').c('jid').t('romeo@montague.lit').up().up() - .c('never').c('jid').t('montague@montague.lit'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - await u.waitUntil(() => _converse.onMAMPreferences.calls.count()); - expect(_converse.onMAMPreferences).toHaveBeenCalled(); - - sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(s => sizzle('iq[type="set"] prefs[xmlns="urn:xmpp:mam:2"]', s).length).pop()); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ``+ - `romeo@montague.lit`+ - `montague@montague.lit`+ - ``+ - `` - ); - - expect(feature.get('preference')).toBe(undefined); - /* - * - * - * romeo@montague.lit - * - * - * montague@montague.lit - * - * - * - */ - stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) - .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'}) - .c('always').up() - .c('never'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => feature.save.calls.count()); - expect(feature.save).toHaveBeenCalled(); - expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation - done(); - })); - }); + const result = await promise; + expect(result.messages.length).toBe(2); + expect(result.messages[0].outerHTML).toBe(msg1.nodeTree.outerHTML); + expect(result.messages[1].outerHTML).toBe(msg2.nodeTree.outerHTML); + expect(result.rsm['with']).toBe('romeo@capulet.lit'); // eslint-disable-line dot-notation + expect(result.rsm.max).toBe('10'); + expect(result.rsm.count).toBe('16'); + expect(result.rsm.first).toBe('23452-4534-1'); + expect(result.rsm.last).toBe('09af3-cc343-b409f'); + done() + })); }); - describe("Chatboxes", function () { - describe("A Chatbox", function () { + describe("The default preference", function () { - it("will fetch archived messages once it's opened", - mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { + it("is set once server support for MAM has been confirmed", + mock.initConverse([], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + const entity = await _converse.api.disco.entities.get(_converse.domain); + spyOn(_converse, 'onMAMPreferences').and.callThrough(); + _converse.message_archiving = 'never'; - let sent_stanza, IQ_id; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_stanza = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); - await u.waitUntil(() => sent_stanza); - const stanza_el = sent_stanza.root().nodeTree; - const queryid = stanza_el.querySelector('query').getAttribute('queryid'); - expect(sent_stanza.toString()).toBe( - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - `mercutio@montague.lit`+ - ``+ - `50`+ - ``+ - `` - ); - const msg1 = $msg({'id':'aeb212', 'to': contact_jid}) - .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'}) - .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) - .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() - .c('message', { - 'xmlns':'jabber:client', - 'to': contact_jid, - 'from': _converse.bare_jid, - 'type':'chat' }) - .c('body').t("Call me but love, and I'll be new baptized;"); - _converse.connection._dataRecv(test_utils.createRequest(msg1)); - const msg2 = $msg({'id':'aeb213', 'to': contact_jid}) - .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'}) - .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) - .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() - .c('message', { - 'xmlns':'jabber:client', - 'to': contact_jid, - 'from': _converse.bare_jid, - 'type':'chat' }) - .c('body').t("Henceforth I never will be Romeo."); - _converse.connection._dataRecv(test_utils.createRequest(msg2)); - const stanza = $iq({'type': 'result', 'id': IQ_id}) - .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) - .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) - .c('first', {'index': '0'}).t('23452-4534-1').up() - .c('last').t('09af3-cc343-b409f').up() - .c('count').t('16'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - done(); - })); + const feature = new Model({ + 'var': Strophe.NS.MAM + }); + spyOn(feature, 'save').and.callFake(feature.set); // Save will complain about a url not being set - it("will show an error message if the MAM query times out", - mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { + entity.onFeatureAdded(feature); - const sendIQ = _converse.connection.sendIQ; + const IQ_stanzas = _converse.connection.IQ_stanzas; + let sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(s => sizzle('iq[type="get"] prefs[xmlns="urn:xmpp:mam:2"]', s).length).pop()); + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + ``+ + ``); - let timeout_happened = false; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sendIQ.bind(this)(iq, callback, errback); - if (!timeout_happened) { - if (typeof(iq.tree) === "function") { - iq = iq.tree(); - } - if (sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length) { - // We emulate a timeout event - callback(null); - timeout_happened = true; - } - } - }); - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + /* Example 20. Server responds with current preferences + * + * + * + * + * + * + * + */ + let stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) + .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'roster'}) + .c('always').c('jid').t('romeo@montague.lit').up().up() + .c('never').c('jid').t('montague@montague.lit'); + _converse.connection._dataRecv(mock.createRequest(stanza)); - const IQ_stanzas = _converse.connection.IQ_stanzas; - let sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length).pop()); - let queryid = sent_stanza.querySelector('query').getAttribute('queryid'); + await u.waitUntil(() => _converse.onMAMPreferences.calls.count()); + expect(_converse.onMAMPreferences).toHaveBeenCalled(); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - `mercutio@montague.lit`+ - ``+ - `50`+ - ``+ - ``); + sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(s => sizzle('iq[type="set"] prefs[xmlns="urn:xmpp:mam:2"]', s).length).pop()); + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + ``+ + `romeo@montague.lit`+ + `montague@montague.lit`+ + ``+ + `` + ); - const view = _converse.chatboxviews.get(contact_jid); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); - expect(view.model.messages.at(0).get('type')).toBe('error'); - expect(view.model.messages.at(0).get('message')).toBe('Timeout while trying to fetch archived messages.'); - - let err_message = view.el.querySelector('.message.chat-error'); - err_message.querySelector('.retry').click(); - expect(err_message.querySelector('.spinner')).not.toBe(null); - - while (_converse.connection.IQ_stanzas.length) { - _converse.connection.IQ_stanzas.pop(); - } - sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length).pop()); - queryid = sent_stanza.querySelector('query').getAttribute('queryid'); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ``+ - ``+ - `urn:xmpp:mam:2`+ - `mercutio@montague.lit`+ - ``+ - `50`+ - ``+ - ``); - - const msg1 = $msg({'id':'aeb212', 'to': contact_jid}) - .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid': queryid, 'id':'28482-98726-73623'}) - .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) - .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() - .c('message', { - 'xmlns':'jabber:client', - 'to': contact_jid, - 'from': _converse.bare_jid, - 'type':'chat' }) - .c('body').t("Call me but love, and I'll be new baptized;"); - _converse.connection._dataRecv(test_utils.createRequest(msg1)); - - const msg2 = $msg({'id':'aeb213', 'to': contact_jid}) - .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid': queryid, 'id':'28482-98726-73624'}) - .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) - .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:18:25Z'}).up() - .c('message', { - 'xmlns':'jabber:client', - 'to': contact_jid, - 'from': _converse.bare_jid, - 'type':'chat' }) - .c('body').t("Henceforth I never will be Romeo."); - _converse.connection._dataRecv(test_utils.createRequest(msg2)); - - const stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) - .c('fin', {'xmlns': 'urn:xmpp:mam:2', 'complete': true}) - .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) - .c('first', {'index': '0'}).t('28482-98726-73623').up() - .c('last').t('28482-98726-73624').up() - .c('count').t('2'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.messages.length === 2, 500); - err_message = view.el.querySelector('.message.chat-error'); - expect(err_message).toBe(null); - done(); - })); - }); + expect(feature.get('preference')).toBe(undefined); + /* + * + * + * romeo@montague.lit + * + * + * montague@montague.lit + * + * + * + */ + stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) + .c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'}) + .c('always').up() + .c('never'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => feature.save.calls.count()); + expect(feature.save).toHaveBeenCalled(); + expect(feature.get('preferences')['default']).toBe('never'); // eslint-disable-line dot-notation + done(); + })); + }); +}); + +describe("Chatboxes", function () { + describe("A Chatbox", function () { + + it("will fetch archived messages once it's opened", + mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + + let sent_stanza, IQ_id; + const sendIQ = _converse.connection.sendIQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sent_stanza = iq; + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + await u.waitUntil(() => sent_stanza); + const stanza_el = sent_stanza.root().nodeTree; + const queryid = stanza_el.querySelector('query').getAttribute('queryid'); + expect(sent_stanza.toString()).toBe( + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + `mercutio@montague.lit`+ + ``+ + `50`+ + ``+ + `` + ); + const msg1 = $msg({'id':'aeb212', 'to': contact_jid}) + .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73623'}) + .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) + .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() + .c('message', { + 'xmlns':'jabber:client', + 'to': contact_jid, + 'from': _converse.bare_jid, + 'type':'chat' }) + .c('body').t("Call me but love, and I'll be new baptized;"); + _converse.connection._dataRecv(mock.createRequest(msg1)); + const msg2 = $msg({'id':'aeb213', 'to': contact_jid}) + .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid':queryid, 'id':'28482-98726-73624'}) + .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) + .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() + .c('message', { + 'xmlns':'jabber:client', + 'to': contact_jid, + 'from': _converse.bare_jid, + 'type':'chat' }) + .c('body').t("Henceforth I never will be Romeo."); + _converse.connection._dataRecv(mock.createRequest(msg2)); + const stanza = $iq({'type': 'result', 'id': IQ_id}) + .c('fin', {'xmlns': 'urn:xmpp:mam:2'}) + .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) + .c('first', {'index': '0'}).t('23452-4534-1').up() + .c('last').t('09af3-cc343-b409f').up() + .c('count').t('16'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + done(); + })); + + it("will show an error message if the MAM query times out", + mock.initConverse(['discoInitialized'], {}, async function (done, _converse) { + + const sendIQ = _converse.connection.sendIQ; + + let timeout_happened = false; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + sendIQ.bind(this)(iq, callback, errback); + if (!timeout_happened) { + if (typeof(iq.tree) === "function") { + iq = iq.tree(); + } + if (sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length) { + // We emulate a timeout event + callback(null); + timeout_happened = true; + } + } + }); + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, null, [Strophe.NS.MAM]); + + const IQ_stanzas = _converse.connection.IQ_stanzas; + let sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length).pop()); + let queryid = sent_stanza.querySelector('query').getAttribute('queryid'); + + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + `mercutio@montague.lit`+ + ``+ + `50`+ + ``+ + ``); + + const view = _converse.chatboxviews.get(contact_jid); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); + expect(view.model.messages.at(0).get('type')).toBe('error'); + expect(view.model.messages.at(0).get('message')).toBe('Timeout while trying to fetch archived messages.'); + + let err_message = view.el.querySelector('.message.chat-error'); + err_message.querySelector('.retry').click(); + expect(err_message.querySelector('.spinner')).not.toBe(null); + + while (_converse.connection.IQ_stanzas.length) { + _converse.connection.IQ_stanzas.pop(); + } + sent_stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => sizzle('query[xmlns="urn:xmpp:mam:2"]', iq).length).pop()); + queryid = sent_stanza.querySelector('query').getAttribute('queryid'); + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + ``+ + ``+ + `urn:xmpp:mam:2`+ + `mercutio@montague.lit`+ + ``+ + `50`+ + ``+ + ``); + + const msg1 = $msg({'id':'aeb212', 'to': contact_jid}) + .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid': queryid, 'id':'28482-98726-73623'}) + .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) + .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up() + .c('message', { + 'xmlns':'jabber:client', + 'to': contact_jid, + 'from': _converse.bare_jid, + 'type':'chat' }) + .c('body').t("Call me but love, and I'll be new baptized;"); + _converse.connection._dataRecv(mock.createRequest(msg1)); + + const msg2 = $msg({'id':'aeb213', 'to': contact_jid}) + .c('result', {'xmlns': 'urn:xmpp:mam:2', 'queryid': queryid, 'id':'28482-98726-73624'}) + .c('forwarded', {'xmlns':'urn:xmpp:forward:0'}) + .c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:18:25Z'}).up() + .c('message', { + 'xmlns':'jabber:client', + 'to': contact_jid, + 'from': _converse.bare_jid, + 'type':'chat' }) + .c('body').t("Henceforth I never will be Romeo."); + _converse.connection._dataRecv(mock.createRequest(msg2)); + + const stanza = $iq({'type': 'result', 'id': sent_stanza.getAttribute('id')}) + .c('fin', {'xmlns': 'urn:xmpp:mam:2', 'complete': true}) + .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) + .c('first', {'index': '0'}).t('28482-98726-73623').up() + .c('last').t('28482-98726-73624').up() + .c('count').t('2'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.messages.length === 2, 500); + err_message = view.el.querySelector('.message.chat-error'); + expect(err_message).toBe(null); + done(); + })); }); }); diff --git a/spec/messages.js b/spec/messages.js index bb4078f64..63ed946a9 100644 --- a/spec/messages.js +++ b/spec/messages.js @@ -1,2081 +1,2079 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const { Promise, Strophe, $msg, dayjs, sizzle, _ } = converse.env; - const u = converse.env.utils; +/*global mock */ + +const { Promise, Strophe, $msg, dayjs, sizzle, _ } = converse.env; +const u = converse.env.utils; - describe("A Chat Message", function () { +describe("A Chat Message", function () { - it("is rejected if it's an unencapsulated forwarded message", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("is rejected if it's an unencapsulated forwarded message", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 2); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const forwarded_contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - let models = await _converse.api.chats.get(); - expect(models.length).toBe(1); - const received_stanza = u.toStanza(` - - A most courteous exposition! - - - - Yet I should kill thee with much cherishing. - - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(received_stanza)); - const sent_stanzas = _converse.connection.sent_stanzas; - const sent_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('error')).pop()); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ''+ - ''+ - ''+ - 'Forwarded messages not part of an encapsulating protocol are not supported'+ - ''+ - ''); - models = await _converse.api.chats.get(); - expect(models.length).toBe(1); - done(); - })); + await mock.waitForRoster(_converse, 'current', 2); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const forwarded_contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + let models = await _converse.api.chats.get(); + expect(models.length).toBe(1); + const received_stanza = u.toStanza(` + + A most courteous exposition! + + + + Yet I should kill thee with much cherishing. + + + + + + + `); + _converse.connection._dataRecv(mock.createRequest(received_stanza)); + const sent_stanzas = _converse.connection.sent_stanzas; + const sent_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('error')).pop()); + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + ''+ + ''+ + ''+ + 'Forwarded messages not part of an encapsulating protocol are not supported'+ + ''+ + ''); + models = await _converse.api.chats.get(); + expect(models.length).toBe(1); + done(); + })); - it("can be sent as a correction by clicking the pencil icon", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("can be sent as a correction by clicking the pencil icon", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 1); - await test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.api.chatviews.get(contact_jid); - const textarea = view.el.querySelector('textarea.chat-textarea'); + await mock.waitForRoster(_converse, 'current', 1); + await mock.openControlBox(_converse); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.api.chatviews.get(contact_jid); + const textarea = view.el.querySelector('textarea.chat-textarea'); - textarea.value = 'But soft, what light through yonder airlock breaks?'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - await new Promise(resolve => view.once('messageInserted', resolve)); + textarea.value = 'But soft, what light through yonder airlock breaks?'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelector('.chat-msg__text').textContent) - .toBe('But soft, what light through yonder airlock breaks?'); - expect(textarea.value).toBe(''); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelector('.chat-msg__text').textContent) + .toBe('But soft, what light through yonder airlock breaks?'); + expect(textarea.value).toBe(''); - const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'}); - expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2); - let action = view.el.querySelector('.chat-msg .chat-msg__action'); - expect(action.getAttribute('title')).toBe('Edit this message'); + const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'}); + expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2); + let action = view.el.querySelector('.chat-msg .chat-msg__action'); + expect(action.getAttribute('title')).toBe('Edit this message'); - action.style.opacity = 1; - action.click(); + action.style.opacity = 1; + action.click(); - expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?'); - expect(view.model.messages.at(0).get('correcting')).toBe(true); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); - await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg'))); + expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?'); + expect(view.model.messages.at(0).get('correcting')).toBe(true); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); + await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg'))); - spyOn(_converse.connection, 'send'); - textarea.value = 'But soft, what light through yonder window breaks?'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - expect(_converse.connection.send).toHaveBeenCalled(); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); + spyOn(_converse.connection, 'send'); + textarea.value = 'But soft, what light through yonder window breaks?'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + expect(_converse.connection.send).toHaveBeenCalled(); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); - const msg = _converse.connection.send.calls.all()[0].args[0]; - expect(msg.toLocaleString()) - .toBe(``+ - `But soft, what light through yonder window breaks?`+ - ``+ - ``+ - ``+ - ``+ - ``); - expect(view.model.messages.models.length).toBe(1); - const corrected_message = view.model.messages.at(0); - expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid')); - expect(corrected_message.get('correcting')).toBe(false); + const msg = _converse.connection.send.calls.all()[0].args[0]; + expect(msg.toLocaleString()) + .toBe(``+ + `But soft, what light through yonder window breaks?`+ + ``+ + ``+ + ``+ + ``+ + ``); + expect(view.model.messages.models.length).toBe(1); + const corrected_message = view.model.messages.at(0); + expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid')); + expect(corrected_message.get('correcting')).toBe(false); - const older_versions = corrected_message.get('older_versions'); - const keys = Object.keys(older_versions); - expect(keys.length).toBe(1); - expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?'); + const older_versions = corrected_message.get('older_versions'); + const keys = Object.keys(older_versions); + expect(keys.length).toBe(1); + expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?'); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false); - // Test that clicking the pencil icon a second time cancels editing. - action = view.el.querySelector('.chat-msg .chat-msg__action'); - action.style.opacity = 1; - action.click(); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); + // Test that clicking the pencil icon a second time cancels editing. + action = view.el.querySelector('.chat-msg .chat-msg__action'); + action.style.opacity = 1; + action.click(); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); - expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); - expect(view.model.messages.at(0).get('correcting')).toBe(true); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')) === true); + expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); + expect(view.model.messages.at(0).get('correcting')).toBe(true); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')) === true); - action = view.el.querySelector('.chat-msg .chat-msg__action'); - action.style.opacity = 1; - action.click(); - expect(textarea.value).toBe(''); - expect(view.model.messages.at(0).get('correcting')).toBe(false); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - await u.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500); + action = view.el.querySelector('.chat-msg .chat-msg__action'); + action.style.opacity = 1; + action.click(); + expect(textarea.value).toBe(''); + expect(view.model.messages.at(0).get('correcting')).toBe(false); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + await u.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500); - // Test that messages from other users don't have the pencil icon - _converse.handleMessageStanza( - $msg({ - 'from': contact_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': u.getUniqueId() - }).c('body').t('Hello').up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree() - ); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2); + // Test that messages from other users don't have the pencil icon + _converse.handleMessageStanza( + $msg({ + 'from': contact_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': u.getUniqueId() + }).c('body').t('Hello').up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree() + ); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.el.querySelectorAll('.chat-msg .chat-msg__action').length).toBe(2); - // Test confirmation dialog - spyOn(window, 'confirm').and.returnValue(true); - textarea.value = 'But soft, what light through yonder airlock breaks?'; - action = view.el.querySelector('.chat-msg .chat-msg__action'); - action.style.opacity = 1; - action.click(); - expect(window.confirm).toHaveBeenCalledWith( - 'You have an unsent message which will be lost if you continue. Are you sure?'); - expect(view.model.messages.at(0).get('correcting')).toBe(true); - expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); + // Test confirmation dialog + spyOn(window, 'confirm').and.returnValue(true); + textarea.value = 'But soft, what light through yonder airlock breaks?'; + action = view.el.querySelector('.chat-msg .chat-msg__action'); + action.style.opacity = 1; + action.click(); + expect(window.confirm).toHaveBeenCalledWith( + 'You have an unsent message which will be lost if you continue. Are you sure?'); + expect(view.model.messages.at(0).get('correcting')).toBe(true); + expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); - textarea.value = 'But soft, what light through yonder airlock breaks?' - action.click(); - expect(view.model.messages.at(0).get('correcting')).toBe(false); - expect(window.confirm.calls.count()).toBe(2); - expect(window.confirm.calls.argsFor(0)).toEqual( - ['You have an unsent message which will be lost if you continue. Are you sure?']); - expect(window.confirm.calls.argsFor(1)).toEqual( - ['You have an unsent message which will be lost if you continue. Are you sure?']); - done(); - })); + textarea.value = 'But soft, what light through yonder airlock breaks?' + action.click(); + expect(view.model.messages.at(0).get('correcting')).toBe(false); + expect(window.confirm.calls.count()).toBe(2); + expect(window.confirm.calls.argsFor(0)).toEqual( + ['You have an unsent message which will be lost if you continue. Are you sure?']); + expect(window.confirm.calls.argsFor(1)).toEqual( + ['You have an unsent message which will be lost if you continue. Are you sure?']); + done(); + })); - it("can be sent as a correction by using the up arrow", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("can be sent as a correction by using the up arrow", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 1); - await test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid) - const view = _converse.api.chatviews.get(contact_jid); - const textarea = view.el.querySelector('textarea.chat-textarea'); - expect(textarea.value).toBe(''); - view.onKeyDown({ - target: textarea, - keyCode: 38 // Up arrow - }); - expect(textarea.value).toBe(''); + await mock.waitForRoster(_converse, 'current', 1); + await mock.openControlBox(_converse); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid) + const view = _converse.api.chatviews.get(contact_jid); + const textarea = view.el.querySelector('textarea.chat-textarea'); + expect(textarea.value).toBe(''); + view.onKeyDown({ + target: textarea, + keyCode: 38 // Up arrow + }); + expect(textarea.value).toBe(''); - textarea.value = 'But soft, what light through yonder airlock breaks?'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelector('.chat-msg__text').textContent) - .toBe('But soft, what light through yonder airlock breaks?'); + textarea.value = 'But soft, what light through yonder airlock breaks?'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelector('.chat-msg__text').textContent) + .toBe('But soft, what light through yonder airlock breaks?'); - const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'}); - expect(textarea.value).toBe(''); - view.onKeyDown({ - target: textarea, - keyCode: 38 // Up arrow - }); - expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?'); - expect(view.model.messages.at(0).get('correcting')).toBe(true); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500); + const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'}); + expect(textarea.value).toBe(''); + view.onKeyDown({ + target: textarea, + keyCode: 38 // Up arrow + }); + expect(textarea.value).toBe('But soft, what light through yonder airlock breaks?'); + expect(view.model.messages.at(0).get('correcting')).toBe(true); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500); - spyOn(_converse.connection, 'send'); - textarea.value = 'But soft, what light through yonder window breaks?'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - expect(_converse.connection.send).toHaveBeenCalled(); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); + spyOn(_converse.connection, 'send'); + textarea.value = 'But soft, what light through yonder window breaks?'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + expect(_converse.connection.send).toHaveBeenCalled(); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); - const msg = _converse.connection.send.calls.all()[0].args[0]; - expect(msg.toLocaleString()) - .toBe(``+ - `But soft, what light through yonder window breaks?`+ - ``+ - ``+ - ``+ - ``+ - ``); - expect(view.model.messages.models.length).toBe(1); - const corrected_message = view.model.messages.at(0); - expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid')); - expect(corrected_message.get('correcting')).toBe(false); + const msg = _converse.connection.send.calls.all()[0].args[0]; + expect(msg.toLocaleString()) + .toBe(``+ + `But soft, what light through yonder window breaks?`+ + ``+ + ``+ + ``+ + ``+ + ``); + expect(view.model.messages.models.length).toBe(1); + const corrected_message = view.model.messages.at(0); + expect(corrected_message.get('msgid')).toBe(first_msg.get('msgid')); + expect(corrected_message.get('correcting')).toBe(false); - const older_versions = corrected_message.get('older_versions'); - const keys = Object.keys(older_versions); - expect(keys.length).toBe(1); - expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?'); + const older_versions = corrected_message.get('older_versions'); + const keys = Object.keys(older_versions); + expect(keys.length).toBe(1); + expect(older_versions[keys[0]]).toBe('But soft, what light through yonder airlock breaks?'); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - await u.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + await u.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500); - // Test that pressing the down arrow cancels message correction - expect(textarea.value).toBe(''); - view.onKeyDown({ - target: textarea, - keyCode: 38 // Up arrow - }); - expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); - expect(view.model.messages.at(0).get('correcting')).toBe(true); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500); - expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); - view.onKeyDown({ - target: textarea, - keyCode: 40 // Down arrow - }); - expect(textarea.value).toBe(''); - expect(view.model.messages.at(0).get('correcting')).toBe(false); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - await u.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500); + // Test that pressing the down arrow cancels message correction + expect(textarea.value).toBe(''); + view.onKeyDown({ + target: textarea, + keyCode: 38 // Up arrow + }); + expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); + expect(view.model.messages.at(0).get('correcting')).toBe(true); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + await u.waitUntil(() => u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500); + expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); + view.onKeyDown({ + target: textarea, + keyCode: 40 // Down arrow + }); + expect(textarea.value).toBe(''); + expect(view.model.messages.at(0).get('correcting')).toBe(false); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + await u.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500); - textarea.value = 'It is the east, and Juliet is the one.'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); + textarea.value = 'It is the east, and Juliet is the one.'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); - textarea.value = 'Arise, fair sun, and kill the envious moon'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(3); + textarea.value = 'Arise, fair sun, and kill the envious moon'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(3); - view.onKeyDown({ - target: textarea, - keyCode: 38 // Up arrow - }); - expect(textarea.value).toBe('Arise, fair sun, and kill the envious moon'); - expect(view.model.messages.at(0).get('correcting')).toBeFalsy(); - expect(view.model.messages.at(1).get('correcting')).toBeFalsy(); - expect(view.model.messages.at(2).get('correcting')).toBe(true); - await u.waitUntil(() => u.hasClass('correcting', sizzle('.chat-msg:last', view.el).pop()), 500); + view.onKeyDown({ + target: textarea, + keyCode: 38 // Up arrow + }); + expect(textarea.value).toBe('Arise, fair sun, and kill the envious moon'); + expect(view.model.messages.at(0).get('correcting')).toBeFalsy(); + expect(view.model.messages.at(1).get('correcting')).toBeFalsy(); + expect(view.model.messages.at(2).get('correcting')).toBe(true); + await u.waitUntil(() => u.hasClass('correcting', sizzle('.chat-msg:last', view.el).pop()), 500); - textarea.selectionEnd = 0; // Happens by pressing up, - // but for some reason not in tests, so we set it manually. - view.onKeyDown({ - target: textarea, - keyCode: 38 // Up arrow - }); - expect(textarea.value).toBe('It is the east, and Juliet is the one.'); - expect(view.model.messages.at(0).get('correcting')).toBeFalsy(); - expect(view.model.messages.at(1).get('correcting')).toBe(true); - expect(view.model.messages.at(2).get('correcting')).toBeFalsy(); - await u.waitUntil(() => u.hasClass('correcting', sizzle('.chat-msg', view.el)[1]), 500); + textarea.selectionEnd = 0; // Happens by pressing up, + // but for some reason not in tests, so we set it manually. + view.onKeyDown({ + target: textarea, + keyCode: 38 // Up arrow + }); + expect(textarea.value).toBe('It is the east, and Juliet is the one.'); + expect(view.model.messages.at(0).get('correcting')).toBeFalsy(); + expect(view.model.messages.at(1).get('correcting')).toBe(true); + expect(view.model.messages.at(2).get('correcting')).toBeFalsy(); + await u.waitUntil(() => u.hasClass('correcting', sizzle('.chat-msg', view.el)[1]), 500); - textarea.value = 'It is the east, and Juliet is the sun.'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); + textarea.value = 'It is the east, and Juliet is the sun.'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); - expect(textarea.value).toBe(''); - const messages = view.el.querySelectorAll('.chat-msg'); - expect(messages.length).toBe(3); - expect(messages[0].querySelector('.chat-msg__text').textContent) - .toBe('But soft, what light through yonder window breaks?'); - expect(messages[1].querySelector('.chat-msg__text').textContent) - .toBe('It is the east, and Juliet is the sun.'); - expect(messages[2].querySelector('.chat-msg__text').textContent) - .toBe('Arise, fair sun, and kill the envious moon'); + expect(textarea.value).toBe(''); + const messages = view.el.querySelectorAll('.chat-msg'); + expect(messages.length).toBe(3); + expect(messages[0].querySelector('.chat-msg__text').textContent) + .toBe('But soft, what light through yonder window breaks?'); + expect(messages[1].querySelector('.chat-msg__text').textContent) + .toBe('It is the east, and Juliet is the sun.'); + expect(messages[2].querySelector('.chat-msg__text').textContent) + .toBe('Arise, fair sun, and kill the envious moon'); - expect(view.model.messages.at(0).get('correcting')).toBeFalsy(); - expect(view.model.messages.at(1).get('correcting')).toBeFalsy(); - expect(view.model.messages.at(2).get('correcting')).toBeFalsy(); - done(); - })); + expect(view.model.messages.at(0).get('correcting')).toBeFalsy(); + expect(view.model.messages.at(1).get('correcting')).toBeFalsy(); + expect(view.model.messages.at(2).get('correcting')).toBeFalsy(); + done(); + })); - it("can be received out of order, and will still be displayed in the right order", + it("can be received out of order, and will still be displayed in the right order", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length) + _converse.filter_by_resource = true; + + let msg = $msg({ + 'xmlns': 'jabber:client', + 'id': _converse.connection.getUniqueId(), + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("message").up() + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T13:08:25Z'}) + .tree(); + await _converse.handleMessageStanza(msg); + const view = _converse.api.chatviews.get(sender_jid); + + msg = $msg({ + 'xmlns': 'jabber:client', + 'id': _converse.connection.getUniqueId(), + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("Older message").up() + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'}) + .tree(); + _converse.handleMessageStanza(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + + msg = $msg({ + 'xmlns': 'jabber:client', + 'id': _converse.connection.getUniqueId(), + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("Inbetween message").up() + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}) + .tree(); + _converse.handleMessageStanza(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + + msg = $msg({ + 'xmlns': 'jabber:client', + 'id': _converse.connection.getUniqueId(), + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("another inbetween message").up() + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}) + .tree(); + _converse.handleMessageStanza(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + + msg = $msg({ + 'xmlns': 'jabber:client', + 'id': _converse.connection.getUniqueId(), + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("An earlier message on the next day").up() + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'}) + .tree(); + _converse.handleMessageStanza(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + + msg = $msg({ + 'xmlns': 'jabber:client', + 'id': _converse.connection.getUniqueId(), + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat'}) + .c('body').t("newer message from the next day").up() + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'}) + .tree(); + _converse.handleMessageStanza(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + + // Insert message, to also check that + // text messages are inserted correctly with + // temporary chat events in the chat contents. + msg = $msg({ + 'id': _converse.connection.getUniqueId(), + 'to': _converse.bare_jid, + 'xmlns': 'jabber:client', + 'from': sender_jid, + 'type': 'chat'}) + .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up() + .tree(); + _converse.handleMessageStanza(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + + msg = $msg({ + 'id': _converse.connection.getUniqueId(), + 'to': _converse.bare_jid, + 'xmlns': 'jabber:client', + 'from': sender_jid, + 'type': 'chat'}) + .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up() + .c('body').t("latest message") + .tree(); + await _converse.handleMessageStanza(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + + view.clearSpinner(); //cleanup + expect(view.content.querySelectorAll('.date-separator').length).toEqual(4); + + let day = sizzle('.date-separator:first', view.content).pop(); + expect(day.getAttribute('data-isodate')).toEqual(dayjs('2017-12-31T00:00:00').toISOString()); + + let time = sizzle('time:first', view.content).pop(); + expect(time.textContent).toEqual('Sunday Dec 31st 2017') + + day = sizzle('.date-separator:first', view.content).pop(); + expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Older message'); + + let el = sizzle('.chat-msg:first', view.content).pop().querySelector('.chat-msg__text') + expect(u.hasClass('chat-msg--followup', el)).toBe(false); + expect(el.textContent).toEqual('Older message'); + + time = sizzle('time.separator-text:eq(1)', view.content).pop(); + expect(time.textContent).toEqual("Monday Jan 1st 2018"); + + day = sizzle('.date-separator:eq(1)', view.content).pop(); + expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-01T00:00:00').toISOString()); + expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Inbetween message'); + + el = sizzle('.chat-msg:eq(1)', view.content).pop(); + expect(el.querySelector('.chat-msg__text').textContent).toEqual('Inbetween message'); + expect(el.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('another inbetween message'); + el = sizzle('.chat-msg:eq(2)', view.content).pop(); + expect(el.querySelector('.chat-msg__text').textContent) + .toEqual('another inbetween message'); + expect(u.hasClass('chat-msg--followup', el)).toBe(true); + + time = sizzle('time.separator-text:nth(2)', view.content).pop(); + expect(time.textContent).toEqual("Tuesday Jan 2nd 2018"); + + day = sizzle('.date-separator:nth(2)', view.content).pop(); + expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-02T00:00:00').toISOString()); + expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('An earlier message on the next day'); + + el = sizzle('.chat-msg:eq(3)', view.content).pop(); + expect(el.querySelector('.chat-msg__text').textContent).toEqual('An earlier message on the next day'); + expect(u.hasClass('chat-msg--followup', el)).toBe(false); + + el = sizzle('.chat-msg:eq(4)', view.content).pop(); + expect(el.querySelector('.chat-msg__text').textContent).toEqual('message'); + expect(el.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('newer message from the next day'); + expect(u.hasClass('chat-msg--followup', el)).toBe(false); + + day = sizzle('.date-separator:last', view.content).pop(); + expect(day.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString()); + expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message'); + expect(u.hasClass('chat-msg--followup', el)).toBe(false); + done(); + })); + + it("is ignored if it's a malformed headline message", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length) - _converse.filter_by_resource = true; + // Ideally we wouldn't have to filter out headline + // messages, but Prosody gives them the wrong 'type' :( + sinon.spy(converse.env.log, 'info'); + sinon.spy(_converse.api.chatboxes, 'get'); + sinon.spy(u, 'isHeadlineMessage'); + const msg = $msg({ + from: 'montague.lit', + to: _converse.bare_jid, + type: 'chat', + id: u.getUniqueId() + }).c('body').t("This headline message will not be shown").tree(); + await _converse.handleMessageStanza(msg); + expect(converse.env.log.info.calledWith( + "handleMessageStanza: Ignoring incoming headline message from JID: montague.lit" + )).toBeTruthy(); + expect(u.isHeadlineMessage.called).toBeTruthy(); + expect(u.isHeadlineMessage.returned(true)).toBeTruthy(); + expect(_converse.api.chatboxes.get.called).toBeFalsy(); + // Remove sinon spies + converse.env.log.info.restore(); + _converse.api.chatboxes.get.restore(); + u.isHeadlineMessage.restore(); + done(); + })); - let msg = $msg({ - 'xmlns': 'jabber:client', - 'id': _converse.connection.getUniqueId(), - 'to': _converse.bare_jid, - 'from': sender_jid, - 'type': 'chat'}) - .c('body').t("message").up() - .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T13:08:25Z'}) - .tree(); - await _converse.handleMessageStanza(msg); - const view = _converse.api.chatviews.get(sender_jid); - msg = $msg({ - 'xmlns': 'jabber:client', - 'id': _converse.connection.getUniqueId(), - 'to': _converse.bare_jid, - 'from': sender_jid, - 'type': 'chat'}) - .c('body').t("Older message").up() - .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'}) - .tree(); - _converse.handleMessageStanza(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); + it("can be a carbon message, as defined in XEP-0280", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - msg = $msg({ - 'xmlns': 'jabber:client', - 'id': _converse.connection.getUniqueId(), - 'to': _converse.bare_jid, - 'from': sender_jid, - 'type': 'chat'}) - .c('body').t("Inbetween message").up() - .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}) - .tree(); - _converse.handleMessageStanza(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); + const include_nick = false; + await mock.waitForRoster(_converse, 'current', 2, include_nick); + await mock.openControlBox(_converse); - msg = $msg({ - 'xmlns': 'jabber:client', - 'id': _converse.connection.getUniqueId(), - 'to': _converse.bare_jid, - 'from': sender_jid, - 'type': 'chat'}) - .c('body').t("another inbetween message").up() - .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'}) - .tree(); - _converse.handleMessageStanza(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); - - msg = $msg({ - 'xmlns': 'jabber:client', - 'id': _converse.connection.getUniqueId(), - 'to': _converse.bare_jid, - 'from': sender_jid, - 'type': 'chat'}) - .c('body').t("An earlier message on the next day").up() - .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'}) - .tree(); - _converse.handleMessageStanza(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); - - msg = $msg({ - 'xmlns': 'jabber:client', - 'id': _converse.connection.getUniqueId(), - 'to': _converse.bare_jid, - 'from': sender_jid, - 'type': 'chat'}) - .c('body').t("newer message from the next day").up() - .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'}) - .tree(); - _converse.handleMessageStanza(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); - - // Insert message, to also check that - // text messages are inserted correctly with - // temporary chat events in the chat contents. - msg = $msg({ - 'id': _converse.connection.getUniqueId(), - 'to': _converse.bare_jid, + // Send a message from a different resource + const msgtext = 'This is a carbon message'; + const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const msg = $msg({ + 'from': _converse.bare_jid, + 'id': u.getUniqueId(), + 'to': _converse.connection.jid, + 'type': 'chat', + 'xmlns': 'jabber:client' + }).c('received', {'xmlns': 'urn:xmpp:carbons:2'}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('message', { 'xmlns': 'jabber:client', 'from': sender_jid, - 'type': 'chat'}) - .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up() - .tree(); - _converse.handleMessageStanza(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); + 'to': _converse.bare_jid+'/another-resource', + 'type': 'chat' + }).c('body').t(msgtext).tree(); - msg = $msg({ - 'id': _converse.connection.getUniqueId(), - 'to': _converse.bare_jid, + await _converse.handleMessageStanza(msg); + const chatbox = _converse.chatboxes.get(sender_jid); + const view = _converse.chatboxviews.get(sender_jid); + + expect(chatbox).toBeDefined(); + expect(view).toBeDefined(); + // Check that the message was received and check the message parameters + await u.waitUntil(() => chatbox.messages.length); + const msg_obj = chatbox.messages.models[0]; + expect(msg_obj.get('message')).toEqual(msgtext); + expect(msg_obj.get('fullname')).toBeUndefined(); + expect(msg_obj.get('nickname')).toBe(null); + expect(msg_obj.get('sender')).toEqual('them'); + expect(msg_obj.get('is_delayed')).toEqual(false); + // Now check that the message appears inside the chatbox in the DOM + await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text')); + + expect(view.content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(msgtext); + expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); + await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet') + expect(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet'); + done(); + })); + + it("can be a carbon message that this user sent from a different client, as defined in XEP-0280", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + // Send a message from a different resource + const msgtext = 'This is a sent carbon message'; + const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const msg = $msg({ + 'from': _converse.bare_jid, + 'id': u.getUniqueId(), + 'to': _converse.connection.jid, + 'type': 'chat', + 'xmlns': 'jabber:client' + }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('message', { 'xmlns': 'jabber:client', - 'from': sender_jid, - 'type': 'chat'}) - .c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up() - .c('body').t("latest message") - .tree(); - await _converse.handleMessageStanza(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); + 'from': _converse.bare_jid+'/another-resource', + 'to': recipient_jid, + 'type': 'chat' + }).c('body').t(msgtext).tree(); - view.clearSpinner(); //cleanup - expect(view.content.querySelectorAll('.date-separator').length).toEqual(4); + await _converse.handleMessageStanza(msg); + // Check that the chatbox and its view now exist + const chatbox = await _converse.api.chats.get(recipient_jid); + const view = _converse.api.chatviews.get(recipient_jid); + expect(chatbox).toBeDefined(); + expect(view).toBeDefined(); - let day = sizzle('.date-separator:first', view.content).pop(); - expect(day.getAttribute('data-isodate')).toEqual(dayjs('2017-12-31T00:00:00').toISOString()); + // Check that the message was received and check the message parameters + expect(chatbox.messages.length).toEqual(1); + const msg_obj = chatbox.messages.models[0]; + expect(msg_obj.get('message')).toEqual(msgtext); + expect(msg_obj.get('fullname')).toEqual(_converse.xmppstatus.get('fullname')); + expect(msg_obj.get('sender')).toEqual('me'); + expect(msg_obj.get('is_delayed')).toEqual(false); + // Now check that the message appears inside the chatbox in the DOM + const msg_txt = view.el.querySelector('.chat-content .chat-msg .chat-msg__text').textContent; + expect(msg_txt).toEqual(msgtext); + done(); + })); - let time = sizzle('time:first', view.content).pop(); - expect(time.textContent).toEqual('Sunday Dec 31st 2017') + it("will be discarded if it's a malicious message meant to look like a carbon copy", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - day = sizzle('.date-separator:first', view.content).pop(); - expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Older message'); - - let el = sizzle('.chat-msg:first', view.content).pop().querySelector('.chat-msg__text') - expect(u.hasClass('chat-msg--followup', el)).toBe(false); - expect(el.textContent).toEqual('Older message'); - - time = sizzle('time.separator-text:eq(1)', view.content).pop(); - expect(time.textContent).toEqual("Monday Jan 1st 2018"); - - day = sizzle('.date-separator:eq(1)', view.content).pop(); - expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-01T00:00:00').toISOString()); - expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Inbetween message'); - - el = sizzle('.chat-msg:eq(1)', view.content).pop(); - expect(el.querySelector('.chat-msg__text').textContent).toEqual('Inbetween message'); - expect(el.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('another inbetween message'); - el = sizzle('.chat-msg:eq(2)', view.content).pop(); - expect(el.querySelector('.chat-msg__text').textContent) - .toEqual('another inbetween message'); - expect(u.hasClass('chat-msg--followup', el)).toBe(true); - - time = sizzle('time.separator-text:nth(2)', view.content).pop(); - expect(time.textContent).toEqual("Tuesday Jan 2nd 2018"); - - day = sizzle('.date-separator:nth(2)', view.content).pop(); - expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-02T00:00:00').toISOString()); - expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('An earlier message on the next day'); - - el = sizzle('.chat-msg:eq(3)', view.content).pop(); - expect(el.querySelector('.chat-msg__text').textContent).toEqual('An earlier message on the next day'); - expect(u.hasClass('chat-msg--followup', el)).toBe(false); - - el = sizzle('.chat-msg:eq(4)', view.content).pop(); - expect(el.querySelector('.chat-msg__text').textContent).toEqual('message'); - expect(el.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('newer message from the next day'); - expect(u.hasClass('chat-msg--followup', el)).toBe(false); - - day = sizzle('.date-separator:last', view.content).pop(); - expect(day.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString()); - expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message'); - expect(u.hasClass('chat-msg--followup', el)).toBe(false); - done(); - })); - - it("is ignored if it's a malformed headline message", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - // Ideally we wouldn't have to filter out headline - // messages, but Prosody gives them the wrong 'type' :( - sinon.spy(converse.env.log, 'info'); - sinon.spy(_converse.api.chatboxes, 'get'); - sinon.spy(u, 'isHeadlineMessage'); - const msg = $msg({ - from: 'montague.lit', - to: _converse.bare_jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t("This headline message will not be shown").tree(); - await _converse.handleMessageStanza(msg); - expect(converse.env.log.info.calledWith( - "handleMessageStanza: Ignoring incoming headline message from JID: montague.lit" - )).toBeTruthy(); - expect(u.isHeadlineMessage.called).toBeTruthy(); - expect(u.isHeadlineMessage.returned(true)).toBeTruthy(); - expect(_converse.api.chatboxes.get.called).toBeFalsy(); - // Remove sinon spies - converse.env.log.info.restore(); - _converse.api.chatboxes.get.restore(); - u.isHeadlineMessage.restore(); - done(); - })); - - - it("can be a carbon message, as defined in XEP-0280", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const include_nick = false; - await test_utils.waitForRoster(_converse, 'current', 2, include_nick); - await test_utils.openControlBox(_converse); - - // Send a message from a different resource - const msgtext = 'This is a carbon message'; - const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const msg = $msg({ - 'from': _converse.bare_jid, - 'id': u.getUniqueId(), + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + /* + * + * + * + * Please come to Creepy Valley tonight, alone! + * + * + * + * + */ + const msgtext = 'Please come to Creepy Valley tonight, alone!'; + const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const impersonated_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const msg = $msg({ + 'from': sender_jid, + 'id': u.getUniqueId(), + 'to': _converse.connection.jid, + 'type': 'chat', + 'xmlns': 'jabber:client' + }).c('received', {'xmlns': 'urn:xmpp:carbons:2'}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('message', { + 'xmlns': 'jabber:client', + 'from': impersonated_jid, 'to': _converse.connection.jid, - 'type': 'chat', - 'xmlns': 'jabber:client' - }).c('received', {'xmlns': 'urn:xmpp:carbons:2'}) - .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) - .c('message', { - 'xmlns': 'jabber:client', - 'from': sender_jid, - 'to': _converse.bare_jid+'/another-resource', - 'type': 'chat' - }).c('body').t(msgtext).tree(); + 'type': 'chat' + }).c('body').t(msgtext).tree(); + await _converse.handleMessageStanza(msg); - await _converse.handleMessageStanza(msg); - const chatbox = _converse.chatboxes.get(sender_jid); - const view = _converse.chatboxviews.get(sender_jid); + // Check that chatbox for impersonated user is not created. + let chatbox = await _converse.api.chats.get(impersonated_jid); + expect(chatbox).toBe(null); - expect(chatbox).toBeDefined(); - expect(view).toBeDefined(); - // Check that the message was received and check the message parameters - await u.waitUntil(() => chatbox.messages.length); - const msg_obj = chatbox.messages.models[0]; - expect(msg_obj.get('message')).toEqual(msgtext); - expect(msg_obj.get('fullname')).toBeUndefined(); - expect(msg_obj.get('nickname')).toBe(null); - expect(msg_obj.get('sender')).toEqual('them'); - expect(msg_obj.get('is_delayed')).toEqual(false); - // Now check that the message appears inside the chatbox in the DOM - await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text')); + // Check that the chatbox for the malicous user is not created + chatbox = await _converse.api.chats.get(sender_jid); + expect(chatbox).toBe(null); + done(); + })); - expect(view.content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(msgtext); - expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); - await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet') - expect(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet'); - done(); - })); + it("received for a minimized chat box will increment a counter on its header", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - it("can be a carbon message that this user sent from a different client, as defined in XEP-0280", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + if (_converse.view_mode === 'fullscreen') { + return done(); + } + await mock.waitForRoster(_converse, 'current'); + const contact_name = mock.cur_names[0]; + const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openControlBox(_converse); + spyOn(_converse.api, "trigger").and.callThrough(); - await test_utils.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + await mock.openChatBoxFor(_converse, contact_jid); + const chatview = _converse.api.chatviews.get(contact_jid); + expect(u.isVisible(chatview.el)).toBeTruthy(); + expect(chatview.model.get('minimized')).toBeFalsy(); + chatview.el.querySelector('.toggle-chatbox-button').click(); + expect(chatview.model.get('minimized')).toBeTruthy(); + var message = 'This message is sent to a minimized chatbox'; + var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + var msg = $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('body').t(message).up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + await _converse.handleMessageStanza(msg); - // Send a message from a different resource - const msgtext = 'This is a sent carbon message'; - const recipient_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const msg = $msg({ - 'from': _converse.bare_jid, - 'id': u.getUniqueId(), - 'to': _converse.connection.jid, - 'type': 'chat', - 'xmlns': 'jabber:client' - }).c('sent', {'xmlns': 'urn:xmpp:carbons:2'}) - .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) - .c('message', { - 'xmlns': 'jabber:client', - 'from': _converse.bare_jid+'/another-resource', - 'to': recipient_jid, - 'type': 'chat' - }).c('body').t(msgtext).tree(); - - await _converse.handleMessageStanza(msg); - // Check that the chatbox and its view now exist - const chatbox = await _converse.api.chats.get(recipient_jid); - const view = _converse.api.chatviews.get(recipient_jid); - expect(chatbox).toBeDefined(); - expect(view).toBeDefined(); - - // Check that the message was received and check the message parameters - expect(chatbox.messages.length).toEqual(1); - const msg_obj = chatbox.messages.models[0]; - expect(msg_obj.get('message')).toEqual(msgtext); - expect(msg_obj.get('fullname')).toEqual(_converse.xmppstatus.get('fullname')); - expect(msg_obj.get('sender')).toEqual('me'); - expect(msg_obj.get('is_delayed')).toEqual(false); - // Now check that the message appears inside the chatbox in the DOM - const msg_txt = view.el.querySelector('.chat-content .chat-msg .chat-msg__text').textContent; - expect(msg_txt).toEqual(msgtext); - done(); - })); - - it("will be discarded if it's a malicious message meant to look like a carbon copy", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - /* - * - * - * - * Please come to Creepy Valley tonight, alone! - * - * - * - * - */ - const msgtext = 'Please come to Creepy Valley tonight, alone!'; - const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const impersonated_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const msg = $msg({ - 'from': sender_jid, - 'id': u.getUniqueId(), - 'to': _converse.connection.jid, - 'type': 'chat', - 'xmlns': 'jabber:client' - }).c('received', {'xmlns': 'urn:xmpp:carbons:2'}) - .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) - .c('message', { - 'xmlns': 'jabber:client', - 'from': impersonated_jid, - 'to': _converse.connection.jid, - 'type': 'chat' - }).c('body').t(msgtext).tree(); - await _converse.handleMessageStanza(msg); - - // Check that chatbox for impersonated user is not created. - let chatbox = await _converse.api.chats.get(impersonated_jid); - expect(chatbox).toBe(null); - - // Check that the chatbox for the malicous user is not created - chatbox = await _converse.api.chats.get(sender_jid); - expect(chatbox).toBe(null); - done(); - })); - - it("received for a minimized chat box will increment a counter on its header", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - if (_converse.view_mode === 'fullscreen') { - return done(); - } - await test_utils.waitForRoster(_converse, 'current'); - const contact_name = mock.cur_names[0]; - const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openControlBox(_converse); - spyOn(_converse.api, "trigger").and.callThrough(); - - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - await test_utils.openChatBoxFor(_converse, contact_jid); - const chatview = _converse.api.chatviews.get(contact_jid); - expect(u.isVisible(chatview.el)).toBeTruthy(); - expect(chatview.model.get('minimized')).toBeFalsy(); - chatview.el.querySelector('.toggle-chatbox-button').click(); - expect(chatview.model.get('minimized')).toBeTruthy(); - var message = 'This message is sent to a minimized chatbox'; - var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - var msg = $msg({ - from: sender_jid, + await u.waitUntil(() => chatview.model.messages.length); + expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); + const trimmed_chatboxes = _converse.minimized_chats; + const trimmedview = trimmed_chatboxes.get(contact_jid); + let count = trimmedview.el.querySelector('.message-count'); + expect(u.isVisible(chatview.el)).toBeFalsy(); + expect(trimmedview.model.get('minimized')).toBeTruthy(); + expect(u.isVisible(count)).toBeTruthy(); + expect(count.textContent).toBe('1'); + _converse.handleMessageStanza( + $msg({ + from: mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', to: _converse.connection.jid, type: 'chat', id: u.getUniqueId() - }).c('body').t(message).up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); - await _converse.handleMessageStanza(msg); + }).c('body').t('This message is also sent to a minimized chatbox').up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree() + ); - await u.waitUntil(() => chatview.model.messages.length); - expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); - const trimmed_chatboxes = _converse.minimized_chats; - const trimmedview = trimmed_chatboxes.get(contact_jid); - let count = trimmedview.el.querySelector('.message-count'); - expect(u.isVisible(chatview.el)).toBeFalsy(); - expect(trimmedview.model.get('minimized')).toBeTruthy(); - expect(u.isVisible(count)).toBeTruthy(); - expect(count.textContent).toBe('1'); - _converse.handleMessageStanza( - $msg({ - from: mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t('This message is also sent to a minimized chatbox').up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree() - ); + await u.waitUntil(() => (chatview.model.messages.length > 1)); + expect(u.isVisible(chatview.el)).toBeFalsy(); + expect(trimmedview.model.get('minimized')).toBeTruthy(); + count = trimmedview.el.querySelector('.message-count'); + expect(u.isVisible(count)).toBeTruthy(); + expect(count.textContent).toBe('2'); + trimmedview.el.querySelector('.restore-chat').click(); + expect(trimmed_chatboxes.keys().length).toBe(0); + done(); + })); - await u.waitUntil(() => (chatview.model.messages.length > 1)); - expect(u.isVisible(chatview.el)).toBeFalsy(); - expect(trimmedview.model.get('minimized')).toBeTruthy(); - count = trimmedview.el.querySelector('.message-count'); - expect(u.isVisible(count)).toBeTruthy(); - expect(count.textContent).toBe('2'); - trimmedview.el.querySelector('.restore-chat').click(); - expect(trimmed_chatboxes.keys().length).toBe(0); - done(); - })); + it("will indicate when it has a time difference of more than a day between it and its predecessor", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - it("will indicate when it has a time difference of more than a day between it and its predecessor", + const include_nick = false; + await mock.waitForRoster(_converse, 'current', 2, include_nick); + await mock.openControlBox(_converse); + spyOn(_converse.api, "trigger").and.callThrough(); + const contact_name = mock.cur_names[1]; + const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); + await mock.openChatBoxFor(_converse, contact_jid); + await mock.clearChatBoxMessages(_converse, contact_jid); + const one_day_ago = dayjs().subtract(1, 'day'); + const chatbox = _converse.chatboxes.get(contact_jid); + const view = _converse.chatboxviews.get(contact_jid); + + let message = 'This is a day old message'; + let msg = $msg({ + from: contact_jid, + to: _converse.connection.jid, + type: 'chat', + id: one_day_ago.toDate().getTime() + }).c('body').t(message).up() + .c('delay', { xmlns:'urn:xmpp:delay', from: 'montague.lit', stamp: one_day_ago.toISOString() }) + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + await _converse.handleMessageStanza(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + + expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); + expect(chatbox.messages.length).toEqual(1); + let msg_obj = chatbox.messages.models[0]; + expect(msg_obj.get('message')).toEqual(message); + expect(msg_obj.get('fullname')).toBeUndefined(); + expect(msg_obj.get('nickname')).toBe(null); + expect(msg_obj.get('sender')).toEqual('them'); + expect(msg_obj.get('is_delayed')).toEqual(true); + await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet') + expect(view.msgs_container.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message); + expect(view.msgs_container.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); + expect(view.msgs_container.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet'); + + expect(view.msgs_container.querySelectorAll('.date-separator').length).toEqual(1); + let day = view.msgs_container.querySelector('.date-separator'); + expect(day.getAttribute('class')).toEqual('message date-separator'); + expect(day.getAttribute('data-isodate')).toEqual(dayjs(one_day_ago.startOf('day')).toISOString()); + + let time = view.msgs_container.querySelector('time.separator-text'); + expect(time.textContent).toEqual(dayjs(one_day_ago.startOf('day')).format("dddd MMM Do YYYY")); + + message = 'This is a current message'; + msg = $msg({ + from: contact_jid, + to: _converse.connection.jid, + type: 'chat', + id: new Date().getTime() + }).c('body').t(message).up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + await _converse.handleMessageStanza(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + + expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); + // Check that there is a element, with the required props. + expect(view.msgs_container.querySelectorAll('time.separator-text').length).toEqual(2); // There are now two time elements + + const message_date = new Date(); + day = sizzle('.date-separator:last', view.msgs_container); + expect(day.length).toEqual(1); + expect(day[0].getAttribute('class')).toEqual('message date-separator'); + expect(day[0].getAttribute('data-isodate')).toEqual(dayjs(message_date).startOf('day').toISOString()); + + time = sizzle('time.separator-text:last', view.msgs_container).pop(); + expect(time.textContent).toEqual(dayjs(message_date).startOf('day').format("dddd MMM Do YYYY")); + + // Normal checks for the 2nd message + expect(chatbox.messages.length).toEqual(2); + msg_obj = chatbox.messages.models[1]; + expect(msg_obj.get('message')).toEqual(message); + expect(msg_obj.get('fullname')).toBeUndefined(); + expect(msg_obj.get('sender')).toEqual('them'); + expect(msg_obj.get('is_delayed')).toEqual(false); + const msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.msgs_container).pop().textContent; + expect(msg_txt).toEqual(message); + + expect(view.msgs_container.querySelector('.chat-msg:last-child .chat-msg__text').textContent).toEqual(message); + expect(view.msgs_container.querySelector('.chat-msg:last-child .chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); + expect(view.msgs_container.querySelector('.chat-msg:last-child .chat-msg__author').textContent.trim()).toBe('Juliet Capulet'); + done(); + })); + + it("is sanitized to prevent Javascript injection attacks", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid) + const view = _converse.api.chatviews.get(contact_jid); + const message = 'This message contains some markup'; + spyOn(view.model, 'sendMessage').and.callThrough(); + await mock.sendMessage(view, message); + expect(view.model.sendMessage).toHaveBeenCalled(); + const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); + expect(msg.textContent).toEqual(message); + expect(msg.innerHTML).toEqual('<p>This message contains <em>some</em> <b>markup</b></p>'); + done(); + })); + + it("can contain hyperlinks, which will be clickable", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid) + const view = _converse.api.chatviews.get(contact_jid); + const message = 'This message contains a hyperlink: www.opkode.com'; + spyOn(view.model, 'sendMessage').and.callThrough(); + mock.sendMessage(view, message); + expect(view.model.sendMessage).toHaveBeenCalled(); + await new Promise(resolve => view.once('messageInserted', resolve)); + const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); + expect(msg.textContent).toEqual(message); + expect(msg.innerHTML) + .toEqual('This message contains a hyperlink: www.opkode.com'); + done(); + })); + + it("will render newlines", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const view = await mock.openChatBoxFor(_converse, contact_jid); + let stanza = u.toStanza(` + + Hey\nHave you heard the news? + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.content.querySelector('.chat-msg__text').innerHTML).toBe('HeyHave you heard the news?'); + stanza = u.toStanza(` + + Hey\n\n\nHave you heard the news? + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('HeyHave you heard the news?'); + stanza = u.toStanza(` + + Hey\nHave you heard\nthe news? + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('HeyHave you heardthe news?'); + done(); + })); + + it("will render images from their URLs", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + const base_url = 'https://conversejs.org'; + let message = base_url+"/logo/conversejs-filled.svg"; + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.api.chatviews.get(contact_jid); + spyOn(view.model, 'sendMessage').and.callThrough(); + mock.sendMessage(view, message); + await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length, 1000) + expect(view.model.sendMessage).toHaveBeenCalled(); + let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop(); + expect(msg.innerHTML.trim()).toEqual( + ``); + message += "?param1=val1¶m2=val2"; + mock.sendMessage(view, message); + await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 2, 1000); + expect(view.model.sendMessage).toHaveBeenCalled(); + msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop(); + expect(msg.innerHTML.trim()).toEqual( + '') + + // Test now with two images in one message + message += ' hello world '+base_url+"/logo/conversejs-filled.svg"; + mock.sendMessage(view, message); + await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 4, 1000); + expect(view.model.sendMessage).toHaveBeenCalled(); + msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop(); + expect(msg.textContent.trim()).toEqual('hello world'); + expect(msg.querySelectorAll('img').length).toEqual(2); + + // Non-https images aren't rendered + message = base_url+"/logo/conversejs-filled.svg"; + expect(view.content.querySelectorAll('img').length).toBe(4); + mock.sendMessage(view, message); + expect(view.content.querySelectorAll('img').length).toBe(4); + done(); + })); + + it("will render the message time as configured", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { - const include_nick = false; - await test_utils.waitForRoster(_converse, 'current', 2, include_nick); - await test_utils.openControlBox(_converse); - spyOn(_converse.api, "trigger").and.callThrough(); - const contact_name = mock.cur_names[1]; - const contact_jid = contact_name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.waitForRoster(_converse, 'current'); + _converse.time_format = 'hh:mm'; + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid) + const view = _converse.api.chatviews.get(contact_jid); + const message = 'This message is sent from this chatbox'; + await mock.sendMessage(view, message); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length); - await test_utils.openChatBoxFor(_converse, contact_jid); - await test_utils.clearChatBoxMessages(_converse, contact_jid); - const one_day_ago = dayjs().subtract(1, 'day'); - const chatbox = _converse.chatboxes.get(contact_jid); + const chatbox = await _converse.api.chats.get(contact_jid); + expect(chatbox.messages.models.length, 1); + const msg_object = chatbox.messages.models[0]; + + const msg_author = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__author'); + expect(msg_author.textContent.trim()).toBe('Romeo Montague'); + + const msg_time = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__time'); + const time = dayjs(msg_object.get('time')).format(_converse.time_format); + expect(msg_time.textContent).toBe(time); + done(); + })); + + it("will be correctly identified and rendered as a followup message", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + const base_time = new Date(); + const ONE_MINUTE_LATER = 60000; + + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + _converse.filter_by_resource = true; + + jasmine.clock().install(); + jasmine.clock().mockDate(base_time); + + _converse.handleMessageStanza($msg({ + 'from': sender_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': u.getUniqueId() + }).c('body').t('A message').up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve)); + const view = _converse.api.chatviews.get(sender_jid); + await new Promise(resolve => view.once('messageInserted', resolve)); + + jasmine.clock().tick(3*ONE_MINUTE_LATER); + _converse.handleMessageStanza($msg({ + 'from': sender_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': u.getUniqueId() + }).c('body').t("Another message 3 minutes later").up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + await new Promise(resolve => view.once('messageInserted', resolve)); + + jasmine.clock().tick(11*ONE_MINUTE_LATER); + _converse.handleMessageStanza($msg({ + 'from': sender_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': u.getUniqueId() + }).c('body').t("Another message 14 minutes since we started").up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + await new Promise(resolve => view.once('messageInserted', resolve)); + + jasmine.clock().tick(1*ONE_MINUTE_LATER); + + _converse.handleMessageStanza($msg({ + 'from': sender_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': _converse.connection.getUniqueId() + }).c('body').t("Another message 1 minute and 1 second since the previous one").up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + await new Promise(resolve => view.once('messageInserted', resolve)); + + jasmine.clock().tick(1*ONE_MINUTE_LATER); + await mock.sendMessage(view, "Another message within 10 minutes, but from a different person"); + + expect(view.content.querySelectorAll('.message').length).toBe(6); + expect(view.content.querySelectorAll('.chat-msg').length).toBe(5); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(2)'))).toBe(false); + expect(view.content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(3)'))).toBe(true); + expect(view.content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe( + "Another message 3 minutes later"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(4)'))).toBe(false); + expect(view.content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe( + "Another message 14 minutes since we started"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(5)'))).toBe(true); + expect(view.content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe( + "Another message 1 minute and 1 second since the previous one"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(6)'))).toBe(false); + expect(view.content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe( + "Another message within 10 minutes, but from a different person"); + + // Let's add a delayed, inbetween message + _converse.handleMessageStanza( + $msg({ + 'xmlns': 'jabber:client', + 'id': _converse.connection.getUniqueId(), + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat' + }).c('body').t("A delayed message, sent 5 minutes since we started").up() + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp': dayjs(base_time).add(5, 'minutes').toISOString()}) + .tree()); + await new Promise(resolve => view.once('messageInserted', resolve)); + + expect(view.content.querySelectorAll('.message').length).toBe(7); + expect(view.content.querySelectorAll('.chat-msg').length).toBe(6); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(2)'))).toBe(false); + expect(view.content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(3)'))).toBe(true); + expect(view.content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe( + "Another message 3 minutes later"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(4)'))).toBe(true); + expect(view.content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe( + "A delayed message, sent 5 minutes since we started"); + + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(5)'))).toBe(false); + expect(view.content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe( + "Another message 14 minutes since we started"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(6)'))).toBe(true); + expect(view.content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe( + "Another message 1 minute and 1 second since the previous one"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(7)'))).toBe(false); + + _converse.handleMessageStanza( + $msg({ + 'xmlns': 'jabber:client', + 'id': _converse.connection.getUniqueId(), + 'to': sender_jid, + 'from': _converse.bare_jid+"/some-other-resource", + 'type': 'chat'}) + .c('body').t("A carbon message 4 minutes later").up() + .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':dayjs(base_time).add(4, 'minutes').toISOString()}) + .tree()); + await new Promise(resolve => view.once('messageInserted', resolve)); + + expect(view.content.querySelectorAll('.message').length).toBe(8); + expect(view.content.querySelectorAll('.chat-msg').length).toBe(7); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(2)'))).toBe(false); + expect(view.content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(3)'))).toBe(true); + expect(view.content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe( + "Another message 3 minutes later"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(4)'))).toBe(false); + expect(view.content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe( + "A carbon message 4 minutes later"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(5)'))).toBe(false); + expect(view.content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe( + "A delayed message, sent 5 minutes since we started"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(6)'))).toBe(false); + expect(view.content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe( + "Another message 14 minutes since we started"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(7)'))).toBe(true); + expect(view.content.querySelector('.message:nth-child(7) .chat-msg__text').textContent).toBe( + "Another message 1 minute and 1 second since the previous one"); + expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(8)'))).toBe(false); + expect(view.content.querySelector('.message:nth-child(8) .chat-msg__text').textContent).toBe( + "Another message within 10 minutes, but from a different person"); + + jasmine.clock().uninstall(); + done(); + })); + + it("received may emit a message delivery receipt", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const msg_id = u.getUniqueId(); + const sent_stanzas = []; + spyOn(_converse.connection, 'send').and.callFake(stanza => sent_stanzas.push(stanza)); + const msg = $msg({ + 'from': sender_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': msg_id, + }).c('body').t('Message!').up() + .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree(); + await _converse.handleMessageStanza(msg); + const sent_messages = sent_stanzas.map(s => _.isElement(s) ? s : s.nodeTree).filter(s => s.nodeName === 'message'); + // A chat state message is also included + expect(sent_messages.length).toBe(2); + const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, sent_messages[1]).pop(); + expect(Strophe.serialize(receipt)).toBe(``); + done(); + })); + + it("carbon received does not emit a message delivery receipt", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + await mock.waitForRoster(_converse, 'current', 1); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const msg_id = u.getUniqueId(); + const view = await mock.openChatBoxFor(_converse, sender_jid); + spyOn(view.model, 'sendReceiptStanza').and.callThrough(); + const msg = $msg({ + 'from': sender_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': u.getUniqueId(), + }).c('received', {'xmlns': 'urn:xmpp:carbons:2'}) + .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) + .c('message', { + 'xmlns': 'jabber:client', + 'from': sender_jid, + 'to': _converse.bare_jid+'/another-resource', + 'type': 'chat', + 'id': msg_id + }).c('body').t('Message!').up() + .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree(); + await _converse.handleMessageStanza(msg); + expect(view.model.sendReceiptStanza).not.toHaveBeenCalled(); + done(); + })); + + describe("when sent", function () { + + it("can have its delivery acknowledged by a receipt", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.chatboxviews.get(contact_jid); - - let message = 'This is a day old message'; - let msg = $msg({ - from: contact_jid, - to: _converse.connection.jid, - type: 'chat', - id: one_day_ago.toDate().getTime() - }).c('body').t(message).up() - .c('delay', { xmlns:'urn:xmpp:delay', from: 'montague.lit', stamp: one_day_ago.toISOString() }) - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); - await _converse.handleMessageStanza(msg); + const textarea = view.el.querySelector('textarea.chat-textarea'); + textarea.value = 'But soft, what light through yonder airlock breaks?'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + const chatbox = _converse.chatboxes.get(contact_jid); + expect(chatbox).toBeDefined(); await new Promise(resolve => view.once('messageInserted', resolve)); - - expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); - expect(chatbox.messages.length).toEqual(1); let msg_obj = chatbox.messages.models[0]; - expect(msg_obj.get('message')).toEqual(message); - expect(msg_obj.get('fullname')).toBeUndefined(); - expect(msg_obj.get('nickname')).toBe(null); - expect(msg_obj.get('sender')).toEqual('them'); - expect(msg_obj.get('is_delayed')).toEqual(true); - await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet') - expect(view.msgs_container.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message); - expect(view.msgs_container.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); - expect(view.msgs_container.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet'); + let msg_id = msg_obj.get('msgid'); + let msg = $msg({ + 'from': contact_jid, + 'to': _converse.connection.jid, + 'id': u.getUniqueId(), + }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); + expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(1); - expect(view.msgs_container.querySelectorAll('.date-separator').length).toEqual(1); - let day = view.msgs_container.querySelector('.date-separator'); - expect(day.getAttribute('class')).toEqual('message date-separator'); - expect(day.getAttribute('data-isodate')).toEqual(dayjs(one_day_ago.startOf('day')).toISOString()); - - let time = view.msgs_container.querySelector('time.separator-text'); - expect(time.textContent).toEqual(dayjs(one_day_ago.startOf('day')).format("dddd MMM Do YYYY")); - - message = 'This is a current message'; - msg = $msg({ - from: contact_jid, - to: _converse.connection.jid, - type: 'chat', - id: new Date().getTime() - }).c('body').t(message).up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); - await _converse.handleMessageStanza(msg); + // Also handle receipts with type 'chat'. See #1353 + spyOn(_converse, 'handleMessageStanza').and.callThrough(); + textarea.value = 'Another message'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); await new Promise(resolve => view.once('messageInserted', resolve)); - expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); - // Check that there is a element, with the required props. - expect(view.msgs_container.querySelectorAll('time.separator-text').length).toEqual(2); // There are now two time elements - - const message_date = new Date(); - day = sizzle('.date-separator:last', view.msgs_container); - expect(day.length).toEqual(1); - expect(day[0].getAttribute('class')).toEqual('message date-separator'); - expect(day[0].getAttribute('data-isodate')).toEqual(dayjs(message_date).startOf('day').toISOString()); - - time = sizzle('time.separator-text:last', view.msgs_container).pop(); - expect(time.textContent).toEqual(dayjs(message_date).startOf('day').format("dddd MMM Do YYYY")); - - // Normal checks for the 2nd message - expect(chatbox.messages.length).toEqual(2); msg_obj = chatbox.messages.models[1]; - expect(msg_obj.get('message')).toEqual(message); - expect(msg_obj.get('fullname')).toBeUndefined(); - expect(msg_obj.get('sender')).toEqual('them'); - expect(msg_obj.get('is_delayed')).toEqual(false); - const msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.msgs_container).pop().textContent; - expect(msg_txt).toEqual(message); - - expect(view.msgs_container.querySelector('.chat-msg:last-child .chat-msg__text').textContent).toEqual(message); - expect(view.msgs_container.querySelector('.chat-msg:last-child .chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); - expect(view.msgs_container.querySelector('.chat-msg:last-child .chat-msg__author').textContent.trim()).toBe('Juliet Capulet'); + msg_id = msg_obj.get('msgid'); + msg = $msg({ + 'from': contact_jid, + 'type': 'chat', + 'to': _converse.connection.jid, + 'id': u.getUniqueId(), + }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); + expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(2); + expect(_converse.handleMessageStanza.calls.count()).toBe(1); done(); })); - it("is sanitized to prevent Javascript injection attacks", + + it("will appear inside the chatbox it was sent from", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + spyOn(_converse.api, "trigger").and.callThrough(); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid) - const view = _converse.api.chatviews.get(contact_jid); - const message = 'This message contains some markup'; - spyOn(view.model, 'sendMessage').and.callThrough(); - await test_utils.sendMessage(view, message); - expect(view.model.sendMessage).toHaveBeenCalled(); - const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); - expect(msg.textContent).toEqual(message); - expect(msg.innerHTML).toEqual('<p>This message contains <em>some</em> <b>markup</b></p>'); - done(); - })); - - it("can contain hyperlinks, which will be clickable", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid) - const view = _converse.api.chatviews.get(contact_jid); - const message = 'This message contains a hyperlink: www.opkode.com'; - spyOn(view.model, 'sendMessage').and.callThrough(); - test_utils.sendMessage(view, message); - expect(view.model.sendMessage).toHaveBeenCalled(); - await new Promise(resolve => view.once('messageInserted', resolve)); - const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); - expect(msg.textContent).toEqual(message); - expect(msg.innerHTML) - .toEqual('This message contains a hyperlink: www.opkode.com'); - done(); - })); - - it("will render newlines", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const view = await test_utils.openChatBoxFor(_converse, contact_jid); - let stanza = u.toStanza(` - - Hey\nHave you heard the news? - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.content.querySelector('.chat-msg__text').innerHTML).toBe('HeyHave you heard the news?'); - stanza = u.toStanza(` - - Hey\n\n\nHave you heard the news? - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('HeyHave you heard the news?'); - stanza = u.toStanza(` - - Hey\nHave you heard\nthe news? - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.content.querySelector('.message:last-child .chat-msg__text').innerHTML).toBe('HeyHave you heardthe news?'); - done(); - })); - - it("will render images from their URLs", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - const base_url = 'https://conversejs.org'; - let message = base_url+"/logo/conversejs-filled.svg"; - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.api.chatviews.get(contact_jid); - spyOn(view.model, 'sendMessage').and.callThrough(); - test_utils.sendMessage(view, message); - await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length, 1000) - expect(view.model.sendMessage).toHaveBeenCalled(); - let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop(); - expect(msg.innerHTML.trim()).toEqual( - ``); - message += "?param1=val1¶m2=val2"; - test_utils.sendMessage(view, message); - await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 2, 1000); - expect(view.model.sendMessage).toHaveBeenCalled(); - msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop(); - expect(msg.innerHTML.trim()).toEqual( - '') - - // Test now with two images in one message - message += ' hello world '+base_url+"/logo/conversejs-filled.svg"; - test_utils.sendMessage(view, message); - await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-image').length === 4, 1000); - expect(view.model.sendMessage).toHaveBeenCalled(); - msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop(); - expect(msg.textContent.trim()).toEqual('hello world'); - expect(msg.querySelectorAll('img').length).toEqual(2); - - // Non-https images aren't rendered - message = base_url+"/logo/conversejs-filled.svg"; - expect(view.content.querySelectorAll('img').length).toBe(4); - test_utils.sendMessage(view, message); - expect(view.content.querySelectorAll('img').length).toBe(4); - done(); - })); - - it("will render the message time as configured", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - _converse.time_format = 'hh:mm'; - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid) - const view = _converse.api.chatviews.get(contact_jid); + await mock.openChatBoxFor(_converse, contact_jid) + const view = _converse.chatboxviews.get(contact_jid); const message = 'This message is sent from this chatbox'; - await test_utils.sendMessage(view, message); - - const chatbox = await _converse.api.chats.get(contact_jid); - expect(chatbox.messages.models.length, 1); - const msg_object = chatbox.messages.models[0]; - - const msg_author = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__author'); - expect(msg_author.textContent.trim()).toBe('Romeo Montague'); - - const msg_time = view.el.querySelector('.chat-content .chat-msg:last-child .chat-msg__time'); - const time = dayjs(msg_object.get('time')).format(_converse.time_format); - expect(msg_time.textContent).toBe(time); + spyOn(view.model, 'sendMessage').and.callThrough(); + await mock.sendMessage(view, message); + expect(view.model.sendMessage).toHaveBeenCalled(); + expect(view.model.messages.length, 2); + expect(_converse.api.trigger.calls.mostRecent().args, ['messageSend', message]); + expect(sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop().textContent).toEqual(message); done(); })); - it("will be correctly identified and rendered as a followup message", + + it("will be trimmed of leading and trailing whitespace", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid) + const view = _converse.chatboxviews.get(contact_jid); + const message = ' \nThis message is sent from this chatbox \n \n'; + await mock.sendMessage(view, message); + expect(view.model.messages.at(0).get('message')).toEqual(message.trim()); + const message_el = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); + expect(message_el.textContent).toEqual(message.trim()); + done(); + })); + }); + + + describe("when received from someone else", function () { + + it("will open a chatbox and be displayed inside it", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - const base_time = new Date(); - const ONE_MINUTE_LATER = 60000; - + const include_nick = false; + await mock.waitForRoster(_converse, 'current', 1, include_nick); + await mock.openControlBox(_converse); await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300); + spyOn(_converse.api, "trigger").and.callThrough(); + const message = 'This is a received message'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - _converse.filter_by_resource = true; - - jasmine.clock().install(); - jasmine.clock().mockDate(base_time); - - _converse.handleMessageStanza($msg({ + // We don't already have an open chatbox for this user + expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined(); + await _converse.handleMessageStanza( + $msg({ 'from': sender_jid, 'to': _converse.connection.jid, 'type': 'chat', 'id': u.getUniqueId() - }).c('body').t('A message').up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); - await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve)); + }).c('body').t(message).up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree() + ); + const chatbox = await _converse.chatboxes.get(sender_jid); + expect(chatbox).toBeDefined(); const view = _converse.api.chatviews.get(sender_jid); - await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view).toBeDefined(); - jasmine.clock().tick(3*ONE_MINUTE_LATER); - _converse.handleMessageStanza($msg({ - 'from': sender_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': u.getUniqueId() - }).c('body').t("Another message 3 minutes later").up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); - await new Promise(resolve => view.once('messageInserted', resolve)); - - jasmine.clock().tick(11*ONE_MINUTE_LATER); - _converse.handleMessageStanza($msg({ - 'from': sender_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': u.getUniqueId() - }).c('body').t("Another message 14 minutes since we started").up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); - await new Promise(resolve => view.once('messageInserted', resolve)); - - jasmine.clock().tick(1*ONE_MINUTE_LATER); - - _converse.handleMessageStanza($msg({ - 'from': sender_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': _converse.connection.getUniqueId() - }).c('body').t("Another message 1 minute and 1 second since the previous one").up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); - await new Promise(resolve => view.once('messageInserted', resolve)); - - jasmine.clock().tick(1*ONE_MINUTE_LATER); - await test_utils.sendMessage(view, "Another message within 10 minutes, but from a different person"); - - expect(view.content.querySelectorAll('.message').length).toBe(6); - expect(view.content.querySelectorAll('.chat-msg').length).toBe(5); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(2)'))).toBe(false); - expect(view.content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(3)'))).toBe(true); - expect(view.content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe( - "Another message 3 minutes later"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(4)'))).toBe(false); - expect(view.content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe( - "Another message 14 minutes since we started"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(5)'))).toBe(true); - expect(view.content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe( - "Another message 1 minute and 1 second since the previous one"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(6)'))).toBe(false); - expect(view.content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe( - "Another message within 10 minutes, but from a different person"); - - // Let's add a delayed, inbetween message - _converse.handleMessageStanza( - $msg({ - 'xmlns': 'jabber:client', - 'id': _converse.connection.getUniqueId(), - 'to': _converse.bare_jid, - 'from': sender_jid, - 'type': 'chat' - }).c('body').t("A delayed message, sent 5 minutes since we started").up() - .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp': dayjs(base_time).add(5, 'minutes').toISOString()}) - .tree()); - await new Promise(resolve => view.once('messageInserted', resolve)); - - expect(view.content.querySelectorAll('.message').length).toBe(7); - expect(view.content.querySelectorAll('.chat-msg').length).toBe(6); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(2)'))).toBe(false); - expect(view.content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(3)'))).toBe(true); - expect(view.content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe( - "Another message 3 minutes later"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(4)'))).toBe(true); - expect(view.content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe( - "A delayed message, sent 5 minutes since we started"); - - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(5)'))).toBe(false); - expect(view.content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe( - "Another message 14 minutes since we started"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(6)'))).toBe(true); - expect(view.content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe( - "Another message 1 minute and 1 second since the previous one"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(7)'))).toBe(false); - - _converse.handleMessageStanza( - $msg({ - 'xmlns': 'jabber:client', - 'id': _converse.connection.getUniqueId(), - 'to': sender_jid, - 'from': _converse.bare_jid+"/some-other-resource", - 'type': 'chat'}) - .c('body').t("A carbon message 4 minutes later").up() - .c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':dayjs(base_time).add(4, 'minutes').toISOString()}) - .tree()); - await new Promise(resolve => view.once('messageInserted', resolve)); - - expect(view.content.querySelectorAll('.message').length).toBe(8); - expect(view.content.querySelectorAll('.chat-msg').length).toBe(7); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(2)'))).toBe(false); - expect(view.content.querySelector('.message:nth-child(2) .chat-msg__text').textContent).toBe("A message"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(3)'))).toBe(true); - expect(view.content.querySelector('.message:nth-child(3) .chat-msg__text').textContent).toBe( - "Another message 3 minutes later"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(4)'))).toBe(false); - expect(view.content.querySelector('.message:nth-child(4) .chat-msg__text').textContent).toBe( - "A carbon message 4 minutes later"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(5)'))).toBe(false); - expect(view.content.querySelector('.message:nth-child(5) .chat-msg__text').textContent).toBe( - "A delayed message, sent 5 minutes since we started"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(6)'))).toBe(false); - expect(view.content.querySelector('.message:nth-child(6) .chat-msg__text').textContent).toBe( - "Another message 14 minutes since we started"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(7)'))).toBe(true); - expect(view.content.querySelector('.message:nth-child(7) .chat-msg__text').textContent).toBe( - "Another message 1 minute and 1 second since the previous one"); - expect(u.hasClass('chat-msg--followup', view.content.querySelector('.message:nth-child(8)'))).toBe(false); - expect(view.content.querySelector('.message:nth-child(8) .chat-msg__text').textContent).toBe( - "Another message within 10 minutes, but from a different person"); - - jasmine.clock().uninstall(); + expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); + // Check that the message was received and check the message parameters + await u.waitUntil(() => chatbox.messages.length); + expect(chatbox.messages.length).toEqual(1); + const msg_obj = chatbox.messages.models[0]; + expect(msg_obj.get('message')).toEqual(message); + expect(msg_obj.get('fullname')).toBeUndefined(); + expect(msg_obj.get('sender')).toEqual('them'); + expect(msg_obj.get('is_delayed')).toEqual(false); + // Now check that the message appears inside the chatbox in the DOM + const mel = await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text')); + expect(mel.textContent).toEqual(message); + expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); + await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]); + expect(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio'); done(); })); - it("received may emit a message delivery receipt", + it("will be trimmed of leading and trailing whitespace", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1, false); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300); + const message = '\n\n This is a received message \n\n'; + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await _converse.handleMessageStanza( + $msg({ + 'from': sender_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': u.getUniqueId() + }).c('body').t(message).up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree() + ); + const view = _converse.api.chatviews.get(sender_jid); + await u.waitUntil(() => view.model.messages.length); + expect(view.model.messages.length).toEqual(1); + const msg_obj = view.model.messages.at(0); + expect(msg_obj.get('message')).toEqual(message.trim()); + const mel = await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text')); + expect(mel.textContent).toEqual(message.trim()); + done(); + })); + + + it("can be replaced with a correction", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); + await mock.waitForRoster(_converse, 'current', 1); + await mock.openControlBox(_converse); const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; const msg_id = u.getUniqueId(); - const sent_stanzas = []; - spyOn(_converse.connection, 'send').and.callFake(stanza => sent_stanzas.push(stanza)); - const msg = $msg({ + const view = await mock.openChatBoxFor(_converse, sender_jid); + _converse.handleMessageStanza($msg({ 'from': sender_jid, 'to': _converse.connection.jid, 'type': 'chat', 'id': msg_id, - }).c('body').t('Message!').up() - .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree(); - await _converse.handleMessageStanza(msg); - const sent_messages = sent_stanzas.map(s => _.isElement(s) ? s : s.nodeTree).filter(s => s.nodeName === 'message'); - // A chat state message is also included - expect(sent_messages.length).toBe(2); - const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, sent_messages[1]).pop(); - expect(Strophe.serialize(receipt)).toBe(``); - done(); - })); + }).c('body').t('But soft, what light through yonder airlock breaks?').tree()); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelector('.chat-msg__text').textContent) + .toBe('But soft, what light through yonder airlock breaks?'); - it("carbon received does not emit a message delivery receipt", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 1); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const msg_id = u.getUniqueId(); - const view = await test_utils.openChatBoxFor(_converse, sender_jid); - spyOn(view.model, 'sendReceiptStanza').and.callThrough(); - const msg = $msg({ + _converse.handleMessageStanza($msg({ 'from': sender_jid, 'to': _converse.connection.jid, 'type': 'chat', 'id': u.getUniqueId(), - }).c('received', {'xmlns': 'urn:xmpp:carbons:2'}) - .c('forwarded', {'xmlns': 'urn:xmpp:forward:0'}) - .c('message', { - 'xmlns': 'jabber:client', - 'from': sender_jid, - 'to': _converse.bare_jid+'/another-resource', - 'type': 'chat', - 'id': msg_id - }).c('body').t('Message!').up() - .c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree(); - await _converse.handleMessageStanza(msg); - expect(view.model.sendReceiptStanza).not.toHaveBeenCalled(); + }).c('body').t('But soft, what light through yonder chimney breaks?').up() + .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree()); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); + + expect(view.el.querySelector('.chat-msg__text').textContent) + .toBe('But soft, what light through yonder chimney breaks?'); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1); + expect(view.model.messages.models.length).toBe(1); + + _converse.handleMessageStanza($msg({ + 'from': sender_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': u.getUniqueId(), + }).c('body').t('But soft, what light through yonder window breaks?').up() + .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree()); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); + + expect(view.el.querySelector('.chat-msg__text').textContent) + .toBe('But soft, what light through yonder window breaks?'); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1); + view.el.querySelector('.chat-msg__content .fa-edit').click(); + const modal = view.model.messages.at(0).message_versions_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + const older_msgs = modal.el.querySelectorAll('.older-msg'); + expect(older_msgs.length).toBe(2); + expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME'); + expect(older_msgs[0].childNodes[2].textContent).toBe('But soft, what light through yonder airlock breaks?'); + expect(view.model.messages.models.length).toBe(1); done(); })); - describe("when sent", function () { - it("can have its delivery acknowledged by a receipt", + describe("when a chatbox is opened for someone who is not in the roster", function () { + + it("the VCard for that user is fetched and the chatbox updated with the results", mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + ['rosterGroupsFetched'], {'allow_non_roster_messaging': true}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.chatboxviews.get(contact_jid); - const textarea = view.el.querySelector('textarea.chat-textarea'); - textarea.value = 'But soft, what light through yonder airlock breaks?'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - const chatbox = _converse.chatboxes.get(contact_jid); - expect(chatbox).toBeDefined(); - await new Promise(resolve => view.once('messageInserted', resolve)); - let msg_obj = chatbox.messages.models[0]; - let msg_id = msg_obj.get('msgid'); - let msg = $msg({ - 'from': contact_jid, - 'to': _converse.connection.jid, - 'id': u.getUniqueId(), - }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); - expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(1); - - // Also handle receipts with type 'chat'. See #1353 - spyOn(_converse, 'handleMessageStanza').and.callThrough(); - textarea.value = 'Another message'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - await new Promise(resolve => view.once('messageInserted', resolve)); - - msg_obj = chatbox.messages.models[1]; - msg_id = msg_obj.get('msgid'); - msg = $msg({ - 'from': contact_jid, - 'type': 'chat', - 'to': _converse.connection.jid, - 'id': u.getUniqueId(), - }).c('received', {'id': msg_id, xmlns: Strophe.NS.RECEIPTS}).up().tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); - expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(2); - expect(_converse.handleMessageStanza.calls.count()).toBe(1); - done(); - })); - - - it("will appear inside the chatbox it was sent from", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 0); spyOn(_converse.api, "trigger").and.callThrough(); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid) - const view = _converse.chatboxviews.get(contact_jid); - const message = 'This message is sent from this chatbox'; - spyOn(view.model, 'sendMessage').and.callThrough(); - await test_utils.sendMessage(view, message); - expect(view.model.sendMessage).toHaveBeenCalled(); - expect(view.model.messages.length, 2); - expect(_converse.api.trigger.calls.mostRecent().args, ['messageSend', message]); - expect(sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop().textContent).toEqual(message); - done(); - })); - - it("will be trimmed of leading and trailing whitespace", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid) - const view = _converse.chatboxviews.get(contact_jid); - const message = ' \nThis message is sent from this chatbox \n \n'; - await test_utils.sendMessage(view, message); - expect(view.model.messages.at(0).get('message')).toEqual(message.trim()); - const message_el = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); - expect(message_el.textContent).toEqual(message.trim()); - done(); - })); - }); - - - describe("when received from someone else", function () { - - it("will open a chatbox and be displayed inside it", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const include_nick = false; - await test_utils.waitForRoster(_converse, 'current', 1, include_nick); - await test_utils.openControlBox(_converse); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300); - spyOn(_converse.api, "trigger").and.callThrough(); - const message = 'This is a received message'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - // We don't already have an open chatbox for this user - expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined(); - await _converse.handleMessageStanza( - $msg({ - 'from': sender_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': u.getUniqueId() - }).c('body').t(message).up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree() - ); - const chatbox = await _converse.chatboxes.get(sender_jid); - expect(chatbox).toBeDefined(); - const view = _converse.api.chatviews.get(sender_jid); - expect(view).toBeDefined(); - - expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); - // Check that the message was received and check the message parameters - await u.waitUntil(() => chatbox.messages.length); - expect(chatbox.messages.length).toEqual(1); - const msg_obj = chatbox.messages.models[0]; - expect(msg_obj.get('message')).toEqual(message); - expect(msg_obj.get('fullname')).toBeUndefined(); - expect(msg_obj.get('sender')).toEqual('them'); - expect(msg_obj.get('is_delayed')).toEqual(false); - // Now check that the message appears inside the chatbox in the DOM - const mel = await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text')); - expect(mel.textContent).toEqual(message); - expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); - await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]); - expect(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio'); - done(); - })); - - it("will be trimmed of leading and trailing whitespace", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1, false); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300); - const message = '\n\n This is a received message \n\n'; - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await _converse.handleMessageStanza( - $msg({ - 'from': sender_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': u.getUniqueId() - }).c('body').t(message).up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree() - ); - const view = _converse.api.chatviews.get(sender_jid); - await u.waitUntil(() => view.model.messages.length); - expect(view.model.messages.length).toEqual(1); - const msg_obj = view.model.messages.at(0); - expect(msg_obj.get('message')).toEqual(message.trim()); - const mel = await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text')); - expect(mel.textContent).toEqual(message.trim()); - done(); - })); - - - it("can be replaced with a correction", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - await test_utils.openControlBox(_converse); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const msg_id = u.getUniqueId(); - const view = await test_utils.openChatBoxFor(_converse, sender_jid); - _converse.handleMessageStanza($msg({ - 'from': sender_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': msg_id, - }).c('body').t('But soft, what light through yonder airlock breaks?').tree()); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelector('.chat-msg__text').textContent) - .toBe('But soft, what light through yonder airlock breaks?'); - - _converse.handleMessageStanza($msg({ - 'from': sender_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': u.getUniqueId(), - }).c('body').t('But soft, what light through yonder chimney breaks?').up() - .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree()); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); - - expect(view.el.querySelector('.chat-msg__text').textContent) - .toBe('But soft, what light through yonder chimney breaks?'); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1); - expect(view.model.messages.models.length).toBe(1); - - _converse.handleMessageStanza($msg({ - 'from': sender_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': u.getUniqueId(), - }).c('body').t('But soft, what light through yonder window breaks?').up() - .c('replace', {'id': msg_id, 'xmlns': 'urn:xmpp:message-correct:0'}).tree()); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); - - expect(view.el.querySelector('.chat-msg__text').textContent) - .toBe('But soft, what light through yonder window breaks?'); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1); - view.el.querySelector('.chat-msg__content .fa-edit').click(); - const modal = view.model.messages.at(0).message_versions_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - const older_msgs = modal.el.querySelectorAll('.older-msg'); - expect(older_msgs.length).toBe(2); - expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME'); - expect(older_msgs[0].childNodes[2].textContent).toBe('But soft, what light through yonder airlock breaks?'); - expect(view.model.messages.models.length).toBe(1); - done(); - })); - - - describe("when a chatbox is opened for someone who is not in the roster", function () { - - it("the VCard for that user is fetched and the chatbox updated with the results", - mock.initConverse( - ['rosterGroupsFetched'], {'allow_non_roster_messaging': true}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 0); - spyOn(_converse.api, "trigger").and.callThrough(); - - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - var vcard_fetched = false; - spyOn(_converse.api.vcard, "get").and.callFake(function () { - vcard_fetched = true; - return Promise.resolve({ - 'fullname': mock.cur_names[0], - 'vcard_updated': (new Date()).toISOString(), - 'jid': sender_jid - }); + var vcard_fetched = false; + spyOn(_converse.api.vcard, "get").and.callFake(function () { + vcard_fetched = true; + return Promise.resolve({ + 'fullname': mock.cur_names[0], + 'vcard_updated': (new Date()).toISOString(), + 'jid': sender_jid }); - const message = 'This is a received message from someone not on the roster'; - const msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t(message).up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); - - // We don't already have an open chatbox for this user - expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined(); - - await _converse.handleMessageStanza(msg); - const view = await u.waitUntil(() => _converse.api.chatviews.get(sender_jid)); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); - - // Check that the chatbox and its view now exist - const chatbox = await _converse.api.chats.get(sender_jid); - expect(chatbox.get('fullname') === sender_jid); - - await u.waitUntil(() => view.el.querySelector('.chat-msg__author').textContent.trim() === 'Mercutio'); - let author_el = view.el.querySelector('.chat-msg__author'); - expect( _.includes(author_el.textContent.trim(), 'Mercutio')).toBeTruthy(); - await u.waitUntil(() => vcard_fetched, 100); - expect(_converse.api.vcard.get).toHaveBeenCalled(); - await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]) - author_el = view.el.querySelector('.chat-msg__author'); - expect( _.includes(author_el.textContent.trim(), 'Mercutio')).toBeTruthy(); - done(); - })); - }); - - - describe("who is not on the roster", function () { - - it("will open a chatbox and be displayed inside it if allow_non_roster_messaging is true", - mock.initConverse( - ['rosterGroupsFetched'], {'allow_non_roster_messaging': false}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 0); - - spyOn(_converse.api, "trigger").and.callThrough(); - const message = 'This is a received message from someone not on the roster'; - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const msg = $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t(message).up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); - - // We don't already have an open chatbox for this user - expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined(); - - let chatbox = await _converse.api.chats.get(sender_jid); - expect(chatbox).toBe(null); - await _converse.handleMessageStanza(msg); - let view = _converse.chatboxviews.get(sender_jid); - expect(view).not.toBeDefined(); - - _converse.allow_non_roster_messaging = true; - await _converse.handleMessageStanza(msg); - view = _converse.chatboxviews.get(sender_jid); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); - // Check that the chatbox and its view now exist - chatbox = await _converse.api.chats.get(sender_jid); - expect(chatbox).toBeDefined(); - expect(view).toBeDefined(); - // Check that the message was received and check the message parameters - expect(chatbox.messages.length).toEqual(1); - const msg_obj = chatbox.messages.models[0]; - expect(msg_obj.get('message')).toEqual(message); - expect(msg_obj.get('fullname')).toEqual(undefined); - expect(msg_obj.get('sender')).toEqual('them'); - expect(msg_obj.get('is_delayed')).toEqual(false); - - await u.waitUntil(() => view.el.querySelector('.chat-msg__author').textContent.trim() === 'Mercutio'); - // Now check that the message appears inside the chatbox in the DOM - expect(view.content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message); - expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); - expect(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio'); - done(); - })); - }); - - - describe("and for which then an error message is received from the server", function () { - - it("will have the error message displayed after itself", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - - // TODO: what could still be done for error - // messages... if the element has type - // "cancel", then we know the messages wasn't sent, - // and can give the user a nicer indication of - // that. - /* - * yo - * - * - */ - const error_txt = 'Server-to-server connection failed: Connecting failed: connection timeout'; - const sender_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - let fullname = _converse.xmppstatus.get('fullname'); // eslint-disable-line no-unused-vars - fullname = _.isEmpty(fullname) ? _converse.bare_jid: fullname; - await _converse.api.chats.open(sender_jid) - let msg_text = 'This message will not be sent, due to an error'; - const view = _converse.api.chatviews.get(sender_jid); - const message = await view.model.sendMessage(msg_text); - await new Promise(resolve => view.once('messageInserted', resolve)); - let msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent; - expect(msg_txt).toEqual(msg_text); - - // We send another message, for which an error will - // not be received, to test that errors appear - // after the relevant message. - msg_text = 'This message will be sent, and also receive an error'; - const second_message = await view.model.sendMessage(msg_text); - await u.waitUntil(() => sizzle('.chat-msg .chat-msg__text', view.content).length === 2, 1000); - msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent; - expect(msg_txt).toEqual(msg_text); - - /* - * - * - * Server-to-server connection failed: Connecting failed: connection timeout - * - * - */ - let stanza = $msg({ - 'to': _converse.connection.jid, - 'type': 'error', - 'id': message.get('msgid'), - 'from': sender_jid - }) - .c('error', {'type': 'cancel'}) - .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up() - .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }) - .t('Server-to-server connection failed: Connecting failed: connection timeout'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.content.querySelector('.chat-error').textContent.trim()).toEqual(error_txt); - stanza = $msg({ - 'to': _converse.connection.jid, - 'type': 'error', - 'id': second_message.get('id'), - 'from': sender_jid - }) - .c('error', {'type': 'cancel'}) - .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up() - .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }) - .t('Server-to-server connection failed: Connecting failed: connection timeout'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.content.querySelectorAll('.chat-error').length).toEqual(2); - - // We don't render duplicates - stanza = $msg({ - 'to': _converse.connection.jid, - 'type':'error', - 'id': '6fcdeee3-000f-4ce8-a17e-9ce28f0ae104', - 'from': sender_jid - }) - .c('error', {'type': 'cancel'}) - .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up() - .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }) - .t('Server-to-server connection failed: Connecting failed: connection timeout'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(view.content.querySelectorAll('.chat-error').length).toEqual(2); - - msg_text = 'This message will be sent, and also receive an error'; - const third_message = await view.model.sendMessage(msg_text); - await new Promise(resolve => view.once('messageInserted', resolve)); - msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent; - expect(msg_txt).toEqual(msg_text); - - // A different error message will however render - stanza = $msg({ - 'to': _converse.connection.jid, - 'type':'error', - 'id': third_message.get('id'), - 'from': sender_jid - }) - .c('error', {'type': 'cancel'}) - .c('not-allowed', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up() - .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }) - .t('Something else went wrong as well'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.messages.length > 3); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.content.querySelectorAll('.chat-error').length).toEqual(3); - done(); - })); - - it("will not show to the user an error message for a CSI message", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - // See #1317 - // https://github.com/conversejs/converse.js/issues/1317 - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - const contact_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - - const messages = _converse.connection.sent_stanzas.filter(s => s.nodeName === 'message'); - expect(messages.length).toBe(1); - expect(Strophe.serialize(messages[0])).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - const stanza = $msg({ - 'from': contact_jid, - 'type': 'error', - 'id': messages[0].getAttribute('id') - }).c('error', {'type': 'cancel', 'code': '503'}) - .c('service-unavailable', { 'xmlns': 'urn:ietf:params:xml:ns:xmpp-stanzas' }).up() - .c('text', { 'xmlns': 'urn:ietf:params:xml:ns:xmpp-stanzas' }) - .t('User session not found') - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - const view = _converse.chatboxviews.get(contact_jid); - expect(view.content.querySelectorAll('.chat-error').length).toEqual(0); - done(); - })); - }); - - - it("will cause the chat area to be scrolled down only if it was at the bottom originally", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, sender_jid) - const view = _converse.api.chatviews.get(sender_jid); - // Create enough messages so that there's a scrollbar. - const promises = []; - for (let i=0; i<20; i++) { - _converse.handleMessageStanza($msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: _converse.connection.getUniqueId(), - }).c('body').t('Message: '+i).up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); - promises.push(new Promise(resolve => view.once('messageInserted', resolve))); - } - await Promise.all(promises); - // XXX Fails on Travis - // await u.waitUntil(() => view.content.scrollTop, 1000) - await u.waitUntil(() => !view.model.get('auto_scrolled'), 500); - view.content.scrollTop = 0; - // XXX Fails on Travis - // await u.waitUntil(() => view.model.get('scrolled'), 900); - view.model.set('scrolled', true); - - const message = 'This message is received while the chat area is scrolled up'; - _converse.handleMessageStanza($msg({ + }); + const message = 'This is a received message from someone not on the roster'; + const msg = $msg({ from: sender_jid, to: _converse.connection.jid, type: 'chat', id: u.getUniqueId() }).c('body').t(message).up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + + // We don't already have an open chatbox for this user + expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined(); + + await _converse.handleMessageStanza(msg); + const view = await u.waitUntil(() => _converse.api.chatviews.get(sender_jid)); await new Promise(resolve => view.once('messageInserted', resolve)); - await u.waitUntil(() => view.model.messages.length > 20, 1000); - // Now check that the message appears inside the chatbox in the DOM - const msg_txt = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop().textContent; - expect(msg_txt).toEqual(message); - await u.waitUntil(() => u.isVisible(view.el.querySelector('.new-msgs-indicator')), 900); - expect(view.model.get('scrolled')).toBe(true); - expect(view.content.scrollTop).toBe(0); - expect(u.isVisible(view.el.querySelector('.new-msgs-indicator'))).toBeTruthy(); - // Scroll down again - view.content.scrollTop = view.content.scrollHeight; - // XXX Fails on Travis - // await u.waitUntil(() => !u.isVisible(view.el.querySelector('.new-msgs-indicator')), 900); + expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); + + // Check that the chatbox and its view now exist + const chatbox = await _converse.api.chats.get(sender_jid); + expect(chatbox.get('fullname') === sender_jid); + + await u.waitUntil(() => view.el.querySelector('.chat-msg__author').textContent.trim() === 'Mercutio'); + let author_el = view.el.querySelector('.chat-msg__author'); + expect( _.includes(author_el.textContent.trim(), 'Mercutio')).toBeTruthy(); + await u.waitUntil(() => vcard_fetched, 100); + expect(_converse.api.vcard.get).toHaveBeenCalled(); + await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]) + author_el = view.el.querySelector('.chat-msg__author'); + expect( _.includes(author_el.textContent.trim(), 'Mercutio')).toBeTruthy(); done(); })); + }); - it("is ignored if it's intended for a different resource and filter_by_resource is set to true", + + describe("who is not on the roster", function () { + + it("will open a chatbox and be displayed inside it if allow_non_roster_messaging is true", mock.initConverse( - ['rosterGroupsFetched'], {}, + ['rosterGroupsFetched'], {'allow_non_roster_messaging': false}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length) - // Send a message from a different resource - spyOn(converse.env.log, 'info'); - spyOn(_converse.api.chatboxes, 'create').and.callThrough(); - _converse.filter_by_resource = true; + await mock.waitForRoster(_converse, 'current', 0); + + spyOn(_converse.api, "trigger").and.callThrough(); + const message = 'This is a received message from someone not on the roster'; const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - let msg = $msg({ + const msg = $msg({ from: sender_jid, - to: _converse.bare_jid+"/some-other-resource", + to: _converse.connection.jid, type: 'chat', id: u.getUniqueId() - }).c('body').t("This message will not be shown").up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); - await _converse.handleMessageStanza(msg); - - expect(converse.env.log.info).toHaveBeenCalledWith( - "handleMessageStanza: Ignoring incoming message intended for a different resource: romeo@montague.lit/some-other-resource", - ); - expect(_converse.api.chatboxes.create).not.toHaveBeenCalled(); - _converse.filter_by_resource = false; - - const message = "This message sent to a different resource will be shown"; - msg = $msg({ - from: sender_jid, - to: _converse.bare_jid+"/some-other-resource", - type: 'chat', - id: '134234623462346' }).c('body').t(message).up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + + // We don't already have an open chatbox for this user + expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined(); + + let chatbox = await _converse.api.chats.get(sender_jid); + expect(chatbox).toBe(null); await _converse.handleMessageStanza(msg); - await u.waitUntil(() => _converse.chatboxviews.keys().length > 1, 1000); - const view = _converse.chatboxviews.get(sender_jid); - await u.waitUntil(() => view.model.messages.length); - expect(_converse.api.chatboxes.create).toHaveBeenCalled(); - const last_message = await u.waitUntil(() => sizzle('.chat-content:last .chat-msg__text', view.el).pop()); - const msg_txt = last_message.textContent; - expect(msg_txt).toEqual(message); + let view = _converse.chatboxviews.get(sender_jid); + expect(view).not.toBeDefined(); + + _converse.allow_non_roster_messaging = true; + await _converse.handleMessageStanza(msg); + view = _converse.chatboxviews.get(sender_jid); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); + // Check that the chatbox and its view now exist + chatbox = await _converse.api.chats.get(sender_jid); + expect(chatbox).toBeDefined(); + expect(view).toBeDefined(); + // Check that the message was received and check the message parameters + expect(chatbox.messages.length).toEqual(1); + const msg_obj = chatbox.messages.models[0]; + expect(msg_obj.get('message')).toEqual(message); + expect(msg_obj.get('fullname')).toEqual(undefined); + expect(msg_obj.get('sender')).toEqual('them'); + expect(msg_obj.get('is_delayed')).toEqual(false); + + await u.waitUntil(() => view.el.querySelector('.chat-msg__author').textContent.trim() === 'Mercutio'); + // Now check that the message appears inside the chatbox in the DOM + expect(view.content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message); + expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy(); + expect(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio'); done(); })); }); - describe("which contains an OOB URL", function () { + describe("and for which then an error message is received from the server", function () { - it("will render audio from oob mp3 URLs", + it("will have the error message displayed after itself", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.api.chatviews.get(contact_jid); - spyOn(view.model, 'sendMessage').and.callThrough(); + await mock.waitForRoster(_converse, 'current', 1); - let stanza = u.toStanza(` - - Have you heard this funny audio? - https://montague.lit/audio.mp3 - `) - _converse.connection._dataRecv(test_utils.createRequest(stanza)); + // TODO: what could still be done for error + // messages... if the element has type + // "cancel", then we know the messages wasn't sent, + // and can give the user a nicer indication of + // that. + /* + * yo + * + * + */ + const error_txt = 'Server-to-server connection failed: Connecting failed: connection timeout'; + const sender_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + let fullname = _converse.xmppstatus.get('fullname'); // eslint-disable-line no-unused-vars + fullname = _.isEmpty(fullname) ? _converse.bare_jid: fullname; + await _converse.api.chats.open(sender_jid) + let msg_text = 'This message will not be sent, due to an error'; + const view = _converse.api.chatviews.get(sender_jid); + const message = await view.model.sendMessage(msg_text); await new Promise(resolve => view.once('messageInserted', resolve)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg audio').length, 1000); - let msg = view.el.querySelector('.chat-msg .chat-msg__text'); - expect(msg.classList.length).toEqual(1); - expect(u.hasClass('chat-msg__text', msg)).toBe(true); - expect(msg.textContent).toEqual('Have you heard this funny audio?'); - let media = view.el.querySelector('.chat-msg .chat-msg__media'); - expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( - ` `+ - `Download audio file "audio.mp3"`); + let msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent; + expect(msg_txt).toEqual(msg_text); - // If the and contents is the same, don't duplicate. - stanza = u.toStanza(` - - https://montague.lit/audio.mp3 - https://montague.lit/audio.mp3 - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); + // We send another message, for which an error will + // not be received, to test that errors appear + // after the relevant message. + msg_text = 'This message will be sent, and also receive an error'; + const second_message = await view.model.sendMessage(msg_text); + await u.waitUntil(() => sizzle('.chat-msg .chat-msg__text', view.content).length === 2, 1000); + msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent; + expect(msg_txt).toEqual(msg_text); + + /* + * + * + * Server-to-server connection failed: Connecting failed: connection timeout + * + * + */ + let stanza = $msg({ + 'to': _converse.connection.jid, + 'type': 'error', + 'id': message.get('msgid'), + 'from': sender_jid + }) + .c('error', {'type': 'cancel'}) + .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up() + .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }) + .t('Server-to-server connection failed: Connecting failed: connection timeout'); + _converse.connection._dataRecv(mock.createRequest(stanza)); await new Promise(resolve => view.once('messageInserted', resolve)); - msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text'); - expect(msg.innerHTML).toEqual(''); // Emtpy - media = view.el.querySelector('.chat-msg:last-child .chat-msg__media'); - expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( - ` `+ - ``+ - `Download audio file "audio.mp3"`); + expect(view.content.querySelector('.chat-error').textContent.trim()).toEqual(error_txt); + stanza = $msg({ + 'to': _converse.connection.jid, + 'type': 'error', + 'id': second_message.get('id'), + 'from': sender_jid + }) + .c('error', {'type': 'cancel'}) + .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up() + .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }) + .t('Server-to-server connection failed: Connecting failed: connection timeout'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.content.querySelectorAll('.chat-error').length).toEqual(2); + + // We don't render duplicates + stanza = $msg({ + 'to': _converse.connection.jid, + 'type':'error', + 'id': '6fcdeee3-000f-4ce8-a17e-9ce28f0ae104', + 'from': sender_jid + }) + .c('error', {'type': 'cancel'}) + .c('remote-server-not-found', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up() + .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }) + .t('Server-to-server connection failed: Connecting failed: connection timeout'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(view.content.querySelectorAll('.chat-error').length).toEqual(2); + + msg_text = 'This message will be sent, and also receive an error'; + const third_message = await view.model.sendMessage(msg_text); + await new Promise(resolve => view.once('messageInserted', resolve)); + msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent; + expect(msg_txt).toEqual(msg_text); + + // A different error message will however render + stanza = $msg({ + 'to': _converse.connection.jid, + 'type':'error', + 'id': third_message.get('id'), + 'from': sender_jid + }) + .c('error', {'type': 'cancel'}) + .c('not-allowed', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }).up() + .c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" }) + .t('Something else went wrong as well'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.messages.length > 3); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.content.querySelectorAll('.chat-error').length).toEqual(3); done(); })); - it("will render video from oob mp4 URLs", + it("will not show to the user an error message for a CSI message", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid) - const view = _converse.api.chatviews.get(contact_jid); - spyOn(view.model, 'sendMessage').and.callThrough(); + // See #1317 + // https://github.com/conversejs/converse.js/issues/1317 + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); - let stanza = u.toStanza(` - - Have you seen this funny video? - https://montague.lit/video.mp4 - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg video').length, 2000) - let msg = view.el.querySelector('.chat-msg .chat-msg__text'); - expect(msg.classList.length).toBe(1); - expect(msg.textContent).toEqual('Have you seen this funny video?'); - let media = view.el.querySelector('.chat-msg .chat-msg__media'); - expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( - ``); + const contact_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const messages = _converse.connection.sent_stanzas.filter(s => s.nodeName === 'message'); + expect(messages.length).toBe(1); + expect(Strophe.serialize(messages[0])).toBe( + ``+ + ``+ + ``+ + ``+ + ``); - // If the and contents is the same, don't duplicate. - stanza = u.toStanza(` - - https://montague.lit/video.mp4 - https://montague.lit/video.mp4 - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text'); - expect(msg.innerHTML).toEqual(''); // Emtpy - media = view.el.querySelector('.chat-msg:last-child .chat-msg__media'); - expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( - ``); - done(); - })); - - it("will render download links for files from oob URLs", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.api.chatviews.get(contact_jid); - spyOn(view.model, 'sendMessage').and.callThrough(); - const stanza = u.toStanza(` - - Have you downloaded this funny file? - https://montague.lit/funny.pdf - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg a').length, 1000); - const msg = view.el.querySelector('.chat-msg .chat-msg__text'); - expect(u.hasClass('chat-msg__text', msg)).toBe(true); - expect(msg.textContent).toEqual('Have you downloaded this funny file?'); - const media = view.el.querySelector('.chat-msg .chat-msg__media'); - expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( - `Download file "funny.pdf"`); - done(); - })); - - it("will render images from oob URLs", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const base_url = 'https://conversejs.org'; - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid) - const view = _converse.api.chatviews.get(contact_jid); - spyOn(view.model, 'sendMessage').and.callThrough(); - const url = base_url+"/logo/conversejs-filled.svg"; - - const stanza = u.toStanza(` - - Have you seen this funny image? - ${url} - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg img').length, 2000); - - const msg = view.el.querySelector('.chat-msg .chat-msg__text'); - expect(u.hasClass('chat-msg__text', msg)).toBe(true); - expect(msg.textContent).toEqual('Have you seen this funny image?'); - const media = view.el.querySelector('.chat-msg .chat-msg__media'); - expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( - ``+ - ``); + const stanza = $msg({ + 'from': contact_jid, + 'type': 'error', + 'id': messages[0].getAttribute('id') + }).c('error', {'type': 'cancel', 'code': '503'}) + .c('service-unavailable', { 'xmlns': 'urn:ietf:params:xml:ns:xmpp-stanzas' }).up() + .c('text', { 'xmlns': 'urn:ietf:params:xml:ns:xmpp-stanzas' }) + .t('User session not found') + _converse.connection._dataRecv(mock.createRequest(stanza)); + const view = _converse.chatboxviews.get(contact_jid); + expect(view.content.querySelectorAll('.chat-error').length).toEqual(0); done(); })); }); + + + it("will cause the chat area to be scrolled down only if it was at the bottom originally", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, sender_jid) + const view = _converse.api.chatviews.get(sender_jid); + // Create enough messages so that there's a scrollbar. + const promises = []; + for (let i=0; i<20; i++) { + _converse.handleMessageStanza($msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: _converse.connection.getUniqueId(), + }).c('body').t('Message: '+i).up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + promises.push(new Promise(resolve => view.once('messageInserted', resolve))); + } + await Promise.all(promises); + // XXX Fails on Travis + // await u.waitUntil(() => view.content.scrollTop, 1000) + await u.waitUntil(() => !view.model.get('auto_scrolled'), 500); + view.content.scrollTop = 0; + // XXX Fails on Travis + // await u.waitUntil(() => view.model.get('scrolled'), 900); + view.model.set('scrolled', true); + + const message = 'This message is received while the chat area is scrolled up'; + _converse.handleMessageStanza($msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('body').t(message).up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + await new Promise(resolve => view.once('messageInserted', resolve)); + await u.waitUntil(() => view.model.messages.length > 20, 1000); + // Now check that the message appears inside the chatbox in the DOM + const msg_txt = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop().textContent; + expect(msg_txt).toEqual(message); + await u.waitUntil(() => u.isVisible(view.el.querySelector('.new-msgs-indicator')), 900); + expect(view.model.get('scrolled')).toBe(true); + expect(view.content.scrollTop).toBe(0); + expect(u.isVisible(view.el.querySelector('.new-msgs-indicator'))).toBeTruthy(); + // Scroll down again + view.content.scrollTop = view.content.scrollHeight; + // XXX Fails on Travis + // await u.waitUntil(() => !u.isVisible(view.el.querySelector('.new-msgs-indicator')), 900); + done(); + })); + + it("is ignored if it's intended for a different resource and filter_by_resource is set to true", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length) + // Send a message from a different resource + spyOn(converse.env.log, 'info'); + spyOn(_converse.api.chatboxes, 'create').and.callThrough(); + _converse.filter_by_resource = true; + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + let msg = $msg({ + from: sender_jid, + to: _converse.bare_jid+"/some-other-resource", + type: 'chat', + id: u.getUniqueId() + }).c('body').t("This message will not be shown").up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + await _converse.handleMessageStanza(msg); + + expect(converse.env.log.info).toHaveBeenCalledWith( + "handleMessageStanza: Ignoring incoming message intended for a different resource: romeo@montague.lit/some-other-resource", + ); + expect(_converse.api.chatboxes.create).not.toHaveBeenCalled(); + _converse.filter_by_resource = false; + + const message = "This message sent to a different resource will be shown"; + msg = $msg({ + from: sender_jid, + to: _converse.bare_jid+"/some-other-resource", + type: 'chat', + id: '134234623462346' + }).c('body').t(message).up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + await _converse.handleMessageStanza(msg); + await u.waitUntil(() => _converse.chatboxviews.keys().length > 1, 1000); + const view = _converse.chatboxviews.get(sender_jid); + await u.waitUntil(() => view.model.messages.length); + expect(_converse.api.chatboxes.create).toHaveBeenCalled(); + const last_message = await u.waitUntil(() => sizzle('.chat-content:last .chat-msg__text', view.el).pop()); + const msg_txt = last_message.textContent; + expect(msg_txt).toEqual(message); + done(); + })); }); - describe("A XEP-0333 Chat Marker", function () { - it("is sent when a markable message is received from a roster contact", + describe("which contains an OOB URL", function () { + + it("will render audio from oob mp3 URLs", mock.initConverse( - ['rosterGroupsFetched'], {}, + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 1); + await mock.waitForRoster(_converse, 'current', 1); const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const view = _converse.api.chatviews.get(contact_jid); - const msgid = u.getUniqueId(); - const stanza = u.toStanza(` - - My lord, dispatch; read o'er these articles. - - `); - - const sent_stanzas = []; - spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s)); - spyOn(view.model, 'sendMarker').and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.sendMarker.calls.count() === 1); - expect(Strophe.serialize(sent_stanzas[0])).toBe( - ``+ - ``+ - ``); - done(); - })); - - it("is not sent when a markable message is received from someone not on the roster", - mock.initConverse( - ['rosterGroupsFetched'], {'allow_non_roster_messaging': true}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 0); - const contact_jid = 'someone@montague.lit'; - const msgid = u.getUniqueId(); - const stanza = u.toStanza(` - - My lord, dispatch; read o'er these articles. - - `); - - const sent_stanzas = []; - spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s)); - await _converse.handleMessageStanza(stanza); - const sent_messages = sent_stanzas - .map(s => _.isElement(s) ? s : s.nodeTree) - .filter(e => e.nodeName === 'message'); - - expect(sent_messages.length).toBe(1); - expect(Strophe.serialize(sent_messages[0])).toBe( - ``+ - ``+ - ``+ - ``+ - `` - ); - done(); - })); - - it("is ignored if it's a carbon copy of one that I sent from a different client", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 1); - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]); - - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); + await mock.openChatBoxFor(_converse, contact_jid); const view = _converse.api.chatviews.get(contact_jid); + spyOn(view.model, 'sendMessage').and.callThrough(); let stanza = u.toStanza(` - - 😊 - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); + to="romeo@montague.lit/orchard"> + Have you heard this funny audio? + https://montague.lit/audio.mp3 + `) + _converse.connection._dataRecv(mock.createRequest(stanza)); await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.model.messages.length).toBe(1); + await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg audio').length, 1000); + let msg = view.el.querySelector('.chat-msg .chat-msg__text'); + expect(msg.classList.length).toEqual(1); + expect(u.hasClass('chat-msg__text', msg)).toBe(true); + expect(msg.textContent).toEqual('Have you heard this funny audio?'); + let media = view.el.querySelector('.chat-msg .chat-msg__media'); + expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( + ` `+ + `Download audio file "audio.mp3"`); - stanza = u.toStanza( - ` - - - - - - - - - + // If the and contents is the same, don't duplicate. + stanza = u.toStanza(` + + https://montague.lit/audio.mp3 + https://montague.lit/audio.mp3 `); - spyOn(_converse.api, "trigger").and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.api.trigger.calls.count(), 500); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.model.messages.length).toBe(1); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text'); + expect(msg.innerHTML).toEqual(''); // Emtpy + media = view.el.querySelector('.chat-msg:last-child .chat-msg__media'); + expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( + ` `+ + ``+ + `Download audio file "audio.mp3"`); + done(); + })); + + it("will render video from oob mp4 URLs", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid) + const view = _converse.api.chatviews.get(contact_jid); + spyOn(view.model, 'sendMessage').and.callThrough(); + + let stanza = u.toStanza(` + + Have you seen this funny video? + https://montague.lit/video.mp4 + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg video').length, 2000) + let msg = view.el.querySelector('.chat-msg .chat-msg__text'); + expect(msg.classList.length).toBe(1); + expect(msg.textContent).toEqual('Have you seen this funny video?'); + let media = view.el.querySelector('.chat-msg .chat-msg__media'); + expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( + ``); + + + // If the and contents is the same, don't duplicate. + stanza = u.toStanza(` + + https://montague.lit/video.mp4 + https://montague.lit/video.mp4 + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + msg = view.el.querySelector('.chat-msg:last-child .chat-msg__text'); + expect(msg.innerHTML).toEqual(''); // Emtpy + media = view.el.querySelector('.chat-msg:last-child .chat-msg__media'); + expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( + ``); + done(); + })); + + it("will render download links for files from oob URLs", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.api.chatviews.get(contact_jid); + spyOn(view.model, 'sendMessage').and.callThrough(); + const stanza = u.toStanza(` + + Have you downloaded this funny file? + https://montague.lit/funny.pdf + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg a').length, 1000); + const msg = view.el.querySelector('.chat-msg .chat-msg__text'); + expect(u.hasClass('chat-msg__text', msg)).toBe(true); + expect(msg.textContent).toEqual('Have you downloaded this funny file?'); + const media = view.el.querySelector('.chat-msg .chat-msg__media'); + expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( + `Download file "funny.pdf"`); + done(); + })); + + it("will render images from oob URLs", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const base_url = 'https://conversejs.org'; + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid) + const view = _converse.api.chatviews.get(contact_jid); + spyOn(view.model, 'sendMessage').and.callThrough(); + const url = base_url+"/logo/conversejs-filled.svg"; + + const stanza = u.toStanza(` + + Have you seen this funny image? + ${url} + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-msg img').length, 2000); + + const msg = view.el.querySelector('.chat-msg .chat-msg__text'); + expect(u.hasClass('chat-msg__text', msg)).toBe(true); + expect(msg.textContent).toEqual('Have you seen this funny image?'); + const media = view.el.querySelector('.chat-msg .chat-msg__media'); + expect(media.innerHTML.replace(/(\r\n|\n|\r)/gm, "")).toEqual( + ``+ + ``); done(); })); }); }); + +describe("A XEP-0333 Chat Marker", function () { + + it("is sent when a markable message is received from a roster contact", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.api.chatviews.get(contact_jid); + const msgid = u.getUniqueId(); + const stanza = u.toStanza(` + + My lord, dispatch; read o'er these articles. + + `); + + const sent_stanzas = []; + spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s)); + spyOn(view.model, 'sendMarker').and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.sendMarker.calls.count() === 1); + expect(Strophe.serialize(sent_stanzas[0])).toBe( + ``+ + ``+ + ``); + done(); + })); + + it("is not sent when a markable message is received from someone not on the roster", + mock.initConverse( + ['rosterGroupsFetched'], {'allow_non_roster_messaging': true}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 0); + const contact_jid = 'someone@montague.lit'; + const msgid = u.getUniqueId(); + const stanza = u.toStanza(` + + My lord, dispatch; read o'er these articles. + + `); + + const sent_stanzas = []; + spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s)); + await _converse.handleMessageStanza(stanza); + const sent_messages = sent_stanzas + .map(s => _.isElement(s) ? s : s.nodeTree) + .filter(e => e.nodeName === 'message'); + + expect(sent_messages.length).toBe(1); + expect(Strophe.serialize(sent_messages[0])).toBe( + ``+ + ``+ + ``+ + ``+ + `` + ); + done(); + })); + + it("is ignored if it's a carbon copy of one that I sent from a different client", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 1); + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]); + + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const view = _converse.api.chatviews.get(contact_jid); + + let stanza = u.toStanza(` + + 😊 + + + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.model.messages.length).toBe(1); + + stanza = u.toStanza( + ` + + + + + + + + + + `); + spyOn(_converse.api, "trigger").and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.api.trigger.calls.count(), 500); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.model.messages.length).toBe(1); + done(); + })); +}); diff --git a/spec/minchats.js b/spec/minchats.js index 4e05c6118..953ef4ff8 100644 --- a/spec/minchats.js +++ b/spec/minchats.js @@ -1,167 +1,165 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const _ = converse.env._; - const $msg = converse.env.$msg; - const u = converse.env.utils; +/*global mock */ - describe("The Minimized Chats Widget", function () { +const _ = converse.env._; +const $msg = converse.env.$msg; +const u = converse.env.utils; - it("shows chats that have been minimized", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { +describe("The Minimized Chats Widget", function () { - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - _converse.minimized_chats.initToggle(); + it("shows chats that have been minimized", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - let contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid) - let chatview = _converse.chatboxviews.get(contact_jid); - expect(chatview.model.get('minimized')).toBeFalsy(); - expect(u.isVisible(_converse.minimized_chats.el)).toBe(false); - chatview.el.querySelector('.toggle-chatbox-button').click(); - expect(chatview.model.get('minimized')).toBeTruthy(); - expect(u.isVisible(_converse.minimized_chats.el)).toBe(true); - expect(_converse.minimized_chats.keys().length).toBe(1); - expect(_converse.minimized_chats.keys()[0]).toBe(contact_jid); + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + _converse.minimized_chats.initToggle(); - contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - chatview = _converse.chatboxviews.get(contact_jid); - expect(chatview.model.get('minimized')).toBeFalsy(); - chatview.el.querySelector('.toggle-chatbox-button').click(); - expect(chatview.model.get('minimized')).toBeTruthy(); - expect(u.isVisible(_converse.minimized_chats.el)).toBe(true); - expect(_converse.minimized_chats.keys().length).toBe(2); - expect(_.includes(_converse.minimized_chats.keys(), contact_jid)).toBeTruthy(); - done(); - })); + let contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid) + let chatview = _converse.chatboxviews.get(contact_jid); + expect(chatview.model.get('minimized')).toBeFalsy(); + expect(u.isVisible(_converse.minimized_chats.el)).toBe(false); + chatview.el.querySelector('.toggle-chatbox-button').click(); + expect(chatview.model.get('minimized')).toBeTruthy(); + expect(u.isVisible(_converse.minimized_chats.el)).toBe(true); + expect(_converse.minimized_chats.keys().length).toBe(1); + expect(_converse.minimized_chats.keys()[0]).toBe(contact_jid); - it("can be toggled to hide or show minimized chats", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + chatview = _converse.chatboxviews.get(contact_jid); + expect(chatview.model.get('minimized')).toBeFalsy(); + chatview.el.querySelector('.toggle-chatbox-button').click(); + expect(chatview.model.get('minimized')).toBeTruthy(); + expect(u.isVisible(_converse.minimized_chats.el)).toBe(true); + expect(_converse.minimized_chats.keys().length).toBe(2); + expect(_.includes(_converse.minimized_chats.keys(), contact_jid)).toBeTruthy(); + done(); + })); - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - _converse.minimized_chats.initToggle(); + it("can be toggled to hide or show minimized chats", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatBoxFor(_converse, contact_jid); - const chatview = _converse.chatboxviews.get(contact_jid); - expect(u.isVisible(_converse.minimized_chats.el)).toBeFalsy(); - chatview.model.set({'minimized': true}); - expect(u.isVisible(_converse.minimized_chats.el)).toBeTruthy(); - expect(_converse.minimized_chats.keys().length).toBe(1); - expect(_converse.minimized_chats.keys()[0]).toBe(contact_jid); - expect(u.isVisible(_converse.minimized_chats.el.querySelector('.minimized-chats-flyout'))).toBeTruthy(); - expect(_converse.minimized_chats.toggleview.model.get('collapsed')).toBeFalsy(); - _converse.minimized_chats.el.querySelector('#toggle-minimized-chats').click(); - await u.waitUntil(() => u.isVisible(_converse.minimized_chats.el.querySelector('.minimized-chats-flyout'))); - expect(_converse.minimized_chats.toggleview.model.get('collapsed')).toBeTruthy(); - done(); - })); + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + _converse.minimized_chats.initToggle(); - it("shows the number messages received to minimized chats", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + const chatview = _converse.chatboxviews.get(contact_jid); + expect(u.isVisible(_converse.minimized_chats.el)).toBeFalsy(); + chatview.model.set({'minimized': true}); + expect(u.isVisible(_converse.minimized_chats.el)).toBeTruthy(); + expect(_converse.minimized_chats.keys().length).toBe(1); + expect(_converse.minimized_chats.keys()[0]).toBe(contact_jid); + expect(u.isVisible(_converse.minimized_chats.el.querySelector('.minimized-chats-flyout'))).toBeTruthy(); + expect(_converse.minimized_chats.toggleview.model.get('collapsed')).toBeFalsy(); + _converse.minimized_chats.el.querySelector('#toggle-minimized-chats').click(); + await u.waitUntil(() => u.isVisible(_converse.minimized_chats.el.querySelector('.minimized-chats-flyout'))); + expect(_converse.minimized_chats.toggleview.model.get('collapsed')).toBeTruthy(); + done(); + })); - await test_utils.waitForRoster(_converse, 'current', 4); - await test_utils.openControlBox(_converse); - _converse.minimized_chats.initToggle(); + it("shows the number messages received to minimized chats", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - var i, contact_jid, chatview, msg; - _converse.minimized_chats.toggleview.model.set({'collapsed': true}); + await mock.waitForRoster(_converse, 'current', 4); + await mock.openControlBox(_converse); + _converse.minimized_chats.initToggle(); - const unread_el = _converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'); - expect(unread_el === null).toBe(true); + var i, contact_jid, chatview, msg; + _converse.minimized_chats.toggleview.model.set({'collapsed': true}); - for (i=0; i<3; i++) { - contact_jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - test_utils.openChatBoxFor(_converse, contact_jid); - } - await u.waitUntil(() => _converse.chatboxes.length == 4); + const unread_el = _converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'); + expect(unread_el === null).toBe(true); - chatview = _converse.chatboxviews.get(contact_jid); - chatview.model.set({'minimized': true}); - for (i=0; i<3; i++) { - msg = $msg({ - from: contact_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('body').t('This message is sent to a minimized chatbox').up() - .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); - _converse.handleMessageStanza(msg); - } - await u.waitUntil(() => chatview.model.messages.length === 3, 500); + for (i=0; i<3; i++) { + contact_jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + mock.openChatBoxFor(_converse, contact_jid); + } + await u.waitUntil(() => _converse.chatboxes.length == 4); - expect(u.isVisible(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'))).toBeTruthy(); - expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((3).toString()); - // Chat state notifications don't increment the unread messages counter - // state - _converse.handleMessageStanza($msg({ + chatview = _converse.chatboxviews.get(contact_jid); + chatview.model.set({'minimized': true}); + for (i=0; i<3; i++) { + msg = $msg({ from: contact_jid, to: _converse.connection.jid, type: 'chat', id: u.getUniqueId() - }).c('composing', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); - expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString()); + }).c('body').t('This message is sent to a minimized chatbox').up() + .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree(); + _converse.handleMessageStanza(msg); + } + await u.waitUntil(() => chatview.model.messages.length === 3, 500); - // state - _converse.handleMessageStanza($msg({ - from: contact_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('paused', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); - expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString()); + expect(u.isVisible(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'))).toBeTruthy(); + expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((3).toString()); + // Chat state notifications don't increment the unread messages counter + // state + _converse.handleMessageStanza($msg({ + from: contact_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('composing', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString()); - // state - _converse.handleMessageStanza($msg({ - from: contact_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('gone', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); - expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString()); + // state + _converse.handleMessageStanza($msg({ + from: contact_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('paused', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString()); - // state - _converse.handleMessageStanza($msg({ - from: contact_jid, - to: _converse.connection.jid, - type: 'chat', - id: u.getUniqueId() - }).c('inactive', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); - expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString()); - done(); - })); + // state + _converse.handleMessageStanza($msg({ + from: contact_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('gone', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString()); - it("shows the number messages received to minimized groupchats", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + // state + _converse.handleMessageStanza($msg({ + from: contact_jid, + to: _converse.connection.jid, + type: 'chat', + id: u.getUniqueId() + }).c('inactive', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()); + expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString()); + done(); + })); - const muc_jid = 'kitchen@conference.shakespeare.lit'; - await test_utils.openAndEnterChatRoom(_converse, 'kitchen@conference.shakespeare.lit', 'fires'); - const view = _converse.chatboxviews.get(muc_jid); - view.model.set({'minimized': true}); - const message = 'fires: Your attention is required'; - const nick = mock.chatroom_names[0]; - const msg = $msg({ - from: muc_jid+'/'+nick, - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t(message).tree(); - view.model.queueMessage(msg); - await u.waitUntil(() => view.model.messages.length); - expect(u.isVisible(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'))).toBeTruthy(); - expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe('1'); - done(); - })); - }); + it("shows the number messages received to minimized groupchats", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'kitchen@conference.shakespeare.lit'; + await mock.openAndEnterChatRoom(_converse, 'kitchen@conference.shakespeare.lit', 'fires'); + const view = _converse.chatboxviews.get(muc_jid); + view.model.set({'minimized': true}); + const message = 'fires: Your attention is required'; + const nick = mock.chatroom_names[0]; + const msg = $msg({ + from: muc_jid+'/'+nick, + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t(message).tree(); + view.model.queueMessage(msg); + await u.waitUntil(() => view.model.messages.length); + expect(u.isVisible(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count'))).toBeTruthy(); + expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe('1'); + done(); + })); }); diff --git a/spec/modtools.js b/spec/modtools.js index 82915a0c8..7111b7585 100644 --- a/spec/modtools.js +++ b/spec/modtools.js @@ -1,370 +1,368 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const _ = converse.env._; - const $iq = converse.env.$iq; - const $pres = converse.env.$pres; - const sizzle = converse.env.sizzle; - const Strophe = converse.env.Strophe; - const u = converse.env.utils; +/*global mock */ - describe("The groupchat moderator tool", function () { +const _ = converse.env._; +const $iq = converse.env.$iq; +const $pres = converse.env.$pres; +const sizzle = converse.env.sizzle; +const Strophe = converse.env.Strophe; +const u = converse.env.utils; - it("allows you to set affiliations and roles", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { +describe("The groupchat moderator tool", function () { - spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough(); - const muc_jid = 'lounge@montague.lit'; - - let members = [ - {'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'}, - {'jid': 'gower@shakespeare.lit', 'nick': 'gower', 'affiliation': 'member'}, - {'jid': 'wiccarocks@shakespeare.lit', 'nick': 'wiccan', 'affiliation': 'admin'}, - {'jid': 'crone1@shakespeare.lit', 'nick': 'thirdwitch', 'affiliation': 'owner'}, - {'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'owner'}, - ]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members); - const view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => (view.model.occupants.length === 5), 1000); - - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = '/modtools'; - const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; - view.onKeyDown(enter); - await u.waitUntil(() => view.showModeratorToolsModal.calls.count()); - - const modal = view.modtools_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - let tab = modal.el.querySelector('#affiliations-tab'); - // Clear so that we don't match older stanzas - _converse.connection.IQ_stanzas = []; - tab.click(); - let select = modal.el.querySelector('.select-affiliation'); - expect(select.value).toBe('owner'); - select.value = 'admin'; - let button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]'); - button.click(); - await u.waitUntil(() => !modal.loading_users_with_affiliation); - let user_els = modal.el.querySelectorAll('.list-group--users > li'); - expect(user_els.length).toBe(1); - expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: wiccarocks@shakespeare.lit'); - expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: wiccan'); - expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: admin'); - - _converse.connection.IQ_stanzas = []; - select.value = 'owner'; - button.click(); - await u.waitUntil(() => !modal.loading_users_with_affiliation); - user_els = modal.el.querySelectorAll('.list-group--users > li'); - expect(user_els.length).toBe(2); - expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: romeo@montague.lit'); - expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: romeo'); - expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner'); - - expect(user_els[1].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: crone1@shakespeare.lit'); - expect(user_els[1].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: thirdwitch'); - expect(user_els[1].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner'); - - const toggle = user_els[1].querySelector('.list-group-item:nth-child(3n) .toggle-form'); - const form = user_els[1].querySelector('.list-group-item:nth-child(3n) .affiliation-form'); - expect(u.hasClass('hidden', form)).toBeTruthy(); - toggle.click(); - expect(u.hasClass('hidden', form)).toBeFalsy(); - select = form.querySelector('.select-affiliation'); - expect(select.value).toBe('owner'); - select.value = 'admin'; - const input = form.querySelector('input[name="reason"]'); - input.value = "You're an admin now"; - const submit = form.querySelector('.btn-primary'); - submit.click(); - - spyOn(_converse.ChatRoomOccupants.prototype, 'fetchMembers').and.callThrough(); - const sent_IQ = _converse.connection.IQ_stanzas.pop(); - expect(Strophe.serialize(sent_IQ)).toBe( - ``+ - ``+ - ``+ - `You're an admin now`+ - ``+ - ``+ - ``); - - _converse.connection.IQ_stanzas = []; - const stanza = $iq({ - 'type': 'result', - 'id': sent_IQ.getAttribute('id'), - 'from': view.model.get('jid'), - 'to': _converse.connection.jid - }); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.occupants.fetchMembers.calls.count()); - - members = [ - {'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'}, - {'jid': 'gower@shakespeare.lit', 'nick': 'gower', 'affiliation': 'member'}, - {'jid': 'wiccarocks@shakespeare.lit', 'nick': 'wiccan', 'affiliation': 'admin'}, - {'jid': 'crone1@shakespeare.lit', 'nick': 'thirdwitch', 'affiliation': 'admin'}, - {'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'owner'}, - ]; - await test_utils.returnMemberLists(_converse, muc_jid, members); - await u.waitUntil(() => view.model.occupants.pluck('affiliation').filter(o => o === 'owner').length === 1); - const alert = modal.el.querySelector('.alert-primary'); - expect(alert.textContent.trim()).toBe('Affiliation changed'); - - user_els = modal.el.querySelectorAll('.list-group--users > li'); - expect(user_els.length).toBe(1); - expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: romeo@montague.lit'); - expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: romeo'); - expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner'); - - tab = modal.el.querySelector('#roles-tab'); - tab.click(); - select = modal.el.querySelector('.select-role'); - expect(u.isVisible(select)).toBe(true); - expect(select.value).toBe('moderator'); - button = modal.el.querySelector('.btn-primary[name="users_with_role"]'); - button.click(); - - const roles_panel = modal.el.querySelector('#roles-tabpanel'); - await u.waitUntil(() => roles_panel.querySelectorAll('.list-group--users > li').length === 1); - select.value = 'participant'; - button.click(); - await u.waitUntil(() => !modal.loading_users_with_affiliation); - user_els = roles_panel.querySelectorAll('.list-group--users > li') - expect(user_els.length).toBe(1); - expect(user_els[0].textContent.trim()).toBe('No users with that role found.'); - done(); - })); - - it("allows you to filter affiliation search results", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough(); - const muc_jid = 'lounge@montague.lit'; - const members = [ - {'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'}, - {'jid': 'gower@shakespeare.lit', 'nick': 'gower', 'affiliation': 'member'}, - {'jid': 'wiccarocks@shakespeare.lit', 'nick': 'wiccan', 'affiliation': 'member'}, - {'jid': 'crone1@shakespeare.lit', 'nick': 'thirdwitch', 'affiliation': 'member'}, - {'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'member'}, - {'jid': 'juliet@capulet.lit', 'nick': 'juliet', 'affiliation': 'member'}, - ]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members); - const view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => (view.model.occupants.length === 6), 1000); - - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = '/modtools'; - const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; - view.onKeyDown(enter); - await u.waitUntil(() => view.showModeratorToolsModal.calls.count()); - - const modal = view.modtools_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - // Clear so that we don't match older stanzas - _converse.connection.IQ_stanzas = []; - const select = modal.el.querySelector('.select-affiliation'); - expect(select.value).toBe('owner'); - select.value = 'member'; - const button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]'); - button.click(); - await u.waitUntil(() => !modal.loading_users_with_affiliation); - const user_els = modal.el.querySelectorAll('.list-group--users > li'); - expect(user_els.length).toBe(6); - - const nicks = Array.from(modal.el.querySelectorAll('.list-group--users > li')).map(el => el.getAttribute('data-nick')); - expect(nicks.join(' ')).toBe('gower juliet romeo thirdwitch wiccan witch'); - - const filter = modal.el.querySelector('[name="filter"]'); - expect(filter).not.toBe(null); - - filter.value = 'romeo'; - u.triggerEvent(filter, "keyup", "KeyboardEvent"); - await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1)); - - filter.value = 'r'; - u.triggerEvent(filter, "keyup", "KeyboardEvent"); - await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 3)); - - filter.value = 'gower'; - u.triggerEvent(filter, "keyup", "KeyboardEvent"); - await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1)); - done(); - })); - - it("allows you to filter role search results", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough(); - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', []); - const view = _converse.chatboxviews.get(muc_jid); - - _converse.connection._dataRecv(test_utils.createRequest( - $pres({to: _converse.jid, from: `${muc_jid}/nomorenicks`}) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': `nomorenicks@montague.lit`, - 'role': 'participant' - }) - )); - _converse.connection._dataRecv(test_utils.createRequest( - $pres({to: _converse.jid, from: `${muc_jid}/newb`}) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': `newb@montague.lit`, - 'role': 'participant' - }) - )); - _converse.connection._dataRecv(test_utils.createRequest( - $pres({to: _converse.jid, from: `${muc_jid}/some1`}) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': `some1@montague.lit`, - 'role': 'participant' - }) - )); - _converse.connection._dataRecv(test_utils.createRequest( - $pres({to: _converse.jid, from: `${muc_jid}/oldhag`}) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': `oldhag@montague.lit`, - 'role': 'participant' - }) - )); - _converse.connection._dataRecv(test_utils.createRequest( - $pres({to: _converse.jid, from: `${muc_jid}/crone`}) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': `crone@montague.lit`, - 'role': 'participant' - }) - )); - _converse.connection._dataRecv(test_utils.createRequest( - $pres({to: _converse.jid, from: `${muc_jid}/tux`}) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': `tux@montague.lit`, - 'role': 'participant' - }) - )); - await u.waitUntil(() => (view.model.occupants.length === 7), 1000); - - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = '/modtools'; - const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; - view.onKeyDown(enter); - await u.waitUntil(() => view.showModeratorToolsModal.calls.count()); - - const modal = view.modtools_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - - const tab = modal.el.querySelector('#roles-tab'); - tab.click(); - - // Clear so that we don't match older stanzas - _converse.connection.IQ_stanzas = []; - - const select = modal.el.querySelector('.select-role'); - expect(select.value).toBe('moderator'); - select.value = 'participant'; - - const button = modal.el.querySelector('.btn-primary[name="users_with_role"]'); - button.click(); - await u.waitUntil(() => !modal.loading_users_with_role); - const user_els = modal.el.querySelectorAll('.list-group--users > li'); - expect(user_els.length).toBe(6); - - const nicks = Array.from(modal.el.querySelectorAll('.list-group--users > li')).map(el => el.getAttribute('data-nick')); - expect(nicks.join(' ')).toBe('crone newb nomorenicks oldhag some1 tux'); - - const filter = modal.el.querySelector('[name="filter"]'); - expect(filter).not.toBe(null); - - filter.value = 'tux'; - u.triggerEvent(filter, "keyup", "KeyboardEvent"); - await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1)); - - filter.value = 'r'; - u.triggerEvent(filter, "keyup", "KeyboardEvent"); - await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 2)); - - filter.value = 'crone'; - u.triggerEvent(filter, "keyup", "KeyboardEvent"); - await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1)); - done(); - })); - - it("shows an error message if a particular affiliation list may not be retrieved", + it("allows you to set affiliations and roles", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { - spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough(); - const muc_jid = 'lounge@montague.lit'; - const members = [ - {'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'}, - {'jid': 'gower@shakespeare.lit', 'nick': 'gower', 'affiliation': 'member'}, - {'jid': 'wiccarocks@shakespeare.lit', 'nick': 'wiccan', 'affiliation': 'admin'}, - {'jid': 'crone1@shakespeare.lit', 'nick': 'thirdwitch', 'affiliation': 'owner'}, - {'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'owner'}, - ]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members); - const view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => (view.model.occupants.length === 5)); + spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough(); + const muc_jid = 'lounge@montague.lit'; - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = '/modtools'; - const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; - view.onKeyDown(enter); - await u.waitUntil(() => view.showModeratorToolsModal.calls.count()); + let members = [ + {'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'}, + {'jid': 'gower@shakespeare.lit', 'nick': 'gower', 'affiliation': 'member'}, + {'jid': 'wiccarocks@shakespeare.lit', 'nick': 'wiccan', 'affiliation': 'admin'}, + {'jid': 'crone1@shakespeare.lit', 'nick': 'thirdwitch', 'affiliation': 'owner'}, + {'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'owner'}, + ]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members); + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => (view.model.occupants.length === 5), 1000); - const modal = view.modtools_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - const tab = modal.el.querySelector('#affiliations-tab'); - // Clear so that we don't match older stanzas - _converse.connection.IQ_stanzas = []; - const IQ_stanzas = _converse.connection.IQ_stanzas; - tab.click(); - const select = modal.el.querySelector('.select-affiliation'); - select.value = 'outcast'; - const button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]'); - button.click(); + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = '/modtools'; + const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; + view.onKeyDown(enter); + await u.waitUntil(() => view.showModeratorToolsModal.calls.count()); - const iq_query = await u.waitUntil(() => _.filter( - IQ_stanzas, - s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="outcast"]`, s).length - ).pop()); + const modal = view.modtools_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + let tab = modal.el.querySelector('#affiliations-tab'); + // Clear so that we don't match older stanzas + _converse.connection.IQ_stanzas = []; + tab.click(); + let select = modal.el.querySelector('.select-affiliation'); + expect(select.value).toBe('owner'); + select.value = 'admin'; + let button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]'); + button.click(); + await u.waitUntil(() => !modal.loading_users_with_affiliation); + let user_els = modal.el.querySelectorAll('.list-group--users > li'); + expect(user_els.length).toBe(1); + expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: wiccarocks@shakespeare.lit'); + expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: wiccan'); + expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: admin'); - const error = u.toStanza( - ` + _converse.connection.IQ_stanzas = []; + select.value = 'owner'; + button.click(); + await u.waitUntil(() => !modal.loading_users_with_affiliation); + user_els = modal.el.querySelectorAll('.list-group--users > li'); + expect(user_els.length).toBe(2); + expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: romeo@montague.lit'); + expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: romeo'); + expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner'); - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(error)); - await u.waitUntil(() => !modal.loading_users_with_affiliation); + expect(user_els[1].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: crone1@shakespeare.lit'); + expect(user_els[1].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: thirdwitch'); + expect(user_els[1].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner'); - const user_els = modal.el.querySelectorAll('.list-group--users > li'); - expect(user_els.length).toBe(1); - expect(user_els[0].textContent.trim()).toBe('Error: not allowed to fetch outcast list for MUC lounge@montague.lit'); - done(); - })); - }); + const toggle = user_els[1].querySelector('.list-group-item:nth-child(3n) .toggle-form'); + const form = user_els[1].querySelector('.list-group-item:nth-child(3n) .affiliation-form'); + expect(u.hasClass('hidden', form)).toBeTruthy(); + toggle.click(); + expect(u.hasClass('hidden', form)).toBeFalsy(); + select = form.querySelector('.select-affiliation'); + expect(select.value).toBe('owner'); + select.value = 'admin'; + const input = form.querySelector('input[name="reason"]'); + input.value = "You're an admin now"; + const submit = form.querySelector('.btn-primary'); + submit.click(); + + spyOn(_converse.ChatRoomOccupants.prototype, 'fetchMembers').and.callThrough(); + const sent_IQ = _converse.connection.IQ_stanzas.pop(); + expect(Strophe.serialize(sent_IQ)).toBe( + ``+ + ``+ + ``+ + `You're an admin now`+ + ``+ + ``+ + ``); + + _converse.connection.IQ_stanzas = []; + const stanza = $iq({ + 'type': 'result', + 'id': sent_IQ.getAttribute('id'), + 'from': view.model.get('jid'), + 'to': _converse.connection.jid + }); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.occupants.fetchMembers.calls.count()); + + members = [ + {'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'}, + {'jid': 'gower@shakespeare.lit', 'nick': 'gower', 'affiliation': 'member'}, + {'jid': 'wiccarocks@shakespeare.lit', 'nick': 'wiccan', 'affiliation': 'admin'}, + {'jid': 'crone1@shakespeare.lit', 'nick': 'thirdwitch', 'affiliation': 'admin'}, + {'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'owner'}, + ]; + await mock.returnMemberLists(_converse, muc_jid, members); + await u.waitUntil(() => view.model.occupants.pluck('affiliation').filter(o => o === 'owner').length === 1); + const alert = modal.el.querySelector('.alert-primary'); + expect(alert.textContent.trim()).toBe('Affiliation changed'); + + user_els = modal.el.querySelectorAll('.list-group--users > li'); + expect(user_els.length).toBe(1); + expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: romeo@montague.lit'); + expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: romeo'); + expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner'); + + tab = modal.el.querySelector('#roles-tab'); + tab.click(); + select = modal.el.querySelector('.select-role'); + expect(u.isVisible(select)).toBe(true); + expect(select.value).toBe('moderator'); + button = modal.el.querySelector('.btn-primary[name="users_with_role"]'); + button.click(); + + const roles_panel = modal.el.querySelector('#roles-tabpanel'); + await u.waitUntil(() => roles_panel.querySelectorAll('.list-group--users > li').length === 1); + select.value = 'participant'; + button.click(); + await u.waitUntil(() => !modal.loading_users_with_affiliation); + user_els = roles_panel.querySelectorAll('.list-group--users > li') + expect(user_els.length).toBe(1); + expect(user_els[0].textContent.trim()).toBe('No users with that role found.'); + done(); + })); + + it("allows you to filter affiliation search results", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough(); + const muc_jid = 'lounge@montague.lit'; + const members = [ + {'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'}, + {'jid': 'gower@shakespeare.lit', 'nick': 'gower', 'affiliation': 'member'}, + {'jid': 'wiccarocks@shakespeare.lit', 'nick': 'wiccan', 'affiliation': 'member'}, + {'jid': 'crone1@shakespeare.lit', 'nick': 'thirdwitch', 'affiliation': 'member'}, + {'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'member'}, + {'jid': 'juliet@capulet.lit', 'nick': 'juliet', 'affiliation': 'member'}, + ]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members); + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => (view.model.occupants.length === 6), 1000); + + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = '/modtools'; + const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; + view.onKeyDown(enter); + await u.waitUntil(() => view.showModeratorToolsModal.calls.count()); + + const modal = view.modtools_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + // Clear so that we don't match older stanzas + _converse.connection.IQ_stanzas = []; + const select = modal.el.querySelector('.select-affiliation'); + expect(select.value).toBe('owner'); + select.value = 'member'; + const button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]'); + button.click(); + await u.waitUntil(() => !modal.loading_users_with_affiliation); + const user_els = modal.el.querySelectorAll('.list-group--users > li'); + expect(user_els.length).toBe(6); + + const nicks = Array.from(modal.el.querySelectorAll('.list-group--users > li')).map(el => el.getAttribute('data-nick')); + expect(nicks.join(' ')).toBe('gower juliet romeo thirdwitch wiccan witch'); + + const filter = modal.el.querySelector('[name="filter"]'); + expect(filter).not.toBe(null); + + filter.value = 'romeo'; + u.triggerEvent(filter, "keyup", "KeyboardEvent"); + await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1)); + + filter.value = 'r'; + u.triggerEvent(filter, "keyup", "KeyboardEvent"); + await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 3)); + + filter.value = 'gower'; + u.triggerEvent(filter, "keyup", "KeyboardEvent"); + await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1)); + done(); + })); + + it("allows you to filter role search results", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough(); + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', []); + const view = _converse.chatboxviews.get(muc_jid); + + _converse.connection._dataRecv(mock.createRequest( + $pres({to: _converse.jid, from: `${muc_jid}/nomorenicks`}) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': `nomorenicks@montague.lit`, + 'role': 'participant' + }) + )); + _converse.connection._dataRecv(mock.createRequest( + $pres({to: _converse.jid, from: `${muc_jid}/newb`}) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': `newb@montague.lit`, + 'role': 'participant' + }) + )); + _converse.connection._dataRecv(mock.createRequest( + $pres({to: _converse.jid, from: `${muc_jid}/some1`}) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': `some1@montague.lit`, + 'role': 'participant' + }) + )); + _converse.connection._dataRecv(mock.createRequest( + $pres({to: _converse.jid, from: `${muc_jid}/oldhag`}) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': `oldhag@montague.lit`, + 'role': 'participant' + }) + )); + _converse.connection._dataRecv(mock.createRequest( + $pres({to: _converse.jid, from: `${muc_jid}/crone`}) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': `crone@montague.lit`, + 'role': 'participant' + }) + )); + _converse.connection._dataRecv(mock.createRequest( + $pres({to: _converse.jid, from: `${muc_jid}/tux`}) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': `tux@montague.lit`, + 'role': 'participant' + }) + )); + await u.waitUntil(() => (view.model.occupants.length === 7), 1000); + + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = '/modtools'; + const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; + view.onKeyDown(enter); + await u.waitUntil(() => view.showModeratorToolsModal.calls.count()); + + const modal = view.modtools_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + + const tab = modal.el.querySelector('#roles-tab'); + tab.click(); + + // Clear so that we don't match older stanzas + _converse.connection.IQ_stanzas = []; + + const select = modal.el.querySelector('.select-role'); + expect(select.value).toBe('moderator'); + select.value = 'participant'; + + const button = modal.el.querySelector('.btn-primary[name="users_with_role"]'); + button.click(); + await u.waitUntil(() => !modal.loading_users_with_role); + const user_els = modal.el.querySelectorAll('.list-group--users > li'); + expect(user_els.length).toBe(6); + + const nicks = Array.from(modal.el.querySelectorAll('.list-group--users > li')).map(el => el.getAttribute('data-nick')); + expect(nicks.join(' ')).toBe('crone newb nomorenicks oldhag some1 tux'); + + const filter = modal.el.querySelector('[name="filter"]'); + expect(filter).not.toBe(null); + + filter.value = 'tux'; + u.triggerEvent(filter, "keyup", "KeyboardEvent"); + await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1)); + + filter.value = 'r'; + u.triggerEvent(filter, "keyup", "KeyboardEvent"); + await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 2)); + + filter.value = 'crone'; + u.triggerEvent(filter, "keyup", "KeyboardEvent"); + await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1)); + done(); + })); + + it("shows an error message if a particular affiliation list may not be retrieved", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough(); + const muc_jid = 'lounge@montague.lit'; + const members = [ + {'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'}, + {'jid': 'gower@shakespeare.lit', 'nick': 'gower', 'affiliation': 'member'}, + {'jid': 'wiccarocks@shakespeare.lit', 'nick': 'wiccan', 'affiliation': 'admin'}, + {'jid': 'crone1@shakespeare.lit', 'nick': 'thirdwitch', 'affiliation': 'owner'}, + {'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'owner'}, + ]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members); + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => (view.model.occupants.length === 5)); + + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = '/modtools'; + const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; + view.onKeyDown(enter); + await u.waitUntil(() => view.showModeratorToolsModal.calls.count()); + + const modal = view.modtools_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + const tab = modal.el.querySelector('#affiliations-tab'); + // Clear so that we don't match older stanzas + _converse.connection.IQ_stanzas = []; + const IQ_stanzas = _converse.connection.IQ_stanzas; + tab.click(); + const select = modal.el.querySelector('.select-affiliation'); + select.value = 'outcast'; + const button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]'); + button.click(); + + const iq_query = await u.waitUntil(() => _.filter( + IQ_stanzas, + s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="outcast"]`, s).length + ).pop()); + + const error = u.toStanza( + ` + + + + + `); + _converse.connection._dataRecv(mock.createRequest(error)); + await u.waitUntil(() => !modal.loading_users_with_affiliation); + + const user_els = modal.el.querySelectorAll('.list-group--users > li'); + expect(user_els.length).toBe(1); + expect(user_els[0].textContent.trim()).toBe('Error: not allowed to fetch outcast list for MUC lounge@montague.lit'); + done(); + })); }); diff --git a/spec/muc.js b/spec/muc.js index 831aa35c3..90fcf351e 100644 --- a/spec/muc.js +++ b/spec/muc.js @@ -1,374 +1,447 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const _ = converse.env._, - $pres = converse.env.$pres, - $iq = converse.env.$iq, - $msg = converse.env.$msg, - Model = converse.env.Model, - Strophe = converse.env.Strophe, - Promise = converse.env.Promise, - sizzle = converse.env.sizzle, - u = converse.env.utils; +/*global mock */ - describe("Groupchats", function () { +const _ = converse.env._, + $pres = converse.env.$pres, + $iq = converse.env.$iq, + $msg = converse.env.$msg, + Model = converse.env.Model, + Strophe = converse.env.Strophe, + Promise = converse.env.Promise, + sizzle = converse.env.sizzle, + u = converse.env.utils; - describe("The \"rooms\" API", function () { +describe("Groupchats", function () { - it("has a method 'close' which closes rooms by JID or all rooms when called with no arguments", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + describe("The \"rooms\" API", function () { - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + it("has a method 'close' which closes rooms by JID or all rooms when called with no arguments", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - _converse.connection.IQ_stanzas = []; - await test_utils.openAndEnterChatRoom(_converse, 'leisure@montague.lit', 'romeo'); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - _converse.connection.IQ_stanzas = []; - await test_utils.openAndEnterChatRoom(_converse, 'news@montague.lit', 'romeo'); - expect(u.isVisible(_converse.chatboxviews.get('lounge@montague.lit').el)).toBeTruthy(); - expect(u.isVisible(_converse.chatboxviews.get('leisure@montague.lit').el)).toBeTruthy(); - expect(u.isVisible(_converse.chatboxviews.get('news@montague.lit').el)).toBeTruthy(); + _converse.connection.IQ_stanzas = []; + await mock.openAndEnterChatRoom(_converse, 'leisure@montague.lit', 'romeo'); - await _converse.api.roomviews.close('lounge@montague.lit'); - expect(_converse.chatboxviews.get('lounge@montague.lit')).toBeUndefined(); - expect(u.isVisible(_converse.chatboxviews.get('leisure@montague.lit').el)).toBeTruthy(); - expect(u.isVisible(_converse.chatboxviews.get('news@montague.lit').el)).toBeTruthy(); + _converse.connection.IQ_stanzas = []; + await mock.openAndEnterChatRoom(_converse, 'news@montague.lit', 'romeo'); + expect(u.isVisible(_converse.chatboxviews.get('lounge@montague.lit').el)).toBeTruthy(); + expect(u.isVisible(_converse.chatboxviews.get('leisure@montague.lit').el)).toBeTruthy(); + expect(u.isVisible(_converse.chatboxviews.get('news@montague.lit').el)).toBeTruthy(); - await _converse.api.roomviews.close(['leisure@montague.lit', 'news@montague.lit']); - expect(_converse.chatboxviews.get('lounge@montague.lit')).toBeUndefined(); - expect(_converse.chatboxviews.get('leisure@montague.lit')).toBeUndefined(); - expect(_converse.chatboxviews.get('news@montague.lit')).toBeUndefined(); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - await test_utils.openAndEnterChatRoom(_converse, 'leisure@montague.lit', 'romeo'); - expect(u.isVisible(_converse.chatboxviews.get('lounge@montague.lit').el)).toBeTruthy(); - expect(u.isVisible(_converse.chatboxviews.get('leisure@montague.lit').el)).toBeTruthy(); - await _converse.api.roomviews.close(); - expect(_converse.chatboxviews.get('lounge@montague.lit')).toBeUndefined(); - expect(_converse.chatboxviews.get('leisure@montague.lit')).toBeUndefined(); - done(); - })); + await _converse.api.roomviews.close('lounge@montague.lit'); + expect(_converse.chatboxviews.get('lounge@montague.lit')).toBeUndefined(); + expect(u.isVisible(_converse.chatboxviews.get('leisure@montague.lit').el)).toBeTruthy(); + expect(u.isVisible(_converse.chatboxviews.get('news@montague.lit').el)).toBeTruthy(); - it("has a method 'get' which returns a wrapped groupchat (if it exists)", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + await _converse.api.roomviews.close(['leisure@montague.lit', 'news@montague.lit']); + expect(_converse.chatboxviews.get('lounge@montague.lit')).toBeUndefined(); + expect(_converse.chatboxviews.get('leisure@montague.lit')).toBeUndefined(); + expect(_converse.chatboxviews.get('news@montague.lit')).toBeUndefined(); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + await mock.openAndEnterChatRoom(_converse, 'leisure@montague.lit', 'romeo'); + expect(u.isVisible(_converse.chatboxviews.get('lounge@montague.lit').el)).toBeTruthy(); + expect(u.isVisible(_converse.chatboxviews.get('leisure@montague.lit').el)).toBeTruthy(); + await _converse.api.roomviews.close(); + expect(_converse.chatboxviews.get('lounge@montague.lit')).toBeUndefined(); + expect(_converse.chatboxviews.get('leisure@montague.lit')).toBeUndefined(); + done(); + })); - await test_utils.waitForRoster(_converse, 'current'); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group .group-toggle').length, 300); - let muc_jid = 'chillout@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - let room = await _converse.api.rooms.get(muc_jid); - expect(room instanceof Object).toBeTruthy(); + it("has a method 'get' which returns a wrapped groupchat (if it exists)", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - let chatroomview = _converse.chatboxviews.get(muc_jid); - expect(chatroomview.is_chatroom).toBeTruthy(); + await mock.waitForRoster(_converse, 'current'); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group .group-toggle').length, 300); + let muc_jid = 'chillout@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + let room = await _converse.api.rooms.get(muc_jid); + expect(room instanceof Object).toBeTruthy(); - expect(u.isVisible(chatroomview.el)).toBeTruthy(); - await chatroomview.close(); + let chatroomview = _converse.chatboxviews.get(muc_jid); + expect(chatroomview.is_chatroom).toBeTruthy(); - // Test with mixed case - muc_jid = 'Leisure@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - room = await _converse.api.rooms.get(muc_jid); - expect(room instanceof Object).toBeTruthy(); - chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase()); - expect(u.isVisible(chatroomview.el)).toBeTruthy(); + expect(u.isVisible(chatroomview.el)).toBeTruthy(); + await chatroomview.close(); - muc_jid = 'leisure@montague.lit'; - room = await _converse.api.rooms.get(muc_jid); - expect(room instanceof Object).toBeTruthy(); - chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase()); - expect(u.isVisible(chatroomview.el)).toBeTruthy(); + // Test with mixed case + muc_jid = 'Leisure@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + room = await _converse.api.rooms.get(muc_jid); + expect(room instanceof Object).toBeTruthy(); + chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase()); + expect(u.isVisible(chatroomview.el)).toBeTruthy(); - muc_jid = 'leiSure@montague.lit'; - room = await _converse.api.rooms.get(muc_jid); - expect(room instanceof Object).toBeTruthy(); - chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase()); - expect(u.isVisible(chatroomview.el)).toBeTruthy(); - chatroomview.close(); + muc_jid = 'leisure@montague.lit'; + room = await _converse.api.rooms.get(muc_jid); + expect(room instanceof Object).toBeTruthy(); + chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase()); + expect(u.isVisible(chatroomview.el)).toBeTruthy(); - // Non-existing room - muc_jid = 'chillout2@montague.lit'; - room = await _converse.api.rooms.get(muc_jid); - expect(room).toBe(null); - done(); - })); + muc_jid = 'leiSure@montague.lit'; + room = await _converse.api.rooms.get(muc_jid); + expect(room instanceof Object).toBeTruthy(); + chatroomview = _converse.chatboxviews.get(muc_jid.toLowerCase()); + expect(u.isVisible(chatroomview.el)).toBeTruthy(); + chatroomview.close(); - it("has a method 'open' which opens (optionally configures) and returns a wrapped chat box", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + // Non-existing room + muc_jid = 'chillout2@montague.lit'; + room = await _converse.api.rooms.get(muc_jid); + expect(room).toBe(null); + done(); + })); - // Mock 'getDiscoInfo', otherwise the room won't be - // displayed as it waits first for the features to be returned - // (when it's a new room being created). - spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); + it("has a method 'open' which opens (optionally configures) and returns a wrapped chat box", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - let jid = 'lounge@montague.lit'; - let chatroomview, IQ_id; - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current'); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group .group-toggle').length); + // Mock 'getDiscoInfo', otherwise the room won't be + // displayed as it waits first for the features to be returned + // (when it's a new room being created). + spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); - let room = await _converse.api.rooms.open(jid); - // Test on groupchat that's not yet open - expect(room instanceof Model).toBeTruthy(); - chatroomview = _converse.chatboxviews.get(jid); - expect(chatroomview.is_chatroom).toBeTruthy(); - await u.waitUntil(() => u.isVisible(chatroomview.el)); + let jid = 'lounge@montague.lit'; + let chatroomview, IQ_id; + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group .group-toggle').length); - // Test again, now that the room exists. - room = await _converse.api.rooms.open(jid); - expect(room instanceof Model).toBeTruthy(); - chatroomview = _converse.chatboxviews.get(jid); - expect(chatroomview.is_chatroom).toBeTruthy(); - expect(u.isVisible(chatroomview.el)).toBeTruthy(); - await chatroomview.close(); + let room = await _converse.api.rooms.open(jid); + // Test on groupchat that's not yet open + expect(room instanceof Model).toBeTruthy(); + chatroomview = _converse.chatboxviews.get(jid); + expect(chatroomview.is_chatroom).toBeTruthy(); + await u.waitUntil(() => u.isVisible(chatroomview.el)); - // Test with mixed case in JID - jid = 'Leisure@montague.lit'; - room = await _converse.api.rooms.open(jid); - expect(room instanceof Model).toBeTruthy(); - chatroomview = _converse.chatboxviews.get(jid.toLowerCase()); - await u.waitUntil(() => u.isVisible(chatroomview.el)); + // Test again, now that the room exists. + room = await _converse.api.rooms.open(jid); + expect(room instanceof Model).toBeTruthy(); + chatroomview = _converse.chatboxviews.get(jid); + expect(chatroomview.is_chatroom).toBeTruthy(); + expect(u.isVisible(chatroomview.el)).toBeTruthy(); + await chatroomview.close(); - jid = 'leisure@montague.lit'; - room = await _converse.api.rooms.open(jid); - expect(room instanceof Model).toBeTruthy(); - chatroomview = _converse.chatboxviews.get(jid.toLowerCase()); - await u.waitUntil(() => u.isVisible(chatroomview.el)); + // Test with mixed case in JID + jid = 'Leisure@montague.lit'; + room = await _converse.api.rooms.open(jid); + expect(room instanceof Model).toBeTruthy(); + chatroomview = _converse.chatboxviews.get(jid.toLowerCase()); + await u.waitUntil(() => u.isVisible(chatroomview.el)); - jid = 'leiSure@montague.lit'; - room = await _converse.api.rooms.open(jid); - expect(room instanceof Model).toBeTruthy(); - chatroomview = _converse.chatboxviews.get(jid.toLowerCase()); - await u.waitUntil(() => u.isVisible(chatroomview.el)); - chatroomview.close(); + jid = 'leisure@montague.lit'; + room = await _converse.api.rooms.open(jid); + expect(room instanceof Model).toBeTruthy(); + chatroomview = _converse.chatboxviews.get(jid.toLowerCase()); + await u.waitUntil(() => u.isVisible(chatroomview.el)); - _converse.muc_instant_rooms = false; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); - // Test with configuration - room = await _converse.api.rooms.open('room@conference.example.org', { - 'nick': 'some1', - 'auto_configure': true, - 'roomconfig': { - 'getmemberlist': ['moderator', 'participant'], - 'changesubject': false, - 'membersonly': true, - 'persistentroom': true, - 'publicroom': true, - 'roomdesc': 'Welcome to this groupchat', - 'whois': 'anyone' - } - }); - expect(room instanceof Model).toBeTruthy(); - chatroomview = _converse.chatboxviews.get('room@conference.example.org'); + jid = 'leiSure@montague.lit'; + room = await _converse.api.rooms.open(jid); + expect(room instanceof Model).toBeTruthy(); + chatroomview = _converse.chatboxviews.get(jid.toLowerCase()); + await u.waitUntil(() => u.isVisible(chatroomview.el)); + chatroomview.close(); - // We pretend this is a new room, so no disco info is returned. - const features_stanza = $iq({ - from: 'room@conference.example.org', - 'id': IQ_id, - 'to': 'romeo@montague.lit/desktop', - 'type': 'error' - }).c('error', {'type': 'cancel'}) - .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); + _converse.muc_instant_rooms = false; + const sendIQ = _converse.connection.sendIQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + // Test with configuration + room = await _converse.api.rooms.open('room@conference.example.org', { + 'nick': 'some1', + 'auto_configure': true, + 'roomconfig': { + 'getmemberlist': ['moderator', 'participant'], + 'changesubject': false, + 'membersonly': true, + 'persistentroom': true, + 'publicroom': true, + 'roomdesc': 'Welcome to this groupchat', + 'whois': 'anyone' + } + }); + expect(room instanceof Model).toBeTruthy(); + chatroomview = _converse.chatboxviews.get('room@conference.example.org'); - /* - * - * - * - * - * - * - */ - const presence = $pres({ - from:'room@conference.example.org/some1', - to:'romeo@montague.lit/pda' - }) - .c('x', {xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item', { - affiliation: 'owner', - jid: 'romeo@montague.lit/pda', - role: 'moderator' - }).up() - .c('status', {code:'110'}).up() - .c('status', {code:'201'}); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(_converse.connection.sendIQ).toHaveBeenCalled(); + // We pretend this is a new room, so no disco info is returned. + const features_stanza = $iq({ + from: 'room@conference.example.org', + 'id': IQ_id, + 'to': 'romeo@montague.lit/desktop', + 'type': 'error' + }).c('error', {'type': 'cancel'}) + .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); + _converse.connection._dataRecv(mock.createRequest(features_stanza)); - const IQ_stanzas = _converse.connection.IQ_stanzas; - const iq = IQ_stanzas.filter(s => s.querySelector(`query[xmlns="${Strophe.NS.MUC_OWNER}"]`)).pop(); - expect(Strophe.serialize(iq)).toBe( - ``+ - ``); - - const node = u.toStanza(` - - - - Configuration for room@conference.example.org - Complete and submit this form to configure the room. - - http://jabber.org/protocol/muc#roomconfig - - - Room - - - - 1 - - - moderatorsanyone - - - moderator - participant - visitor - - - - - - 20 - - - `); - - spyOn(chatroomview.model, 'sendConfiguration').and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(node)); - await u.waitUntil(() => chatroomview.model.sendConfiguration.calls.count() === 1); - - const sent_stanza = IQ_stanzas.filter(s => s.getAttribute('type') === 'set').pop(); - expect(sizzle('field[var="muc#roomconfig_roomname"] value', sent_stanza).pop().textContent.trim()).toBe('Room'); - expect(sizzle('field[var="muc#roomconfig_roomdesc"] value', sent_stanza).pop().textContent.trim()).toBe('Welcome to this groupchat'); - expect(sizzle('field[var="muc#roomconfig_persistentroom"] value', sent_stanza).pop().textContent.trim()).toBe('1'); - expect(sizzle('field[var="muc#roomconfig_getmemberlist"] value', sent_stanza).map(e => e.textContent.trim()).join(' ')).toBe('moderator participant'); - expect(sizzle('field[var="muc#roomconfig_publicroom"] value ', sent_stanza).pop().textContent.trim()).toBe('1'); - expect(sizzle('field[var="muc#roomconfig_changesubject"] value', sent_stanza).pop().textContent.trim()).toBe('0'); - expect(sizzle('field[var="muc#roomconfig_whois"] value ', sent_stanza).pop().textContent.trim()).toBe('anyone'); - expect(sizzle('field[var="muc#roomconfig_membersonly"] value', sent_stanza).pop().textContent.trim()).toBe('1'); - expect(sizzle('field[var="muc#roomconfig_historylength"] value', sent_stanza).pop().textContent.trim()).toBe('20'); - done(); - })); - }); - - describe("An instant groupchat", function () { - - it("will be created when muc_instant_rooms is set to true", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - let IQ_stanzas = _converse.connection.IQ_stanzas; - const muc_jid = 'lounge@montague.lit'; - await test_utils.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); - - const disco_selector = `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`; - const stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(disco_selector)).pop()); - // We pretend this is a new room, so no disco info is returned. - const features_stanza = $iq({ - 'from': 'lounge@montague.lit', - 'id': stanza.getAttribute('id'), - 'to': 'romeo@montague.lit/desktop', - 'type': 'error' - }).c('error', {'type': 'cancel'}) - .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); - - const view = _converse.chatboxviews.get('lounge@montague.lit'); - spyOn(view.model, 'join').and.callThrough(); - await test_utils.waitForReservedNick(_converse, muc_jid, ''); - const input = await u.waitUntil(() => view.el.querySelector('input[name="nick"]'), 1000); - expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.NICKNAME_REQUIRED); - input.value = 'nicky'; - view.el.querySelector('input[type=submit]').click(); - expect(view.model.join).toHaveBeenCalled(); - _converse.connection.IQ_stanzas = []; - await test_utils.getRoomFeatures(_converse, muc_jid); - await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING); - - // The user has just entered the room (because join was called) - // and receives their own presence from the server. - // See example 24: - // https://xmpp.org/extensions/xep-0045.html#enter-pres - // - /* - * - * - * - * - * - * - */ - const presence = $pres({ - to:'romeo@montague.lit/orchard', - from:'lounge@montague.lit/nicky', - id:'5025e055-036c-4bc5-a227-706e7e352053' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ + /* + * + * + * + * + * + * + */ + const presence = $pres({ + from:'room@conference.example.org/some1', + to:'romeo@montague.lit/pda' + }) + .c('x', {xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item', { affiliation: 'owner', - jid: 'romeo@montague.lit/orchard', + jid: 'romeo@montague.lit/pda', role: 'moderator' }).up() - .c('status').attrs({code:'110'}).up() - .c('status').attrs({code:'201'}).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(presence)); + .c('status', {code:'110'}).up() + .c('status', {code:'201'}); + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(_converse.connection.sendIQ).toHaveBeenCalled(); - await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED); - await test_utils.returnMemberLists(_converse, muc_jid); - const num_info_msgs = await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length); - expect(num_info_msgs).toBe(1); + const IQ_stanzas = _converse.connection.IQ_stanzas; + const iq = IQ_stanzas.filter(s => s.querySelector(`query[xmlns="${Strophe.NS.MUC_OWNER}"]`)).pop(); + expect(Strophe.serialize(iq)).toBe( + ``+ + ``); - const info_texts = Array.from(view.el.querySelectorAll('.chat-content .chat-info')).map(e => e.textContent.trim()); - expect(info_texts[0]).toBe('A new groupchat has been created'); + const node = u.toStanza(` + + + + Configuration for room@conference.example.org + Complete and submit this form to configure the room. + + http://jabber.org/protocol/muc#roomconfig + + + Room + + + + 1 + + + moderatorsanyone + + + moderator + participant + visitor + + + + + + 20 + + + `); - const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext.trim()).toEqual("nicky has entered the groupchat"); + spyOn(chatroomview.model, 'sendConfiguration').and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(node)); + await u.waitUntil(() => chatroomview.model.sendConfiguration.calls.count() === 1); - // An instant room is created by saving the default configuratoin. - // - /* - * - * - */ - const selector = `query[xmlns="${Strophe.NS.MUC_OWNER}"]`; - IQ_stanzas = _converse.connection.IQ_stanzas; - const iq = await u.waitUntil(() => IQ_stanzas.filter(s => s.querySelector(selector)).pop()); - expect(Strophe.serialize(iq)).toBe( - ``+ - ``+ - ``); + const sent_stanza = IQ_stanzas.filter(s => s.getAttribute('type') === 'set').pop(); + expect(sizzle('field[var="muc#roomconfig_roomname"] value', sent_stanza).pop().textContent.trim()).toBe('Room'); + expect(sizzle('field[var="muc#roomconfig_roomdesc"] value', sent_stanza).pop().textContent.trim()).toBe('Welcome to this groupchat'); + expect(sizzle('field[var="muc#roomconfig_persistentroom"] value', sent_stanza).pop().textContent.trim()).toBe('1'); + expect(sizzle('field[var="muc#roomconfig_getmemberlist"] value', sent_stanza).map(e => e.textContent.trim()).join(' ')).toBe('moderator participant'); + expect(sizzle('field[var="muc#roomconfig_publicroom"] value ', sent_stanza).pop().textContent.trim()).toBe('1'); + expect(sizzle('field[var="muc#roomconfig_changesubject"] value', sent_stanza).pop().textContent.trim()).toBe('0'); + expect(sizzle('field[var="muc#roomconfig_whois"] value ', sent_stanza).pop().textContent.trim()).toBe('anyone'); + expect(sizzle('field[var="muc#roomconfig_membersonly"] value', sent_stanza).pop().textContent.trim()).toBe('1'); + expect(sizzle('field[var="muc#roomconfig_historylength"] value', sent_stanza).pop().textContent.trim()).toBe('20'); + done(); + })); + }); + describe("An instant groupchat", function () { + + it("will be created when muc_instant_rooms is set to true", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + let IQ_stanzas = _converse.connection.IQ_stanzas; + const muc_jid = 'lounge@montague.lit'; + await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); + + const disco_selector = `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`; + const stanza = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(disco_selector)).pop()); + // We pretend this is a new room, so no disco info is returned. + const features_stanza = $iq({ + 'from': 'lounge@montague.lit', + 'id': stanza.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'error' + }).c('error', {'type': 'cancel'}) + .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + + const view = _converse.chatboxviews.get('lounge@montague.lit'); + spyOn(view.model, 'join').and.callThrough(); + await mock.waitForReservedNick(_converse, muc_jid, ''); + const input = await u.waitUntil(() => view.el.querySelector('input[name="nick"]'), 1000); + expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.NICKNAME_REQUIRED); + input.value = 'nicky'; + view.el.querySelector('input[type=submit]').click(); + expect(view.model.join).toHaveBeenCalled(); + _converse.connection.IQ_stanzas = []; + await mock.getRoomFeatures(_converse, muc_jid); + await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING); + + // The user has just entered the room (because join was called) + // and receives their own presence from the server. + // See example 24: + // https://xmpp.org/extensions/xep-0045.html#enter-pres + // + /* + * + * + * + * + * + * + */ + const presence = $pres({ + to:'romeo@montague.lit/orchard', + from:'lounge@montague.lit/nicky', + id:'5025e055-036c-4bc5-a227-706e7e352053' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: 'owner', + jid: 'romeo@montague.lit/orchard', + role: 'moderator' + }).up() + .c('status').attrs({code:'110'}).up() + .c('status').attrs({code:'201'}).nodeTree; + _converse.connection._dataRecv(mock.createRequest(presence)); + + await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED); + await mock.returnMemberLists(_converse, muc_jid); + const num_info_msgs = await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length); + expect(num_info_msgs).toBe(1); + + const info_texts = Array.from(view.el.querySelectorAll('.chat-content .chat-info')).map(e => e.textContent.trim()); + expect(info_texts[0]).toBe('A new groupchat has been created'); + + const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext.trim()).toEqual("nicky has entered the groupchat"); + + // An instant room is created by saving the default configuratoin. + // + /* + * + * + */ + const selector = `query[xmlns="${Strophe.NS.MUC_OWNER}"]`; + IQ_stanzas = _converse.connection.IQ_stanzas; + const iq = await u.waitUntil(() => IQ_stanzas.filter(s => s.querySelector(selector)).pop()); + expect(Strophe.serialize(iq)).toBe( + ``+ + ``+ + ``); + + done(); + })); + }); + + describe("A Groupchat", function () { + + describe("upon being entered", function () { + + it("will fetch the member list if muc_fetch_members is true", + mock.initConverse( + ['rosterGroupsFetched'], {'muc_fetch_members': true}, + async function (done, _converse) { + + let sent_IQs = _converse.connection.IQ_stanzas; + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + let view = _converse.chatboxviews.get(muc_jid); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(3); + + // Check in reverse order that we requested all three lists + const owner_iq = sent_IQs.pop(); + expect(Strophe.serialize(owner_iq)).toBe( + ``+ + ``+ + ``); + + const admin_iq = sent_IQs.pop(); + expect(Strophe.serialize(admin_iq)).toBe( + ``+ + ``+ + ``); + + const member_iq = sent_IQs.pop(); + expect(Strophe.serialize(member_iq)).toBe( + ``+ + ``+ + ``); + view.close(); + + _converse.connection.IQ_stanzas = []; + sent_IQs = _converse.connection.IQ_stanzas; + _converse.muc_fetch_members = false; + await mock.openAndEnterChatRoom(_converse, 'orchard@montague.lit', 'romeo'); + view = _converse.chatboxviews.get('orchard@montague.lit'); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(0); + await view.close(); + + _converse.connection.IQ_stanzas = []; + sent_IQs = _converse.connection.IQ_stanzas; + _converse.muc_fetch_members = ['admin']; + await mock.openAndEnterChatRoom(_converse, 'courtyard@montague.lit', 'romeo'); + view = _converse.chatboxviews.get('courtyard@montague.lit'); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(1); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation="admin"]')).length).toBe(1); + view.close(); + + _converse.connection.IQ_stanzas = []; + sent_IQs = _converse.connection.IQ_stanzas; + _converse.muc_fetch_members = ['owner']; + await mock.openAndEnterChatRoom(_converse, 'garden@montague.lit', 'romeo'); + view = _converse.chatboxviews.get('garden@montague.lit'); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(1); + expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation="owner"]')).length).toBe(1); + view.close(); done(); })); - }); - describe("A Groupchat", function () { + describe("when fetching the member lists", function () { - describe("upon being entered", function () { - - it("will fetch the member list if muc_fetch_members is true", + it("gracefully handles being forbidden from fetching the lists for certain affiliations", mock.initConverse( ['rosterGroupsFetched'], {'muc_fetch_members': true}, async function (done, _converse) { - let sent_IQs = _converse.connection.IQ_stanzas; + const sent_IQs = _converse.connection.IQ_stanzas; const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - let view = _converse.chatboxviews.get(muc_jid); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(3); + const features = [ + 'http://jabber.org/protocol/muc', + 'jabber:iq:register', + 'muc_hidden', + 'muc_membersonly', + 'muc_passwordprotected', + Strophe.NS.MAM, + Strophe.NS.SID + ]; + const nick = 'romeo'; + await _converse.api.rooms.open(muc_jid); + await mock.getRoomFeatures(_converse, muc_jid, features); + await mock.waitForReservedNick(_converse, muc_jid, nick); + mock.receiveOwnMUCPresence(_converse, muc_jid, nick); + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); // Check in reverse order that we requested all three lists const owner_iq = sent_IQs.pop(); @@ -376,4939 +449,4864 @@ window.addEventListener('converse-loaded', () => { ``+ ``+ ``); - const admin_iq = sent_IQs.pop(); expect(Strophe.serialize(admin_iq)).toBe( ``+ ``+ ``); - const member_iq = sent_IQs.pop(); expect(Strophe.serialize(member_iq)).toBe( ``+ ``+ ``); - view.close(); - _converse.connection.IQ_stanzas = []; - sent_IQs = _converse.connection.IQ_stanzas; - _converse.muc_fetch_members = false; - await test_utils.openAndEnterChatRoom(_converse, 'orchard@montague.lit', 'romeo'); - view = _converse.chatboxviews.get('orchard@montague.lit'); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(0); - await view.close(); + // It might be that the user is not allowed to fetch certain lists. + let err_stanza = u.toStanza( + ` + + `); + _converse.connection._dataRecv(mock.createRequest(err_stanza)); - _converse.connection.IQ_stanzas = []; - sent_IQs = _converse.connection.IQ_stanzas; - _converse.muc_fetch_members = ['admin']; - await test_utils.openAndEnterChatRoom(_converse, 'courtyard@montague.lit', 'romeo'); - view = _converse.chatboxviews.get('courtyard@montague.lit'); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(1); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation="admin"]')).length).toBe(1); - view.close(); + err_stanza = u.toStanza( + ` + + `); + _converse.connection._dataRecv(mock.createRequest(err_stanza)); - _converse.connection.IQ_stanzas = []; - sent_IQs = _converse.connection.IQ_stanzas; - _converse.muc_fetch_members = ['owner']; - await test_utils.openAndEnterChatRoom(_converse, 'garden@montague.lit', 'romeo'); - view = _converse.chatboxviews.get('garden@montague.lit'); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation]')).length).toBe(1); - expect(sent_IQs.filter(iq => iq.querySelector('query item[affiliation="owner"]')).length).toBe(1); - view.close(); - done(); - })); + // Now the service sends the member lists to the user + const member_list_stanza = $iq({ + 'from': muc_jid, + 'id': member_iq.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) + .c('item', { + 'affiliation': 'member', + 'jid': 'hag66@shakespeare.lit', + 'nick': 'thirdwitch', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(member_list_stanza)); - describe("when fetching the member lists", function () { - - it("gracefully handles being forbidden from fetching the lists for certain affiliations", - mock.initConverse( - ['rosterGroupsFetched'], {'muc_fetch_members': true}, - async function (done, _converse) { - - const sent_IQs = _converse.connection.IQ_stanzas; - const muc_jid = 'lounge@montague.lit'; - const features = [ - 'http://jabber.org/protocol/muc', - 'jabber:iq:register', - 'muc_hidden', - 'muc_membersonly', - 'muc_passwordprotected', - Strophe.NS.MAM, - Strophe.NS.SID - ]; - const nick = 'romeo'; - await _converse.api.rooms.open(muc_jid); - await test_utils.getRoomFeatures(_converse, muc_jid, features); - await test_utils.waitForReservedNick(_converse, muc_jid, nick); - test_utils.receiveOwnMUCPresence(_converse, muc_jid, nick); - const view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); - - // Check in reverse order that we requested all three lists - const owner_iq = sent_IQs.pop(); - expect(Strophe.serialize(owner_iq)).toBe( - ``+ - ``+ - ``); - const admin_iq = sent_IQs.pop(); - expect(Strophe.serialize(admin_iq)).toBe( - ``+ - ``+ - ``); - const member_iq = sent_IQs.pop(); - expect(Strophe.serialize(member_iq)).toBe( - ``+ - ``+ - ``); - - // It might be that the user is not allowed to fetch certain lists. - let err_stanza = u.toStanza( - ` - - `); - _converse.connection._dataRecv(test_utils.createRequest(err_stanza)); - - err_stanza = u.toStanza( - ` - - `); - _converse.connection._dataRecv(test_utils.createRequest(err_stanza)); - - // Now the service sends the member lists to the user - const member_list_stanza = $iq({ - 'from': muc_jid, - 'id': member_iq.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) - .c('item', { - 'affiliation': 'member', - 'jid': 'hag66@shakespeare.lit', - 'nick': 'thirdwitch', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(member_list_stanza)); - - await u.waitUntil(() => view.model.occupants.length > 1); - expect(view.model.occupants.length).toBe(2); - // The existing owner occupant should not have their - // affiliation removed due to the owner list - // not being returned (forbidden err). - expect(view.model.occupants.findWhere({'jid': _converse.bare_jid}).get('affiliation')).toBe('owner'); - expect(view.model.occupants.findWhere({'jid': 'hag66@shakespeare.lit'}).get('affiliation')).toBe('member'); - done(); - })); - }); - }); - - describe("topic", function () { - - it("is shown the header", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc'); - const text = 'Jabber/XMPP Development | RFCs and Extensions: https://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org'; - let stanza = u.toStanza(` - - ${text} - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); - await new Promise(resolve => view.model.once('change:subject', resolve)); - - const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')); - expect(head_desc?.textContent.trim()).toBe(text); - - stanza = u.toStanza( - ` - This is a message subject - This is a message - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(sizzle('.chat-msg__subject', view.el).length).toBe(1); - expect(sizzle('.chat-msg__subject', view.el).pop().textContent.trim()).toBe('This is a message subject'); - expect(sizzle('.chat-msg__text').length).toBe(1); - expect(sizzle('.chat-msg__text').pop().textContent.trim()).toBe('This is a message'); - expect(view.el.querySelector('.chat-head__desc').textContent.trim()).toBe(text); - done(); - })); - - it("can be toggled by the user", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc'); - const text = 'Jabber/XMPP Development | RFCs and Extensions: https://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org'; - let stanza = u.toStanza(` - - ${text} - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); - await new Promise(resolve => view.model.once('change:subject', resolve)); - - const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')); - expect(head_desc?.textContent.trim()).toBe(text); - - stanza = u.toStanza( - ` - This is a message subject - This is a message - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(sizzle('.chat-msg__subject', view.el).length).toBe(1); - expect(sizzle('.chat-msg__subject', view.el).pop().textContent.trim()).toBe('This is a message subject'); - expect(sizzle('.chat-msg__text').length).toBe(1); - expect(sizzle('.chat-msg__text').pop().textContent.trim()).toBe('This is a message'); - const topic_el = view.el.querySelector('.chat-head__desc'); - expect(topic_el.textContent.trim()).toBe(text); - expect(u.isVisible(topic_el)).toBe(true); - - const toggle = view.el.querySelector('.hide-topic'); - expect(toggle.textContent).toBe('Hide topic'); - toggle.click(); - await u.waitUntil(() => !u.isVisible(topic_el)); - expect(view.el.querySelector('.hide-topic').textContent).toBe('Show topic'); - done(); - })); - - it("will always be shown when it's new", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc'); - const text = 'Jabber/XMPP Development | RFCs and Extensions: https://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org'; - let stanza = u.toStanza(` - - ${text} - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); - await new Promise(resolve => view.model.once('change:subject', resolve)); - - const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')); - expect(head_desc?.textContent.trim()).toBe(text); - - let topic_el = view.el.querySelector('.chat-head__desc'); - expect(topic_el.textContent.trim()).toBe(text); - expect(u.isVisible(topic_el)).toBe(true); - - const toggle = view.el.querySelector('.hide-topic'); - expect(toggle.textContent).toBe('Hide topic'); - toggle.click(); - await u.waitUntil(() => !u.isVisible(topic_el)); - - stanza = u.toStanza(` - - Another topic - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => u.isVisible(view.el.querySelector('.chat-head__desc'))); - topic_el = view.el.querySelector('.chat-head__desc'); - expect(topic_el.textContent.trim()).toBe('Another topic'); - done(); - })); - - - it("causes an info message to be shown when received in real-time", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - spyOn(_converse.ChatRoom.prototype, 'handleSubjectChange').and.callThrough(); - await test_utils.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'romeo'); - const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); - - _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(` - - This is an older topic - - - `))); - await u.waitUntil(() => view.model.handleSubjectChange.calls.count()); - expect(sizzle('.chat-info__message', view.el).length).toBe(0); - - const desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')); - expect(desc.textContent.trim()).toBe('This is an older topic'); - - _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(` - - This is a new topic - `))); - await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 2); - - let el = sizzle('.chat-info__message', view.el).pop(); - expect(el.textContent.trim()).toBe('Topic set by ralphm'); - await u.waitUntil(() => desc.textContent.trim() === 'This is a new topic'); - - // Doesn't show multiple subsequent topic change notifications - _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(` - - Yet another topic - `))); - await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 3); - await u.waitUntil(() => desc.textContent.trim() === 'Yet another topic'); - expect(sizzle('.chat-info__message', view.el).length).toBe(1); - - // Sow multiple subsequent topic change notification from someone else - _converse.connection._dataRecv(test_utils.createRequest(u.toStanza(` - - Some1's topic - `))); - await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 4); - await u.waitUntil(() => desc.textContent.trim() === "Some1's topic"); - expect(sizzle('.chat-info__message', view.el).length).toBe(2); - el = sizzle('.chat-info__message', view.el).pop(); - expect(el.textContent.trim()).toBe('Topic set by some1'); - - // Removes current topic - const stanza = u.toStanza( - ` - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 5); - await u.waitUntil(() => view.el.querySelector('.chat-head__desc') === null); - expect(view.el.querySelector('.chat-info:last-child').textContent.trim()).toBe("Topic cleared by some1"); + await u.waitUntil(() => view.model.occupants.length > 1); + expect(view.model.occupants.length).toBe(2); + // The existing owner occupant should not have their + // affiliation removed due to the owner list + // not being returned (forbidden err). + expect(view.model.occupants.findWhere({'jid': _converse.bare_jid}).get('affiliation')).toBe('owner'); + expect(view.model.occupants.findWhere({'jid': 'hag66@shakespeare.lit'}).get('affiliation')).toBe('member'); done(); })); }); + }); + describe("topic", function () { - it("clears cached messages when it gets closed and clear_messages_on_reconnection is true", - mock.initConverse( - ['rosterGroupsFetched'], {'clear_messages_on_reconnection': true}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid , 'romeo'); - const view = _converse.chatboxviews.get(muc_jid); - const message = 'Hello world', - nick = mock.chatroom_names[0], - msg = $msg({ - 'from': 'lounge@montague.lit/'+nick, - 'id': u.getUniqueId(), - 'to': 'romeo@montague.lit', - 'type': 'groupchat' - }).c('body').t(message).tree(); - - await view.model.queueMessage(msg); - - spyOn(view.model, 'clearMessages').and.callThrough(); - await view.model.close(); - await u.waitUntil(() => view.model.clearMessages.calls.count()); - expect(view.model.messages.length).toBe(0); - expect(view.msgs_container.innerHTML).toBe(''); - done() - })); - - it("is opened when an xmpp: URI is clicked inside another groupchat", + it("is shown the header", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - if (!view.el.querySelectorAll('.chat-area').length) { - view.renderChatArea(); - } - expect(_converse.chatboxes.length).toEqual(2); - const message = 'Please go to xmpp:coven@chat.shakespeare.lit?join', - nick = mock.chatroom_names[0], - msg = $msg({ - 'from': 'lounge@montague.lit/'+nick, - 'id': u.getUniqueId(), - 'to': 'romeo@montague.lit', - 'type': 'groupchat' - }).c('body').t(message).tree(); + await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc'); + const text = 'Jabber/XMPP Development | RFCs and Extensions: https://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org'; + let stanza = u.toStanza(` + + ${text} + + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); + await new Promise(resolve => view.model.once('change:subject', resolve)); - await view.model.queueMessage(msg); - await u.waitUntil(() => view.el.querySelector('.chat-msg__text a')); - view.el.querySelector('.chat-msg__text a').click(); - await u.waitUntil(() => _converse.chatboxes.length === 3) - expect(_.includes(_converse.chatboxes.pluck('id'), 'coven@chat.shakespeare.lit')).toBe(true); - done() + const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')); + expect(head_desc?.textContent.trim()).toBe(text); + + stanza = u.toStanza( + ` + This is a message subject + This is a message + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(sizzle('.chat-msg__subject', view.el).length).toBe(1); + expect(sizzle('.chat-msg__subject', view.el).pop().textContent.trim()).toBe('This is a message subject'); + expect(sizzle('.chat-msg__text').length).toBe(1); + expect(sizzle('.chat-msg__text').pop().textContent.trim()).toBe('This is a message'); + expect(view.el.querySelector('.chat-head__desc').textContent.trim()).toBe(text); + done(); })); - it("shows a notification if it's not anonymous", + it("can be toggled by the user", mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + ['rosterGroupsFetched'], {}, async function (done, _converse) { - const muc_jid = 'coven@chat.shakespeare.lit'; - const nick = 'romeo'; - await _converse.api.rooms.open(muc_jid); - await test_utils.getRoomFeatures(_converse, muc_jid); - await test_utils.waitForReservedNick(_converse, muc_jid, nick); + await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc'); + const text = 'Jabber/XMPP Development | RFCs and Extensions: https://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org'; + let stanza = u.toStanza(` + + ${text} + + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); + await new Promise(resolve => view.model.once('change:subject', resolve)); - const view = _converse.chatboxviews.get(muc_jid); - /* - * - * - * - * - * - * - */ - const presence = $pres({ - to: 'romeo@montague.lit/orchard', - from: 'coven@chat.shakespeare.lit/some1' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'owner', - 'jid': 'romeo@montague.lit/_converse.js-29092160', - 'role': 'moderator' - }).up() - .c('status', {code: '110'}).up() - .c('status', {code: '100'}); - _converse.connection._dataRecv(test_utils.createRequest(presence)); + const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')); + expect(head_desc?.textContent.trim()).toBe(text); - const num_info_msgs = await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length); - expect(num_info_msgs).toBe(1); - expect(sizzle('div.chat-info', view.content).pop().textContent.trim()).toBe("This groupchat is not anonymous"); + stanza = u.toStanza( + ` + This is a message subject + This is a message + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(sizzle('.chat-msg__subject', view.el).length).toBe(1); + expect(sizzle('.chat-msg__subject', view.el).pop().textContent.trim()).toBe('This is a message subject'); + expect(sizzle('.chat-msg__text').length).toBe(1); + expect(sizzle('.chat-msg__text').pop().textContent.trim()).toBe('This is a message'); + const topic_el = view.el.querySelector('.chat-head__desc'); + expect(topic_el.textContent.trim()).toBe(text); + expect(u.isVisible(topic_el)).toBe(true); - const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext.trim()).toEqual("some1 has entered the groupchat"); + const toggle = view.el.querySelector('.hide-topic'); + expect(toggle.textContent).toBe('Hide topic'); + toggle.click(); + await u.waitUntil(() => !u.isVisible(topic_el)); + expect(view.el.querySelector('.hide-topic').textContent).toBe('Show topic'); + done(); + })); + + it("will always be shown when it's new", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc'); + const text = 'Jabber/XMPP Development | RFCs and Extensions: https://xmpp.org/ | Protocol and XSF discussions: xsf@muc.xmpp.org'; + let stanza = u.toStanza(` + + ${text} + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); + await new Promise(resolve => view.model.once('change:subject', resolve)); + + const head_desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')); + expect(head_desc?.textContent.trim()).toBe(text); + + let topic_el = view.el.querySelector('.chat-head__desc'); + expect(topic_el.textContent.trim()).toBe(text); + expect(u.isVisible(topic_el)).toBe(true); + + const toggle = view.el.querySelector('.hide-topic'); + expect(toggle.textContent).toBe('Hide topic'); + toggle.click(); + await u.waitUntil(() => !u.isVisible(topic_el)); + + stanza = u.toStanza(` + + Another topic + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => u.isVisible(view.el.querySelector('.chat-head__desc'))); + topic_el = view.el.querySelector('.chat-head__desc'); + expect(topic_el.textContent.trim()).toBe('Another topic'); done(); })); - it("shows join/leave messages when users enter or exit a groupchat", + it("causes an info message to be shown when received in real-time", mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_fetch_members': false}, + ['rosterGroupsFetched'], {}, async function (done, _converse) { - const muc_jid = 'coven@chat.shakespeare.lit'; - const nick = 'some1'; - const room_creation_promise = await _converse.api.rooms.open(muc_jid, {nick}); - await test_utils.getRoomFeatures(_converse, muc_jid); - const sent_stanzas = _converse.connection.sent_stanzas; - await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop()); + spyOn(_converse.ChatRoom.prototype, 'handleSubjectChange').and.callThrough(); + await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'romeo'); + const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); - const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); - await _converse.api.waitUntil('chatRoomViewInitialized'); + _converse.connection._dataRecv(mock.createRequest(u.toStanza(` + + This is an older topic + + + `))); + await u.waitUntil(() => view.model.handleSubjectChange.calls.count()); + expect(sizzle('.chat-info__message', view.el).length).toBe(0); - /* We don't show join/leave messages for existing occupants. We - * know about them because we receive their presences before we - * receive our own. - */ - let presence = $pres({ - to: 'romeo@montague.lit/_converse.js-29092160', - from: 'coven@chat.shakespeare.lit/oldguy' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'oldguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); + const desc = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')); + expect(desc.textContent.trim()).toBe('This is an older topic'); - /* - * - * - * - * - * - */ - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-29092160', - from: 'coven@chat.shakespeare.lit/some1' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'owner', - 'jid': 'romeo@montague.lit/_converse.js-29092160', - 'role': 'moderator' - }).up() - .c('status', {code: '110'}); - _converse.connection._dataRecv(test_utils.createRequest(presence)); + _converse.connection._dataRecv(mock.createRequest(u.toStanza(` + + This is a new topic + `))); + await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 2); - const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext.trim()).toEqual("some1 has entered the groupchat"); + let el = sizzle('.chat-info__message', view.el).pop(); + expect(el.textContent.trim()).toBe('Topic set by ralphm'); + await u.waitUntil(() => desc.textContent.trim() === 'This is a new topic'); - await room_creation_promise; - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); - await view.model.messages.fetched; + // Doesn't show multiple subsequent topic change notifications + _converse.connection._dataRecv(mock.createRequest(u.toStanza(` + + Yet another topic + `))); + await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 3); + await u.waitUntil(() => desc.textContent.trim() === 'Yet another topic'); + expect(sizzle('.chat-info__message', view.el).length).toBe(1); - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-29092160', - from: 'coven@chat.shakespeare.lit/newguy' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "some1 and newguy have entered the groupchat"); + // Sow multiple subsequent topic change notification from someone else + _converse.connection._dataRecv(mock.createRequest(u.toStanza(` + + Some1's topic + `))); + await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 4); + await u.waitUntil(() => desc.textContent.trim() === "Some1's topic"); + expect(sizzle('.chat-info__message', view.el).length).toBe(2); + el = sizzle('.chat-info__message', view.el).pop(); + expect(el.textContent.trim()).toBe('Topic set by some1'); - const msg = $msg({ - 'from': 'coven@chat.shakespeare.lit/some1', + // Removes current topic + const stanza = u.toStanza( + ` + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.handleSubjectChange.calls.count() === 5); + await u.waitUntil(() => view.el.querySelector('.chat-head__desc') === null); + expect(view.el.querySelector('.chat-info:last-child').textContent.trim()).toBe("Topic cleared by some1"); + done(); + })); + }); + + + it("clears cached messages when it gets closed and clear_messages_on_reconnection is true", + mock.initConverse( + ['rosterGroupsFetched'], {'clear_messages_on_reconnection': true}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid , 'romeo'); + const view = _converse.chatboxviews.get(muc_jid); + const message = 'Hello world', + nick = mock.chatroom_names[0], + msg = $msg({ + 'from': 'lounge@montague.lit/'+nick, 'id': u.getUniqueId(), 'to': 'romeo@montague.lit', 'type': 'groupchat' - }).c('body').t('hello world').tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - await new Promise(resolve => view.once('messageInserted', resolve)); + }).c('body').t(message).tree(); - // Add another entrant, otherwise the above message will be - // collapsed if "newguy" leaves immediately again - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-29092160', - from: 'coven@chat.shakespeare.lit/newgirl' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newgirl@montague.lit/_converse.js-213098781', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "some1, newguy and newgirl have entered the groupchat"); + await view.model.queueMessage(msg); - // Don't show duplicate join messages - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-290918392', - from: 'coven@chat.shakespeare.lit/newguy' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) + spyOn(view.model, 'clearMessages').and.callThrough(); + await view.model.close(); + await u.waitUntil(() => view.model.clearMessages.calls.count()); + expect(view.model.messages.length).toBe(0); + expect(view.msgs_container.innerHTML).toBe(''); + done() + })); + + it("is opened when an xmpp: URI is clicked inside another groupchat", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current'); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + if (!view.el.querySelectorAll('.chat-area').length) { + view.renderChatArea(); + } + expect(_converse.chatboxes.length).toEqual(2); + const message = 'Please go to xmpp:coven@chat.shakespeare.lit?join', + nick = mock.chatroom_names[0], + msg = $msg({ + 'from': 'lounge@montague.lit/'+nick, + 'id': u.getUniqueId(), + 'to': 'romeo@montague.lit', + 'type': 'groupchat' + }).c('body').t(message).tree(); + + await view.model.queueMessage(msg); + await u.waitUntil(() => view.el.querySelector('.chat-msg__text a')); + view.el.querySelector('.chat-msg__text a').click(); + await u.waitUntil(() => _converse.chatboxes.length === 3) + expect(_.includes(_converse.chatboxes.pluck('id'), 'coven@chat.shakespeare.lit')).toBe(true); + done() + })); + + it("shows a notification if it's not anonymous", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'coven@chat.shakespeare.lit'; + const nick = 'romeo'; + await _converse.api.rooms.open(muc_jid); + await mock.getRoomFeatures(_converse, muc_jid); + await mock.waitForReservedNick(_converse, muc_jid, nick); + + const view = _converse.chatboxviews.get(muc_jid); + /* + * + * + * + * + * + * + */ + const presence = $pres({ + to: 'romeo@montague.lit/orchard', + from: 'coven@chat.shakespeare.lit/some1' + }).c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'owner', + 'jid': 'romeo@montague.lit/_converse.js-29092160', + 'role': 'moderator' + }).up() + .c('status', {code: '110'}).up() + .c('status', {code: '100'}); + _converse.connection._dataRecv(mock.createRequest(presence)); + + const num_info_msgs = await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length); + expect(num_info_msgs).toBe(1); + expect(sizzle('div.chat-info', view.content).pop().textContent.trim()).toBe("This groupchat is not anonymous"); + + const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext.trim()).toEqual("some1 has entered the groupchat"); + done(); + })); + + + it("shows join/leave messages when users enter or exit a groupchat", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_fetch_members': false}, + async function (done, _converse) { + + const muc_jid = 'coven@chat.shakespeare.lit'; + const nick = 'some1'; + const room_creation_promise = await _converse.api.rooms.open(muc_jid, {nick}); + await mock.getRoomFeatures(_converse, muc_jid); + const sent_stanzas = _converse.connection.sent_stanzas; + await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop()); + + const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); + await _converse.api.waitUntil('chatRoomViewInitialized'); + + /* We don't show join/leave messages for existing occupants. We + * know about them because we receive their presences before we + * receive our own. + */ + let presence = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + from: 'coven@chat.shakespeare.lit/oldguy' + }).c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'oldguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + + /* + * + * + * + * + * + */ + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + from: 'coven@chat.shakespeare.lit/some1' + }).c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'owner', + 'jid': 'romeo@montague.lit/_converse.js-29092160', + 'role': 'moderator' + }).up() + .c('status', {code: '110'}); + _converse.connection._dataRecv(mock.createRequest(presence)); + + const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext.trim()).toEqual("some1 has entered the groupchat"); + + await room_creation_promise; + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); + await view.model.messages.fetched; + + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + from: 'coven@chat.shakespeare.lit/newguy' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === + "some1 and newguy have entered the groupchat"); + + const msg = $msg({ + 'from': 'coven@chat.shakespeare.lit/some1', + 'id': u.getUniqueId(), + 'to': 'romeo@montague.lit', + 'type': 'groupchat' + }).c('body').t('hello world').tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); + await new Promise(resolve => view.once('messageInserted', resolve)); + + // Add another entrant, otherwise the above message will be + // collapsed if "newguy" leaves immediately again + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + from: 'coven@chat.shakespeare.lit/newgirl' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newgirl@montague.lit/_converse.js-213098781', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === + "some1, newguy and newgirl have entered the groupchat"); + + // Don't show duplicate join messages + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-290918392', + from: 'coven@chat.shakespeare.lit/newguy' + }).c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + + /* + * Disconnected: Replaced by new connection + * + * + * + * + */ + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + type: 'unavailable', + from: 'coven@chat.shakespeare.lit/newguy' + }) + .c('status', 'Disconnected: Replaced by new connection').up() + .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - /* - * Disconnected: Replaced by new connection - * - * - * - * - */ - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-29092160', - type: 'unavailable', - from: 'coven@chat.shakespeare.lit/newguy' - }) - .c('status', 'Disconnected: Replaced by new connection').up() - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'none' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "some1 and newgirl have entered the groupchat\n newguy has left the groupchat"); - - // When the user immediately joins again, we collapse the - // multiple join/leave messages. - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-29092160', - from: 'coven@chat.shakespeare.lit/newguy' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "some1, newgirl and newguy have entered the groupchat"); - - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-29092160', - type: 'unavailable', - from: 'coven@chat.shakespeare.lit/newguy' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'none' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "some1 and newgirl have entered the groupchat\n newguy has left the groupchat"); - - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-29092160', - from: 'coven@chat.shakespeare.lit/nomorenicks' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'nomorenicks@montague.lit/_converse.js-290929789', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "some1, newgirl and nomorenicks have entered the groupchat\n newguy has left the groupchat"); - - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-290918392', - type: 'unavailable', - from: 'coven@chat.shakespeare.lit/nomorenicks' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'nomorenicks@montague.lit/_converse.js-290929789', 'role': 'none' }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "some1 and newgirl have entered the groupchat\n newguy and nomorenicks have left the groupchat"); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === + "some1 and newgirl have entered the groupchat\n newguy has left the groupchat"); - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-29092160', - from: 'coven@chat.shakespeare.lit/nomorenicks' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) + // When the user immediately joins again, we collapse the + // multiple join/leave messages. + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + from: 'coven@chat.shakespeare.lit/newguy' + }).c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === + "some1, newgirl and newguy have entered the groupchat"); + + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + type: 'unavailable', + from: 'coven@chat.shakespeare.lit/newguy' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', - 'jid': 'nomorenicks@montague.lit/_converse.js-290929789', - 'role': 'participant' + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'none' }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "some1, newgirl and nomorenicks have entered the groupchat\n newguy has left the groupchat"); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === + "some1 and newgirl have entered the groupchat\n newguy has left the groupchat"); - // Test a member joining and leaving - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-290918392', - from: 'coven@chat.shakespeare.lit/insider' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + from: 'coven@chat.shakespeare.lit/nomorenicks' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'nomorenicks@montague.lit/_converse.js-290929789', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === + "some1, newgirl and nomorenicks have entered the groupchat\n newguy has left the groupchat"); + + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-290918392', + type: 'unavailable', + from: 'coven@chat.shakespeare.lit/nomorenicks' + }).c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'nomorenicks@montague.lit/_converse.js-290929789', + 'role': 'none' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === + "some1 and newgirl have entered the groupchat\n newguy and nomorenicks have left the groupchat"); + + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + from: 'coven@chat.shakespeare.lit/nomorenicks' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'nomorenicks@montague.lit/_converse.js-290929789', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === + "some1, newgirl and nomorenicks have entered the groupchat\n newguy has left the groupchat"); + + // Test a member joining and leaving + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-290918392', + from: 'coven@chat.shakespeare.lit/insider' + }).c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'member', + 'jid': 'insider@montague.lit/_converse.js-290929789', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + + /* + * Disconnected: Replaced by new connection + * + * + * + * + */ + presence = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + type: 'unavailable', + from: 'coven@chat.shakespeare.lit/insider' + }) + .c('status', 'Disconnected: Replaced by new connection').up() + .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'member', 'jid': 'insider@montague.lit/_converse.js-290929789', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - /* - * Disconnected: Replaced by new connection - * - * - * - * - */ - presence = $pres({ - to: 'romeo@montague.lit/_converse.js-29092160', - type: 'unavailable', - from: 'coven@chat.shakespeare.lit/insider' - }) - .c('status', 'Disconnected: Replaced by new connection').up() - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'member', - 'jid': 'insider@montague.lit/_converse.js-290929789', - 'role': 'none' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "some1, newgirl and nomorenicks have entered the groupchat\n newguy and insider have left the groupchat"); - - expect(view.model.occupants.length).toBe(5); - expect(view.model.occupants.findWhere({'jid': 'insider@montague.lit'}).get('show')).toBe('offline'); - - // New girl leaves - presence = $pres({ - 'to': 'romeo@montague.lit/_converse.js-29092160', - 'type': 'unavailable', - 'from': 'coven@chat.shakespeare.lit/newgirl' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newgirl@montague.lit/_converse.js-213098781', 'role': 'none' }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === + "some1, newgirl and nomorenicks have entered the groupchat\n newguy and insider have left the groupchat"); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "some1 and nomorenicks have entered the groupchat\n newguy, insider and newgirl have left the groupchat"); - expect(view.model.occupants.length).toBe(4); - done(); - })); + expect(view.model.occupants.length).toBe(5); + expect(view.model.occupants.findWhere({'jid': 'insider@montague.lit'}).get('show')).toBe('offline'); - it("combines subsequent join/leave messages when users enter or exit a groupchat", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'romeo') - const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === "romeo has entered the groupchat"); - - let presence = u.toStanza( - ` - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === "romeo and fabio have entered the groupchat"); - - presence = u.toStanza( - ` - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === "romeo, fabio and Dele Olajide have entered the groupchat"); - - presence = u.toStanza( - ` - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === "romeo, fabio and others have entered the groupchat"); - - presence = u.toStanza( - ` - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "romeo, fabio and jcbrand have entered the groupchat\n Dele Olajide has left the groupchat"); - - presence = u.toStanza( - ` - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "romeo, fabio and others have entered the groupchat"); - - presence = u.toStanza( - ` - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "romeo, fabio and others have entered the groupchat"); - - presence = u.toStanza( - ` - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "romeo, fabio and others have entered the groupchat\n fuvuv has left the groupchat"); - - presence = u.toStanza( - ` - Disconnected: Replaced by new connection - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "romeo, jcbrand and Dele Olajide have entered the groupchat\n fuvuv and fabio have left the groupchat"); - - presence = u.toStanza( - ` - - Ready for a new day - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "romeo, jcbrand and others have entered the groupchat\n fuvuv has left the groupchat"); - - // XXX: hack so that we can test leave/enter of occupants - // who were already in the room when we joined. - view.msgs_container.innerHTML = ''; - - presence = u.toStanza( - ` - Disconnected: closed - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "romeo, jcbrand and Dele Olajide have entered the groupchat\n fuvuv and fabio have left the groupchat"); - - presence = u.toStanza( - ` - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "romeo and jcbrand have entered the groupchat\n fuvuv, fabio and Dele Olajide have left the groupchat"); - - presence = u.toStanza( - ` - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === - "romeo, jcbrand and fabio have entered the groupchat\n fuvuv and Dele Olajide have left the groupchat"); - - expect(1).toBe(1); - done(); - })); - - it("doesn't show the disconnection messages when muc_show_join_leave is false", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_show_join_leave': false}, - async function (done, _converse) { - - spyOn(_converse.ChatRoom.prototype, 'onOccupantAdded').and.callThrough(); - spyOn(_converse.ChatRoom.prototype, 'onOccupantRemoved').and.callThrough(); - await test_utils.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'some1'); - const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); - let presence = $pres({ - to: 'romeo@montague.lit/orchard', - from: 'coven@chat.shakespeare.lit/newguy' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.model.onOccupantAdded.calls.count() === 2); - expect(view.model.notifications.get('entered')).toBeFalsy(); - expect(view.el.querySelector('.chat-content__notifications').textContent.trim()).toBe(''); - await test_utils.sendMessage(view, 'hello world'); - - presence = u.toStanza( - ` - Gotta go! - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - await u.waitUntil(() => view.model.onOccupantRemoved.calls.count()); - expect(view.model.onOccupantRemoved.calls.count()).toBe(1); - expect(view.model.notifications.get('entered')).toBeFalsy(); - await test_utils.sendMessage(view, 'hello world'); - expect(view.el.querySelector('.chat-content__notifications').textContent.trim()).toBe(''); - done(); - })); - - it("role-change messages that follow a MUC leave are left out", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - // See https://github.com/conversejs/converse.js/issues/1259 - - await test_utils.openAndEnterChatRoom(_converse, 'conversations@conference.siacs.eu', 'romeo'); - - const presence = $pres({ - to: 'romeo@montague.lit/orchard', - from: 'conversations@conference.siacs.eu/Guus' - }).c('x', { - 'xmlns': Strophe.NS.MUC_USER - }).c('item', { - 'affiliation': 'none', - 'jid': 'Guus@montague.lit/xxx', - 'role': 'visitor' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - const view = _converse.chatboxviews.get('conversations@conference.siacs.eu'); - const msg = $msg({ - 'from': 'conversations@conference.siacs.eu/romeo', - 'id': u.getUniqueId(), - 'to': 'romeo@montague.lit', - 'type': 'groupchat' - }).c('body').t('Some message').tree(); - - await view.model.queueMessage(msg); - await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.content).pop()); - - let stanza = u.toStanza( - ` - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - stanza = u.toStanza( - ` - - - bf987c486c51fbc05a6a4a9f20dd19b5efba3758 - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() - === "romeo and Guus have entered the groupchat"); - expect(1).toBe(1); - done(); - })); - - it("supports the /me command", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); - await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - if (!view.el.querySelectorAll('.chat-area').length) { - view.renderChatArea(); - } - let message = '/me is tired'; - const nick = mock.chatroom_names[0]; - let msg = $msg({ - 'from': 'lounge@montague.lit/'+nick, - 'id': u.getUniqueId(), - 'to': 'romeo@montague.lit', - 'type': 'groupchat' - }).c('body').t(message).tree(); - await view.model.queueMessage(msg); - await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.content).pop()); - expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, '**Dyon van de Wege')).toBeTruthy(); - expect(view.el.querySelector('.chat-msg__text').textContent.trim()).toBe('is tired'); - - message = '/me is as well'; - msg = $msg({ - from: 'lounge@montague.lit/Romeo Montague', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t(message).tree(); - await view.model.queueMessage(msg); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2); - expect(sizzle('.chat-msg__author:last', view.el).pop().textContent.includes('**Romeo Montague')).toBeTruthy(); - expect(sizzle('.chat-msg__text:last', view.el).pop().textContent.trim()).toBe('is as well'); - done(); - })); - - it("can be configured if you're its owner", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - let sent_IQ, IQ_id; - const sendIQ = _converse.connection.sendIQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - sent_IQ = iq; - IQ_id = sendIQ.bind(this)(iq, callback, errback); + // New girl leaves + presence = $pres({ + 'to': 'romeo@montague.lit/_converse.js-29092160', + 'type': 'unavailable', + 'from': 'coven@chat.shakespeare.lit/newgirl' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newgirl@montague.lit/_converse.js-213098781', + 'role': 'none' }); - await _converse.api.rooms.open('coven@chat.shakespeare.lit', {'nick': 'some1'}); - const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); - await u.waitUntil(() => u.isVisible(view.el)); - // We pretend this is a new room, so no disco info is returned. - const features_stanza = $iq({ - from: 'coven@chat.shakespeare.lit', - 'id': IQ_id, - 'to': 'romeo@montague.lit/desktop', - 'type': 'error' - }).c('error', {'type': 'cancel'}) - .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === + "some1 and nomorenicks have entered the groupchat\n newguy, insider and newgirl have left the groupchat"); + expect(view.model.occupants.length).toBe(4); + done(); + })); - /* - * - * - * - * - *
This message contains some markup