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 `) + _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 - - - - - 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 + + + + + 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(); + })); - /* - * - * - * - * - * - */ - const 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)); - await u.waitUntil(() => view.el.querySelector('.configure-chatroom-button') !== null); - view.el.querySelector('.configure-chatroom-button').click(); + it("combines subsequent join/leave messages when users enter or exit a groupchat", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - /* Check that an IQ is sent out, asking for the - * configuration form. - * See: // https://xmpp.org/extensions/xep-0045.html#example-163 - * - * - * - * - */ - expect(sent_IQ.toLocaleString()).toBe( - ``+ - ``+ - ``); + await mock.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"); - /* Server responds with the configuration form. - * See: // https://xmpp.org/extensions/xep-0045.html#example-165 - */ - const config_stanza = $iq({from: 'coven@chat.shakespeare.lit', + let presence = u.toStanza( + ` + + + + + `); + _converse.connection._dataRecv(mock.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(mock.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(mock.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(mock.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(mock.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(mock.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(mock.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(mock.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(mock.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(mock.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(mock.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(mock.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 mock.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(mock.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 mock.sendMessage(view, 'hello world'); + + presence = u.toStanza( + ` + Gotta go! + + + + `); + _converse.connection._dataRecv(mock.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 mock.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 mock.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(mock.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(mock.createRequest(stanza)); + + stanza = u.toStanza( + ` + + + bf987c486c51fbc05a6a4a9f20dd19b5efba3758 + + + + + `); + _converse.connection._dataRecv(mock.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 mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); + await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname')); + 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(); + } + 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); + }); + + 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': 'result'}) - .c('query', { 'xmlns': 'http://jabber.org/protocol/muc#owner'}) - .c('x', { 'xmlns': 'jabber:x:data', 'type': 'form'}) - .c('title').t('Configuration for "coven" Room').up() - .c('instructions').t('Complete this form to modify the configuration of your room.').up() - .c('field', {'type': 'hidden', 'var': 'FORM_TYPE'}) - .c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up() - .c('field', { - 'label': 'Natural-Language Room Name', - 'type': 'text-single', - 'var': 'muc#roomconfig_roomname'}) - .c('value').t('A Dark Cave').up().up() - .c('field', { - 'label': 'Short Description of Room', - 'type': 'text-single', - 'var': 'muc#roomconfig_roomdesc'}) - .c('value').t('The place for all good witches!').up().up() - .c('field', { - 'label': 'Enable Public Logging?', - 'type': 'boolean', - 'var': 'muc#roomconfig_enablelogging'}) - .c('value').t(0).up().up() - .c('field', { - 'label': 'Allow Occupants to Change Subject?', - 'type': 'boolean', - 'var': 'muc#roomconfig_changesubject'}) - .c('value').t(0).up().up() - .c('field', { - 'label': 'Allow Occupants to Invite Others?', - 'type': 'boolean', - 'var': 'muc#roomconfig_allowinvites'}) - .c('value').t(0).up().up() - .c('field', { - 'label': 'Who Can Send Private Messages?', - 'type': 'list-single', - 'var': 'muc#roomconfig_allowpm'}) - .c('value').t('anyone').up() - .c('option', {'label': 'Anyone'}) - .c('value').t('anyone').up().up() - .c('option', {'label': 'Anyone with Voice'}) - .c('value').t('participants').up().up() - .c('option', {'label': 'Moderators Only'}) - .c('value').t('moderators').up().up() - .c('option', {'label': 'Nobody'}) - .c('value').t('none').up().up().up() - .c('field', { - 'label': 'Roles for which Presence is Broadcasted', - 'type': 'list-multi', - 'var': 'muc#roomconfig_presencebroadcast'}) - .c('value').t('moderator').up() - .c('value').t('participant').up() - .c('value').t('visitor').up() - .c('option', {'label': 'Moderator'}) - .c('value').t('moderator').up().up() - .c('option', {'label': 'Participant'}) - .c('value').t('participant').up().up() - .c('option', {'label': 'Visitor'}) - .c('value').t('visitor').up().up().up() - .c('field', { - 'label': 'Roles and Affiliations that May Retrieve Member List', - 'type': 'list-multi', - 'var': 'muc#roomconfig_getmemberlist'}) - .c('value').t('moderator').up() - .c('value').t('participant').up() - .c('value').t('visitor').up() - .c('option', {'label': 'Moderator'}) - .c('value').t('moderator').up().up() - .c('option', {'label': 'Participant'}) - .c('value').t('participant').up().up() - .c('option', {'label': 'Visitor'}) - .c('value').t('visitor').up().up().up() - .c('field', { - 'label': 'Make Room Publicly Searchable?', - 'type': 'boolean', - 'var': 'muc#roomconfig_publicroom'}) - .c('value').t(0).up().up() - .c('field', { - 'label': 'Make Room Publicly Searchable?', - 'type': 'boolean', - 'var': 'muc#roomconfig_publicroom'}) - .c('value').t(0).up().up() - .c('field', { - 'label': 'Make Room Persistent?', - 'type': 'boolean', - 'var': 'muc#roomconfig_persistentroom'}) - .c('value').t(0).up().up() - .c('field', { - 'label': 'Make Room Moderated?', - 'type': 'boolean', - 'var': 'muc#roomconfig_moderatedroom'}) - .c('value').t(0).up().up() - .c('field', { - 'label': 'Make Room Members Only?', - 'type': 'boolean', - 'var': 'muc#roomconfig_membersonly'}) - .c('value').t(0).up().up() - .c('field', { - 'label': 'Password Required for Entry?', - 'type': 'boolean', - 'var': 'muc#roomconfig_passwordprotectedroom'}) - .c('value').t(1).up().up() - .c('field', {'type': 'fixed'}) - .c('value').t( - 'If a password is required to enter this groupchat, you must specify the password below.' - ).up().up() - .c('field', { - 'label': 'Password', - 'type': 'text-private', - 'var': 'muc#roomconfig_roomsecret'}) - .c('value').t('cauldronburn'); - _converse.connection._dataRecv(test_utils.createRequest(config_stanza)); + '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 form = await u.waitUntil(() => view.el.querySelector('.muc-config-form')); - expect(form.querySelectorAll('fieldset').length).toBe(2); - const membersonly = view.el.querySelectorAll('input[name="muc#roomconfig_membersonly"]'); - expect(membersonly.length).toBe(1); - expect(membersonly[0].getAttribute('type')).toBe('checkbox'); - membersonly[0].checked = true; + /* + * + * + * + * + * + */ + const 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)); + await u.waitUntil(() => view.el.querySelector('.configure-chatroom-button') !== null); + view.el.querySelector('.configure-chatroom-button').click(); - const moderated = view.el.querySelectorAll('input[name="muc#roomconfig_moderatedroom"]'); - expect(moderated.length).toBe(1); - expect(moderated[0].getAttribute('type')).toBe('checkbox'); - moderated[0].checked = true; + /* Check that an IQ is sent out, asking for the + * configuration form. + * See: // https://xmpp.org/extensions/xep-0045.html#example-163 + * + * + * + * + */ + expect(sent_IQ.toLocaleString()).toBe( + ``+ + ``+ + ``); - const password = view.el.querySelectorAll('input[name="muc#roomconfig_roomsecret"]'); - expect(password.length).toBe(1); - expect(password[0].getAttribute('type')).toBe('password'); + /* Server responds with the configuration form. + * See: // https://xmpp.org/extensions/xep-0045.html#example-165 + */ + const config_stanza = $iq({from: 'coven@chat.shakespeare.lit', + 'id': IQ_id, + 'to': 'romeo@montague.lit/desktop', + 'type': 'result'}) + .c('query', { 'xmlns': 'http://jabber.org/protocol/muc#owner'}) + .c('x', { 'xmlns': 'jabber:x:data', 'type': 'form'}) + .c('title').t('Configuration for "coven" Room').up() + .c('instructions').t('Complete this form to modify the configuration of your room.').up() + .c('field', {'type': 'hidden', 'var': 'FORM_TYPE'}) + .c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up() + .c('field', { + 'label': 'Natural-Language Room Name', + 'type': 'text-single', + 'var': 'muc#roomconfig_roomname'}) + .c('value').t('A Dark Cave').up().up() + .c('field', { + 'label': 'Short Description of Room', + 'type': 'text-single', + 'var': 'muc#roomconfig_roomdesc'}) + .c('value').t('The place for all good witches!').up().up() + .c('field', { + 'label': 'Enable Public Logging?', + 'type': 'boolean', + 'var': 'muc#roomconfig_enablelogging'}) + .c('value').t(0).up().up() + .c('field', { + 'label': 'Allow Occupants to Change Subject?', + 'type': 'boolean', + 'var': 'muc#roomconfig_changesubject'}) + .c('value').t(0).up().up() + .c('field', { + 'label': 'Allow Occupants to Invite Others?', + 'type': 'boolean', + 'var': 'muc#roomconfig_allowinvites'}) + .c('value').t(0).up().up() + .c('field', { + 'label': 'Who Can Send Private Messages?', + 'type': 'list-single', + 'var': 'muc#roomconfig_allowpm'}) + .c('value').t('anyone').up() + .c('option', {'label': 'Anyone'}) + .c('value').t('anyone').up().up() + .c('option', {'label': 'Anyone with Voice'}) + .c('value').t('participants').up().up() + .c('option', {'label': 'Moderators Only'}) + .c('value').t('moderators').up().up() + .c('option', {'label': 'Nobody'}) + .c('value').t('none').up().up().up() + .c('field', { + 'label': 'Roles for which Presence is Broadcasted', + 'type': 'list-multi', + 'var': 'muc#roomconfig_presencebroadcast'}) + .c('value').t('moderator').up() + .c('value').t('participant').up() + .c('value').t('visitor').up() + .c('option', {'label': 'Moderator'}) + .c('value').t('moderator').up().up() + .c('option', {'label': 'Participant'}) + .c('value').t('participant').up().up() + .c('option', {'label': 'Visitor'}) + .c('value').t('visitor').up().up().up() + .c('field', { + 'label': 'Roles and Affiliations that May Retrieve Member List', + 'type': 'list-multi', + 'var': 'muc#roomconfig_getmemberlist'}) + .c('value').t('moderator').up() + .c('value').t('participant').up() + .c('value').t('visitor').up() + .c('option', {'label': 'Moderator'}) + .c('value').t('moderator').up().up() + .c('option', {'label': 'Participant'}) + .c('value').t('participant').up().up() + .c('option', {'label': 'Visitor'}) + .c('value').t('visitor').up().up().up() + .c('field', { + 'label': 'Make Room Publicly Searchable?', + 'type': 'boolean', + 'var': 'muc#roomconfig_publicroom'}) + .c('value').t(0).up().up() + .c('field', { + 'label': 'Make Room Publicly Searchable?', + 'type': 'boolean', + 'var': 'muc#roomconfig_publicroom'}) + .c('value').t(0).up().up() + .c('field', { + 'label': 'Make Room Persistent?', + 'type': 'boolean', + 'var': 'muc#roomconfig_persistentroom'}) + .c('value').t(0).up().up() + .c('field', { + 'label': 'Make Room Moderated?', + 'type': 'boolean', + 'var': 'muc#roomconfig_moderatedroom'}) + .c('value').t(0).up().up() + .c('field', { + 'label': 'Make Room Members Only?', + 'type': 'boolean', + 'var': 'muc#roomconfig_membersonly'}) + .c('value').t(0).up().up() + .c('field', { + 'label': 'Password Required for Entry?', + 'type': 'boolean', + 'var': 'muc#roomconfig_passwordprotectedroom'}) + .c('value').t(1).up().up() + .c('field', {'type': 'fixed'}) + .c('value').t( + 'If a password is required to enter this groupchat, you must specify the password below.' + ).up().up() + .c('field', { + 'label': 'Password', + 'type': 'text-private', + 'var': 'muc#roomconfig_roomsecret'}) + .c('value').t('cauldronburn'); + _converse.connection._dataRecv(mock.createRequest(config_stanza)); - const allowpm = view.el.querySelectorAll('select[name="muc#roomconfig_allowpm"]'); - expect(allowpm.length).toBe(1); - allowpm[0].value = 'moderators'; + const form = await u.waitUntil(() => view.el.querySelector('.muc-config-form')); + expect(form.querySelectorAll('fieldset').length).toBe(2); + const membersonly = view.el.querySelectorAll('input[name="muc#roomconfig_membersonly"]'); + expect(membersonly.length).toBe(1); + expect(membersonly[0].getAttribute('type')).toBe('checkbox'); + membersonly[0].checked = true; - const presencebroadcast = view.el.querySelectorAll('select[name="muc#roomconfig_presencebroadcast"]'); - expect(presencebroadcast.length).toBe(1); - presencebroadcast[0].value = ['moderator']; + const moderated = view.el.querySelectorAll('input[name="muc#roomconfig_moderatedroom"]'); + expect(moderated.length).toBe(1); + expect(moderated[0].getAttribute('type')).toBe('checkbox'); + moderated[0].checked = true; - view.el.querySelector('.chatroom-form input[type="submit"]').click(); + const password = view.el.querySelectorAll('input[name="muc#roomconfig_roomsecret"]'); + expect(password.length).toBe(1); + expect(password[0].getAttribute('type')).toBe('password'); - const sent_stanza = sent_IQ.nodeTree; - expect(sent_stanza.querySelector('field[var="muc#roomconfig_membersonly"] value').textContent.trim()).toBe('1'); - expect(sent_stanza.querySelector('field[var="muc#roomconfig_moderatedroom"] value').textContent.trim()).toBe('1'); - expect(sent_stanza.querySelector('field[var="muc#roomconfig_allowpm"] value').textContent.trim()).toBe('moderators'); - expect(sent_stanza.querySelector('field[var="muc#roomconfig_presencebroadcast"] value').textContent.trim()).toBe('moderator'); - done(); - })); + const allowpm = view.el.querySelectorAll('select[name="muc#roomconfig_allowpm"]'); + expect(allowpm.length).toBe(1); + allowpm[0].value = 'moderators'; - it("shows all members even if they're not currently present in the groupchat", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + const presencebroadcast = view.el.querySelectorAll('select[name="muc#roomconfig_presencebroadcast"]'); + expect(presencebroadcast.length).toBe(1); + presencebroadcast[0].value = ['moderator']; - const muc_jid = 'lounge@montague.lit' + view.el.querySelector('.chatroom-form input[type="submit"]').click(); - const members = [{ - 'nick': 'juliet', - 'jid': 'juliet@capulet.lit', - '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 === 2); + const sent_stanza = sent_IQ.nodeTree; + expect(sent_stanza.querySelector('field[var="muc#roomconfig_membersonly"] value').textContent.trim()).toBe('1'); + expect(sent_stanza.querySelector('field[var="muc#roomconfig_moderatedroom"] value').textContent.trim()).toBe('1'); + expect(sent_stanza.querySelector('field[var="muc#roomconfig_allowpm"] value').textContent.trim()).toBe('moderators'); + expect(sent_stanza.querySelector('field[var="muc#roomconfig_presencebroadcast"] value').textContent.trim()).toBe('moderator'); + done(); + })); - const occupants = view.el.querySelector('.occupant-list'); - for (let i=0; i occupants.querySelectorAll('li').length > 2, 500); - expect(occupants.querySelectorAll('li').length).toBe(2+mock.chatroom_names.length); - expect(view.model.occupants.length).toBe(2+mock.chatroom_names.length); + const muc_jid = 'lounge@montague.lit' - mock.chatroom_names.forEach(name => { - const model = view.model.occupants.findWhere({'nick': name}); - const index = view.model.occupants.indexOf(model); - expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name); - }); + const members = [{ + 'nick': 'juliet', + 'jid': 'juliet@capulet.lit', + 'affiliation': 'member' + }]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members); + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => view.model.occupants.length === 2); - // Test users leaving the groupchat - // https://xmpp.org/extensions/xep-0045.html#exit - for (let i=mock.chatroom_names.length-1; i>-1; i--) { - const name = mock.chatroom_names[i]; - // See example 21 https://xmpp.org/extensions/xep-0045.html#enter-pres - const presence = $pres({ - to:'romeo@montague.lit/pda', - from:'lounge@montague.lit/'+name, - type: 'unavailable' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - affiliation: mock.chatroom_roles[name].affiliation, - jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', - role: 'none' - }).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(occupants.querySelectorAll('li').length).toBe(8); - } + const occupants = view.el.querySelector('.occupant-list'); + for (let i=0; i occupants.querySelectorAll('li').length > 8, 500); - expect(occupants.querySelectorAll('li').length).toBe(9); - expect(view.model.occupants.length).toBe(9); - expect(view.model.occupants.filter(o => o.isMember()).length).toBe(8); + _converse.connection._dataRecv(mock.createRequest(presence)); + } - view.model.rejoin(); - // Test that members aren't removed when we reconnect - expect(view.model.occupants.length).toBe(8); + await u.waitUntil(() => occupants.querySelectorAll('li').length > 2, 500); + expect(occupants.querySelectorAll('li').length).toBe(2+mock.chatroom_names.length); + expect(view.model.occupants.length).toBe(2+mock.chatroom_names.length); + + mock.chatroom_names.forEach(name => { + const model = view.model.occupants.findWhere({'nick': name}); + const index = view.model.occupants.indexOf(model); + expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name); + }); + + // Test users leaving the groupchat + // https://xmpp.org/extensions/xep-0045.html#exit + for (let i=mock.chatroom_names.length-1; i>-1; i--) { + const name = mock.chatroom_names[i]; + // See example 21 https://xmpp.org/extensions/xep-0045.html#enter-pres + const presence = $pres({ + to:'romeo@montague.lit/pda', + from:'lounge@montague.lit/'+name, + type: 'unavailable' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: mock.chatroom_roles[name].affiliation, + jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', + role: 'none' + }).nodeTree; + _converse.connection._dataRecv(mock.createRequest(presence)); expect(occupants.querySelectorAll('li').length).toBe(8); - done(); - })); + } + const presence = $pres({ + to: 'romeo@montague.lit/pda', + from: 'lounge@montague.lit/nonmember' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: null, + jid: 'servant@montague.lit', + role: 'visitor' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => occupants.querySelectorAll('li').length > 8, 500); + expect(occupants.querySelectorAll('li').length).toBe(9); + expect(view.model.occupants.length).toBe(9); + expect(view.model.occupants.filter(o => o.isMember()).length).toBe(8); - it("shows users currently present in the groupchat", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + view.model.rejoin(); + // Test that members aren't removed when we reconnect + expect(view.model.occupants.length).toBe(8); + expect(occupants.querySelectorAll('li').length).toBe(8); + done(); + })); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - var view = _converse.chatboxviews.get('lounge@montague.lit'), - occupants = view.el.querySelector('.occupant-list'); - var presence; - for (var i=0; i occupants.querySelectorAll('li').length > 1, 500); - expect(occupants.querySelectorAll('li').length).toBe(1+mock.chatroom_names.length); - - mock.chatroom_names.forEach(name => { - const model = view.model.occupants.findWhere({'nick': name}); - const index = view.model.occupants.indexOf(model); - expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name); - }); - - // Test users leaving the groupchat - // https://xmpp.org/extensions/xep-0045.html#exit - for (i=mock.chatroom_names.length-1; i>-1; i--) { - const name = mock.chatroom_names[i]; - // See example 21 https://xmpp.org/extensions/xep-0045.html#enter-pres - presence = $pres({ + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + var view = _converse.chatboxviews.get('lounge@montague.lit'), + occupants = view.el.querySelector('.occupant-list'); + var presence; + for (var i=0; i view.el.querySelectorAll('.occupant-list li').length, 500); - let occupants = view.el.querySelectorAll('.occupant-list li'); - expect(occupants.length).toBe(1); - expect(occupants[0].querySelector('.occupant-nick').textContent.trim()).toBe("romeo"); - expect(occupants[0].querySelectorAll('.badge').length).toBe(2); - expect(occupants[0].querySelectorAll('.badge')[0].textContent.trim()).toBe('Owner'); - expect(sizzle('.badge:last', occupants[0]).pop().textContent.trim()).toBe('Moderator'); - - var presence = $pres({ - to:'romeo@montague.lit/pda', - from:'lounge@montague.lit/moderatorman' + from:'lounge@montague.lit/'+name }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) .c('item').attrs({ - affiliation: 'admin', - jid: contact_jid, - role: 'moderator', + affiliation: 'none', + jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', + role: role }).up() .c('status').attrs({code:'110'}).nodeTree; + _converse.connection._dataRecv(mock.createRequest(presence)); + } - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelectorAll('.occupant-list li').length > 1, 500); - occupants = view.el.querySelectorAll('.occupant-list li'); - expect(occupants.length).toBe(2); - expect(occupants[0].querySelector('.occupant-nick').textContent.trim()).toBe("moderatorman"); - expect(occupants[1].querySelector('.occupant-nick').textContent.trim()).toBe("romeo"); - expect(occupants[0].querySelectorAll('.badge').length).toBe(2); - expect(occupants[0].querySelectorAll('.badge')[0].textContent.trim()).toBe('Admin'); - expect(occupants[0].querySelectorAll('.badge')[1].textContent.trim()).toBe('Moderator'); + await u.waitUntil(() => occupants.querySelectorAll('li').length > 1, 500); + expect(occupants.querySelectorAll('li').length).toBe(1+mock.chatroom_names.length); - expect(occupants[0].getAttribute('title')).toBe( - contact_jid + ' This user is a moderator. Click to mention moderatorman in your message.' - ); + mock.chatroom_names.forEach(name => { + const model = view.model.occupants.findWhere({'nick': name}); + const index = view.model.occupants.indexOf(model); + expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name); + }); - contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + // Test users leaving the groupchat + // https://xmpp.org/extensions/xep-0045.html#exit + for (i=mock.chatroom_names.length-1; i>-1; i--) { + const name = mock.chatroom_names[i]; + // See example 21 https://xmpp.org/extensions/xep-0045.html#enter-pres presence = $pres({ to:'romeo@montague.lit/pda', - from:'lounge@montague.lit/visitorwoman' + from:'lounge@montague.lit/'+name, + type: 'unavailable' }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) .c('item').attrs({ - jid: contact_jid, - role: 'visitor', - }).up() - .c('status').attrs({code:'110'}).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(presence)); + affiliation: mock.chatroom_roles[name].affiliation, + jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', + role: 'none' + }).nodeTree; + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(occupants.querySelectorAll('li').length).toBe(i+1); + } + done(); + })); - await u.waitUntil(() => view.el.querySelectorAll('.occupant-list li').length > 2, 500); - occupants = view.el.querySelector('.occupant-list').querySelectorAll('li'); - expect(occupants.length).toBe(3); - expect(occupants[2].querySelector('.occupant-nick').textContent.trim()).toBe("visitorwoman"); - expect(occupants[2].querySelectorAll('.badge').length).toBe(1); - expect(sizzle('.badge', occupants[2]).pop().textContent.trim()).toBe('Visitor'); - expect(occupants[2].getAttribute('title')).toBe( - contact_jid + ' This user can NOT send messages in this groupchat. Click to mention visitorwoman in your message.' - ); - done(); - })); + it("indicates moderators and visitors by means of a special css class and tooltip", + mock.initConverse( + ['rosterGroupsFetched'], {'view_mode': 'fullscreen'}, + async function (done, _converse) { - it("properly handles notification that a room has been destroyed", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + let contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.openChatRoomViaModal(_converse, 'problematic@muc.montague.lit', 'romeo') - const presence = $pres().attrs({ - from:'problematic@muc.montague.lit', - id:'n13mt3l', + await u.waitUntil(() => view.el.querySelectorAll('.occupant-list li').length, 500); + let occupants = view.el.querySelectorAll('.occupant-list li'); + expect(occupants.length).toBe(1); + expect(occupants[0].querySelector('.occupant-nick').textContent.trim()).toBe("romeo"); + expect(occupants[0].querySelectorAll('.badge').length).toBe(2); + expect(occupants[0].querySelectorAll('.badge')[0].textContent.trim()).toBe('Owner'); + expect(sizzle('.badge:last', occupants[0]).pop().textContent.trim()).toBe('Moderator'); + + var presence = $pres({ to:'romeo@montague.lit/pda', - type:'error'}) - .c('error').attrs({'type':'cancel'}) - .c('gone').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'}) - .t('xmpp:other-room@chat.jabberfr.org?join').up() - .c('text').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'}) - .t("We didn't like the name").nodeTree; + from:'lounge@montague.lit/moderatorman' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: 'admin', + jid: contact_jid, + role: 'moderator', + }).up() + .c('status').attrs({code:'110'}).nodeTree; - const view = _converse.chatboxviews.get('problematic@muc.montague.lit'); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.el.querySelector('.chatroom-body .disconnect-msg').textContent.trim()) - .toBe('This groupchat no longer exists'); - expect(view.el.querySelector('.chatroom-body .destroyed-reason').textContent.trim()) - .toBe(`"We didn't like the name"`); - expect(view.el.querySelector('.chatroom-body .moved-label').textContent.trim()) - .toBe('The conversation has moved. Click below to enter.'); - expect(view.el.querySelector('.chatroom-body .moved-link').textContent.trim()) - .toBe(`other-room@chat.jabberfr.org`); - done(); - })); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelectorAll('.occupant-list li').length > 1, 500); + occupants = view.el.querySelectorAll('.occupant-list li'); + expect(occupants.length).toBe(2); + expect(occupants[0].querySelector('.occupant-nick').textContent.trim()).toBe("moderatorman"); + expect(occupants[1].querySelector('.occupant-nick').textContent.trim()).toBe("romeo"); + expect(occupants[0].querySelectorAll('.badge').length).toBe(2); + expect(occupants[0].querySelectorAll('.badge')[0].textContent.trim()).toBe('Admin'); + expect(occupants[0].querySelectorAll('.badge')[1].textContent.trim()).toBe('Moderator'); - it("will use the user's reserved nickname, if it exists", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + expect(occupants[0].getAttribute('title')).toBe( + contact_jid + ' This user is a moderator. Click to mention moderatorman in your message.' + ); - const IQ_stanzas = _converse.connection.IQ_stanzas; - const muc_jid = 'lounge@montague.lit'; - await test_utils.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); + contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + presence = $pres({ + to:'romeo@montague.lit/pda', + from:'lounge@montague.lit/visitorwoman' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + jid: contact_jid, + role: 'visitor', + }).up() + .c('status').attrs({code:'110'}).nodeTree; + _converse.connection._dataRecv(mock.createRequest(presence)); - let stanza = await u.waitUntil(() => IQ_stanzas.filter( - iq => iq.querySelector( - `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` - )).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)); + await u.waitUntil(() => view.el.querySelectorAll('.occupant-list li').length > 2, 500); + occupants = view.el.querySelector('.occupant-list').querySelectorAll('li'); + expect(occupants.length).toBe(3); + expect(occupants[2].querySelector('.occupant-nick').textContent.trim()).toBe("visitorwoman"); + expect(occupants[2].querySelectorAll('.badge').length).toBe(1); + expect(sizzle('.badge', occupants[2]).pop().textContent.trim()).toBe('Visitor'); + expect(occupants[2].getAttribute('title')).toBe( + contact_jid + ' This user can NOT send messages in this groupchat. Click to mention visitorwoman in your message.' + ); + done(); + })); + + it("properly handles notification that a room has been destroyed", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.openChatRoomViaModal(_converse, 'problematic@muc.montague.lit', 'romeo') + const presence = $pres().attrs({ + from:'problematic@muc.montague.lit', + id:'n13mt3l', + to:'romeo@montague.lit/pda', + type:'error'}) + .c('error').attrs({'type':'cancel'}) + .c('gone').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'}) + .t('xmpp:other-room@chat.jabberfr.org?join').up() + .c('text').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'}) + .t("We didn't like the name").nodeTree; + + const view = _converse.chatboxviews.get('problematic@muc.montague.lit'); + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.el.querySelector('.chatroom-body .disconnect-msg').textContent.trim()) + .toBe('This groupchat no longer exists'); + expect(view.el.querySelector('.chatroom-body .destroyed-reason').textContent.trim()) + .toBe(`"We didn't like the name"`); + expect(view.el.querySelector('.chatroom-body .moved-label').textContent.trim()) + .toBe('The conversation has moved. Click below to enter.'); + expect(view.el.querySelector('.chatroom-body .moved-link').textContent.trim()) + .toBe(`other-room@chat.jabberfr.org`); + done(); + })); + + it("will use the user's reserved nickname, if it exists", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const IQ_stanzas = _converse.connection.IQ_stanzas; + const muc_jid = 'lounge@montague.lit'; + await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); + + let stanza = await u.waitUntil(() => IQ_stanzas.filter( + iq => iq.querySelector( + `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).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 iq = await u.waitUntil(() => _.filter( - IQ_stanzas, - s => sizzle(`iq[to="${muc_jid}"] query[node="x-roomuser-item"]`, s).length - ).pop() - ); - expect(Strophe.serialize(iq)).toBe( - ``+ - ``); + /* + * + * + */ + const iq = await u.waitUntil(() => _.filter( + IQ_stanzas, + s => sizzle(`iq[to="${muc_jid}"] query[node="x-roomuser-item"]`, s).length + ).pop() + ); + expect(Strophe.serialize(iq)).toBe( + ``+ + ``); - /* - * - * - * - * - */ - const view = _converse.chatboxviews.get('lounge@montague.lit'); - stanza = $iq({ - 'type': 'result', - 'id': iq.getAttribute('id'), - 'from': view.model.get('jid'), - 'to': _converse.connection.jid - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info', 'node': 'x-roomuser-item'}) - .c('identity', {'category': 'conference', 'name': 'thirdwitch', 'type': 'text'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); + /* + * + * + * + * + */ + const view = _converse.chatboxviews.get('lounge@montague.lit'); + stanza = $iq({ + 'type': 'result', + 'id': iq.getAttribute('id'), + 'from': view.model.get('jid'), + 'to': _converse.connection.jid + }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info', 'node': 'x-roomuser-item'}) + .c('identity', {'category': 'conference', 'name': 'thirdwitch', 'type': 'text'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); - // The user has just entered the groupchat (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/thirdwitch', - id:'DC352437-C019-40EC-B590-AF29E879AF97' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - affiliation: 'member', - jid: 'romeo@montague.lit/orchard', - role: 'participant' - }).up() - .c('status').attrs({code:'110'}).up() - .c('status').attrs({code:'210'}).nodeTree; + // The user has just entered the groupchat (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/thirdwitch', + id:'DC352437-C019-40EC-B590-AF29E879AF97' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: 'member', + jid: 'romeo@montague.lit/orchard', + role: 'participant' + }).up() + .c('status').attrs({code:'110'}).up() + .c('status').attrs({code:'210'}).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length); - const info_text = sizzle('.chat-content .chat-info:first', view.el).pop().textContent.trim(); - expect(info_text).toBe('Your nickname has been automatically set to thirdwitch'); - done(); - })); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length); + const info_text = sizzle('.chat-content .chat-info:first', view.el).pop().textContent.trim(); + expect(info_text).toBe('Your nickname has been automatically set to thirdwitch'); + done(); + })); - it("allows the user to invite their roster contacts to enter the groupchat", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'view_mode': 'fullscreen'}, - async function (done, _converse) { + it("allows the user to invite their roster contacts to enter the groupchat", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'view_mode': 'fullscreen'}, + async function (done, _converse) { - // We need roster contacts, so that we have someone to invite - await test_utils.waitForRoster(_converse, 'current'); - const features = [ - 'http://jabber.org/protocol/muc', - 'jabber:iq:register', - 'muc_passwordprotected', - 'muc_hidden', - 'muc_temporary', - 'muc_membersonly', - 'muc_unmoderated', - 'muc_anonymous' - ] - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - expect(view.model.getOwnAffiliation()).toBe('owner'); - expect(view.model.features.get('open')).toBe(false); + // We need roster contacts, so that we have someone to invite + await mock.waitForRoster(_converse, 'current'); + const features = [ + 'http://jabber.org/protocol/muc', + 'jabber:iq:register', + 'muc_passwordprotected', + 'muc_hidden', + 'muc_temporary', + 'muc_membersonly', + 'muc_unmoderated', + 'muc_anonymous' + ] + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + expect(view.model.getOwnAffiliation()).toBe('owner'); + expect(view.model.features.get('open')).toBe(false); - expect(view.el.querySelector('.open-invite-modal')).not.toBe(null); + expect(view.el.querySelector('.open-invite-modal')).not.toBe(null); - // Members can't invite if the room isn't open - view.model.getOwnOccupant().set('affiliation', 'member'); + // Members can't invite if the room isn't open + view.model.getOwnOccupant().set('affiliation', 'member'); - await u.waitUntil(() => view.el.querySelector('.open-invite-modal') === null); + await u.waitUntil(() => view.el.querySelector('.open-invite-modal') === null); - view.model.features.set('open', 'true'); - await u.waitUntil(() => view.el.querySelector('.open-invite-modal')); + view.model.features.set('open', 'true'); + await u.waitUntil(() => view.el.querySelector('.open-invite-modal')); - view.el.querySelector('.open-invite-modal').click(); - const modal = view.muc_invite_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000) + view.el.querySelector('.open-invite-modal').click(); + const modal = view.muc_invite_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000) - expect(modal.el.querySelectorAll('#invitee_jids').length).toBe(1); - expect(modal.el.querySelectorAll('textarea').length).toBe(1); + expect(modal.el.querySelectorAll('#invitee_jids').length).toBe(1); + expect(modal.el.querySelectorAll('textarea').length).toBe(1); - spyOn(view.model, 'directInvite').and.callThrough(); + spyOn(view.model, 'directInvite').and.callThrough(); - const input = modal.el.querySelector('#invitee_jids'); - input.value = "Balt"; - modal.el.querySelector('button[type="submit"]').click(); + const input = modal.el.querySelector('#invitee_jids'); + input.value = "Balt"; + modal.el.querySelector('button[type="submit"]').click(); - await u.waitUntil(() => modal.el.querySelector('.error')); + await u.waitUntil(() => modal.el.querySelector('.error')); - const error = modal.el.querySelector('.error'); - expect(error.textContent).toBe('Please enter a valid XMPP address'); + const error = modal.el.querySelector('.error'); + expect(error.textContent).toBe('Please enter a valid XMPP address'); - let evt = new Event('input'); - input.dispatchEvent(evt); + let evt = new Event('input'); + input.dispatchEvent(evt); - let sent_stanza; - spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza)); - const hint = await u.waitUntil(() => modal.el.querySelector('.suggestion-box__results li')); - expect(input.value).toBe('Balt'); - expect(hint.textContent.trim()).toBe('Balthasar'); + let sent_stanza; + spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza)); + const hint = await u.waitUntil(() => modal.el.querySelector('.suggestion-box__results li')); + expect(input.value).toBe('Balt'); + expect(hint.textContent.trim()).toBe('Balthasar'); - evt = new Event('mousedown', {'bubbles': true}); - evt.button = 0; - hint.dispatchEvent(evt); + evt = new Event('mousedown', {'bubbles': true}); + evt.button = 0; + hint.dispatchEvent(evt); - const textarea = modal.el.querySelector('textarea'); - textarea.value = "Please join!"; - modal.el.querySelector('button[type="submit"]').click(); + const textarea = modal.el.querySelector('textarea'); + textarea.value = "Please join!"; + modal.el.querySelector('button[type="submit"]').click(); - expect(view.model.directInvite).toHaveBeenCalled(); - expect(sent_stanza.toLocaleString()).toBe( - ``+ - ``+ - `` - ); - done(); - })); + expect(view.model.directInvite).toHaveBeenCalled(); + expect(sent_stanza.toLocaleString()).toBe( + ``+ + ``+ + `` + ); + done(); + })); - it("can be joined automatically, based upon a received invite", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("can be joined automatically, based upon a received invite", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current'); // We need roster contacts, who can invite us - const name = mock.cur_names[0]; - const from_jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await u.waitUntil(() => _converse.roster.get(from_jid).vcard.get('fullname')); + await mock.waitForRoster(_converse, 'current'); // We need roster contacts, who can invite us + const name = mock.cur_names[0]; + const from_jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await u.waitUntil(() => _converse.roster.get(from_jid).vcard.get('fullname')); - spyOn(window, 'confirm').and.callFake(() => true); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - await view.close(); // Hack, otherwise we have to mock stanzas. + spyOn(window, 'confirm').and.callFake(() => true); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + await view.close(); // Hack, otherwise we have to mock stanzas. - const muc_jid = 'lounge@montague.lit'; - const reason = "Please join this groupchat"; + const muc_jid = 'lounge@montague.lit'; + const reason = "Please join this groupchat"; - expect(_converse.chatboxes.models.length).toBe(1); - expect(_converse.chatboxes.models[0].id).toBe("controlbox"); + expect(_converse.chatboxes.models.length).toBe(1); + expect(_converse.chatboxes.models[0].id).toBe("controlbox"); - const stanza = u.toStanza(` - - - `); - await _converse.onDirectMUCInvitation(stanza); + const stanza = u.toStanza(` + + + `); + await _converse.onDirectMUCInvitation(stanza); - expect(window.confirm).toHaveBeenCalledWith( - name + ' has invited you to join a groupchat: '+ muc_jid + - ', and left the following reason: "'+reason+'"'); - expect(_converse.chatboxes.models.length).toBe(2); - expect(_converse.chatboxes.models[0].id).toBe('controlbox'); - expect(_converse.chatboxes.models[1].id).toBe(muc_jid); - done(); - })); + expect(window.confirm).toHaveBeenCalledWith( + name + ' has invited you to join a groupchat: '+ muc_jid + + ', and left the following reason: "'+reason+'"'); + expect(_converse.chatboxes.models.length).toBe(2); + expect(_converse.chatboxes.models[0].id).toBe('controlbox'); + expect(_converse.chatboxes.models[1].id).toBe(muc_jid); + done(); + })); - it("shows received groupchat messages", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("shows received groupchat messages", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - const text = 'This is a received message'; - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - spyOn(_converse.api, "trigger").and.callThrough(); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - if (!view.el.querySelectorAll('.chat-area').length) { - view.renderChatArea(); - } - var nick = mock.chatroom_names[0]; - view.model.occupants.create({ - 'nick': nick, - 'muc_jid': `${view.model.get('jid')}/${nick}` - }); + const text = 'This is a received message'; + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + spyOn(_converse.api, "trigger").and.callThrough(); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + if (!view.el.querySelectorAll('.chat-area').length) { + view.renderChatArea(); + } + var nick = mock.chatroom_names[0]; + view.model.occupants.create({ + 'nick': nick, + 'muc_jid': `${view.model.get('jid')}/${nick}` + }); - const message = $msg({ - from: 'lounge@montague.lit/'+nick, - id: '1', - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t(text); - await view.model.queueMessage(message.nodeTree); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length); - expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.content.querySelector('.chat-msg__text').textContent.trim()).toBe(text); - expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); - done(); - })); + const message = $msg({ + from: 'lounge@montague.lit/'+nick, + id: '1', + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t(text); + await view.model.queueMessage(message.nodeTree); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length); + expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.content.querySelector('.chat-msg__text').textContent.trim()).toBe(text); + expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); + done(); + })); - it("shows sent groupchat messages", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("shows sent groupchat messages", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - spyOn(_converse.api, "trigger").and.callThrough(); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - if (!view.el.querySelectorAll('.chat-area').length) { - view.renderChatArea(); - } - const text = 'This is a sent message'; - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = text; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - await new Promise(resolve => view.once('messageInserted', resolve)); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + spyOn(_converse.api, "trigger").and.callThrough(); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + if (!view.el.querySelectorAll('.chat-area').length) { + view.renderChatArea(); + } + const text = 'This is a sent message'; + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = text; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + await new Promise(resolve => view.once('messageInserted', resolve)); - expect(_converse.api.trigger).toHaveBeenCalledWith('messageSend', jasmine.any(_converse.Message)); - expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); + expect(_converse.api.trigger).toHaveBeenCalledWith('messageSend', jasmine.any(_converse.Message)); + expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); - // Let's check that if we receive the same message again, it's - // not shown. - const stanza = u.toStanza(` - - ${text} - - - `); - await view.model.queueMessage(stanza); - expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); - expect(sizzle('.chat-msg__text:last').pop().textContent.trim()).toBe(text); - expect(view.model.messages.length).toBe(1); - // We don't emit an event if it's our own message - expect(_converse.api.trigger.calls.count(), 1); - done(); - })); + // Let's check that if we receive the same message again, it's + // not shown. + const stanza = u.toStanza(` + + ${text} + + + `); + await view.model.queueMessage(stanza); + expect(view.content.querySelectorAll('.chat-msg').length).toBe(1); + expect(sizzle('.chat-msg__text:last').pop().textContent.trim()).toBe(text); + expect(view.model.messages.length).toBe(1); + // We don't emit an event if it's our own message + expect(_converse.api.trigger.calls.count(), 1); + done(); + })); - it("will cause the chat area to be scrolled down only if it was at the bottom already", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("will cause the chat area to be scrolled down only if it was at the bottom already", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - const message = 'This message is received while the chat area is scrolled up'; - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - spyOn(view, 'scrollDown').and.callThrough(); - // Create enough messages so that there's a scrollbar. - const promises = []; - for (let i=0; i<20; i++) { - promises.push( - view.model.queueMessage( - $msg({ - from: 'lounge@montague.lit/someone', - to: 'romeo@montague.lit.com', - type: 'groupchat', - id: u.getUniqueId(), - }).c('body').t('Message: '+i).tree()) - ); - } - await Promise.all(promises); - // Give enough time for `markScrolled` to have been called - setTimeout(async () => { - view.content.scrollTop = 0; - await view.model.queueMessage( + const message = 'This message is received while the chat area is scrolled up'; + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + spyOn(view, 'scrollDown').and.callThrough(); + // Create enough messages so that there's a scrollbar. + const promises = []; + for (let i=0; i<20; i++) { + promises.push( + view.model.queueMessage( $msg({ from: 'lounge@montague.lit/someone', to: 'romeo@montague.lit.com', type: 'groupchat', id: u.getUniqueId(), - }).c('body').t(message).tree()); - await new Promise(resolve => view.once('messageInserted', resolve)); - // Now check that the message appears inside the chatbox in the DOM - const msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent; - expect(msg_txt).toEqual(message); - expect(view.content.scrollTop).toBe(0); - done(); - }, 500); - })); + }).c('body').t('Message: '+i).tree()) + ); + } + await Promise.all(promises); + // Give enough time for `markScrolled` to have been called + setTimeout(async () => { + view.content.scrollTop = 0; + await view.model.queueMessage( + $msg({ + from: 'lounge@montague.lit/someone', + to: 'romeo@montague.lit.com', + type: 'groupchat', + id: u.getUniqueId(), + }).c('body').t(message).tree()); + await new Promise(resolve => view.once('messageInserted', resolve)); + // Now check that the message appears inside the chatbox in the DOM + const msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent; + expect(msg_txt).toEqual(message); + expect(view.content.scrollTop).toBe(0); + done(); + }, 500); + })); - it("reconnects when no-acceptable error is returned when sending a message", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("reconnects when no-acceptable error is returned when sending a message", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - const muc_jid = 'coven@chat.shakespeare.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.chatboxviews.get(muc_jid); - expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); - await test_utils.sendMessage(view, 'hello world'); + const muc_jid = 'coven@chat.shakespeare.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.chatboxviews.get(muc_jid); + expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); + await mock.sendMessage(view, 'hello world'); - const stanza = u.toStanza(` - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - let sent_stanzas = _converse.connection.sent_stanzas; - const iq = await u.waitUntil(() => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.PING}"]`, s).length).pop()); - expect(Strophe.serialize(iq)).toBe( - ``+ - ``+ - ``); - - const result = u.toStanza(` - + const stanza = u.toStanza(` + - `); - sent_stanzas = _converse.connection.sent_stanzas; - const index = sent_stanzas.length -1; + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); - _converse.connection.IQ_stanzas = []; - _converse.connection._dataRecv(test_utils.createRequest(result)); - await test_utils.getRoomFeatures(_converse, muc_jid); + let sent_stanzas = _converse.connection.sent_stanzas; + const iq = await u.waitUntil(() => sent_stanzas.filter(s => sizzle(`[xmlns="${Strophe.NS.PING}"]`, s).length).pop()); + expect(Strophe.serialize(iq)).toBe( + ``+ + ``+ + ``); - const pres = await u.waitUntil( - () => sent_stanzas.slice(index).filter(s => s.nodeName === 'presence').pop()); - expect(Strophe.serialize(pres)).toBe( - ``+ - ``+ - ``); - done(); - })); + const result = u.toStanza(` + + + + + `); + sent_stanzas = _converse.connection.sent_stanzas; + const index = sent_stanzas.length -1; + + _converse.connection.IQ_stanzas = []; + _converse.connection._dataRecv(mock.createRequest(result)); + await mock.getRoomFeatures(_converse, muc_jid); + + const pres = await u.waitUntil( + () => sent_stanzas.slice(index).filter(s => s.nodeName === 'presence').pop()); + expect(Strophe.serialize(pres)).toBe( + ``+ + ``+ + ``); + done(); + })); - it("informs users if the room configuration has changed", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("informs users if the room configuration has changed", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - const muc_jid = 'coven@chat.shakespeare.lit'; - await test_utils.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'romeo'); - const view = _converse.chatboxviews.get(muc_jid); - expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); + const muc_jid = 'coven@chat.shakespeare.lit'; + await mock.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'romeo'); + const view = _converse.chatboxviews.get(muc_jid); + expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); - const stanza = u.toStanza(` - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length); - const info_messages = view.el.querySelectorAll('.chat-content .chat-info'); - expect(info_messages[0].textContent.trim()).toBe('Groupchat logging is now enabled'); - done(); - })); + const stanza = u.toStanza(` + + + + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length); + const info_messages = view.el.querySelectorAll('.chat-content .chat-info'); + expect(info_messages[0].textContent.trim()).toBe('Groupchat logging is now enabled'); + done(); + })); - it("informs users if their nicknames have been changed.", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("informs users if their nicknames have been changed.", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - /* The service then sends two presence stanzas to the full JID - * of each occupant (including the occupant who is changing his - * or her room nickname), one of type "unavailable" for the old - * nickname and one indicating availability for the new - * nickname. - * - * See: https://xmpp.org/extensions/xep-0045.html#changenick - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - */ - const __ = _converse.__; - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'oldnick'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); + /* The service then sends two presence stanzas to the full JID + * of each occupant (including the occupant who is changing his + * or her room nickname), one of type "unavailable" for the old + * nickname and one indicating availability for the new + * nickname. + * + * See: https://xmpp.org/extensions/xep-0045.html#changenick + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ + const __ = _converse.__; + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'oldnick'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); - await u.waitUntil(() => view.el.querySelectorAll('li .occupant-nick').length, 500); - let occupants = view.el.querySelector('.occupant-list'); - expect(occupants.childElementCount).toBe(1); - expect(occupants.firstElementChild.querySelector('.occupant-nick').textContent.trim()).toBe("oldnick"); + await u.waitUntil(() => view.el.querySelectorAll('li .occupant-nick').length, 500); + let occupants = view.el.querySelector('.occupant-list'); + expect(occupants.childElementCount).toBe(1); + expect(occupants.firstElementChild.querySelector('.occupant-nick').textContent.trim()).toBe("oldnick"); - const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext.trim()).toEqual("oldnick has entered the groupchat"); + const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext.trim()).toEqual("oldnick has entered the groupchat"); - let presence = $pres().attrs({ - from:'lounge@montague.lit/oldnick', - id:'DC352437-C019-40EC-B590-AF29E879AF98', - to:'romeo@montague.lit/pda', - type:'unavailable' - }) - .c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - affiliation: 'owner', - jid: 'romeo@montague.lit/pda', - nick: 'newnick', - role: 'moderator' - }).up() - .c('status').attrs({code:'303'}).up() - .c('status').attrs({code:'110'}).nodeTree; + let presence = $pres().attrs({ + from:'lounge@montague.lit/oldnick', + id:'DC352437-C019-40EC-B590-AF29E879AF98', + to:'romeo@montague.lit/pda', + type:'unavailable' + }) + .c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: 'owner', + jid: 'romeo@montague.lit/pda', + nick: 'newnick', + role: 'moderator' + }).up() + .c('status').attrs({code:'303'}).up() + .c('status').attrs({code:'110'}).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length); - expect(sizzle('div.chat-info:last').pop().textContent.trim()).toBe( - __(_converse.muc.new_nickname_messages["303"], "newnick") - ); - expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); + expect(sizzle('div.chat-info:last').pop().textContent.trim()).toBe( + __(_converse.muc.new_nickname_messages["303"], "newnick") + ); + expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); - occupants = view.el.querySelector('.occupant-list'); - expect(occupants.childElementCount).toBe(1); + occupants = view.el.querySelector('.occupant-list'); + expect(occupants.childElementCount).toBe(1); - presence = $pres().attrs({ - from:'lounge@montague.lit/newnick', - id:'5B4F27A4-25ED-43F7-A699-382C6B4AFC67', - to:'romeo@montague.lit/pda' - }) - .c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - affiliation: 'owner', - jid: 'romeo@montague.lit/pda', - role: 'moderator' - }).up() - .c('status').attrs({code:'110'}).nodeTree; + presence = $pres().attrs({ + from:'lounge@montague.lit/newnick', + id:'5B4F27A4-25ED-43F7-A699-382C6B4AFC67', + to:'romeo@montague.lit/pda' + }) + .c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: 'owner', + jid: 'romeo@montague.lit/pda', + role: 'moderator' + }).up() + .c('status').attrs({code:'110'}).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); - expect(view.content.querySelectorAll('div.chat-info').length).toBe(1); - expect(sizzle('div.chat-info', view.content)[0].textContent.trim()).toBe( - __(_converse.muc.new_nickname_messages["303"], "newnick") - ); - occupants = view.el.querySelector('.occupant-list'); - expect(occupants.childElementCount).toBe(1); - expect(sizzle('.occupant-nick:first', occupants).pop().textContent.trim()).toBe("newnick"); - done(); - })); + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); + expect(view.content.querySelectorAll('div.chat-info').length).toBe(1); + expect(sizzle('div.chat-info', view.content)[0].textContent.trim()).toBe( + __(_converse.muc.new_nickname_messages["303"], "newnick") + ); + occupants = view.el.querySelector('.occupant-list'); + expect(occupants.childElementCount).toBe(1); + expect(sizzle('.occupant-nick:first', occupants).pop().textContent.trim()).toBe("newnick"); + done(); + })); - it("queries for the groupchat information before attempting to join the user", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("queries for the groupchat information before attempting to join the user", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - const IQ_stanzas = _converse.connection.IQ_stanzas; - const muc_jid = 'coven@chat.shakespeare.lit'; + const IQ_stanzas = _converse.connection.IQ_stanzas; + const muc_jid = 'coven@chat.shakespeare.lit'; - await _converse.api.rooms.open(muc_jid, {'nick': 'some1'}); - const stanza = await u.waitUntil(() => _.filter( - IQ_stanzas, - iq => iq.querySelector( - `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` - )).pop()); + await _converse.api.rooms.open(muc_jid, {'nick': 'some1'}); + const stanza = await u.waitUntil(() => _.filter( + IQ_stanzas, + iq => iq.querySelector( + `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); - // Check that the groupchat queried for the feautures. - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``); + // Check that the groupchat queried for the feautures. + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``); - /* - * - * - * - * - * - * - * - * - * - * - * - */ - const features_stanza = $iq({ - 'from': muc_jid, - 'id': stanza.getAttribute('id'), - 'to': 'romeo@montague.lit/desktop', - 'type': 'result' - }) - .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', { - 'category': 'conference', - 'name': 'A Dark Cave', - 'type': 'text' - }).up() - .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() - .c('feature', {'var': 'muc_passwordprotected'}).up() - .c('feature', {'var': 'muc_hidden'}).up() - .c('feature', {'var': 'muc_temporary'}).up() - .c('feature', {'var': 'muc_open'}).up() - .c('feature', {'var': 'muc_unmoderated'}).up() - .c('feature', {'var': 'muc_nonanonymous'}); - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); - let view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); - view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); - expect(view.model.features.get('fetched')).toBeTruthy(); - expect(view.model.features.get('passwordprotected')).toBe(true); - expect(view.model.features.get('hidden')).toBe(true); - expect(view.model.features.get('temporary')).toBe(true); - expect(view.model.features.get('open')).toBe(true); - expect(view.model.features.get('unmoderated')).toBe(true); - expect(view.model.features.get('nonanonymous')).toBe(true); - done(); - })); - - it("updates the shown features when the groupchat configuration has changed", - mock.initConverse( - ['rosterGroupsFetched'], {'view_mode': 'fullscreen'}, - async function (done, _converse) { - - let features = [ - 'http://jabber.org/protocol/muc', - 'jabber:iq:register', - 'muc_passwordprotected', - 'muc_publicroom', - 'muc_temporary', - 'muc_open', - 'muc_unmoderated', - 'muc_nonanonymous' - ]; - await test_utils.openAndEnterChatRoom(_converse, 'room@conference.example.org', 'romeo', features); - const jid = 'room@conference.example.org'; - const view = _converse.chatboxviews.get(jid); - - const info_el = view.el.querySelector(".show-room-details-modal"); - info_el.click(); - const modal = view.model.room_details_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - - let features_list = modal.el.querySelector('.features-list'); - let features_shown = features_list.textContent.split('\n').map(s => s.trim()).filter(s => s); - - expect(features_shown.join(' ')).toBe( - 'Password protected - This groupchat requires a password before entry '+ - 'Open - Anyone can join this groupchat '+ - 'Temporary - This groupchat will disappear once the last person leaves '+ - 'Not anonymous - All other groupchat participants can see your XMPP address '+ - 'Not moderated - Participants entering this groupchat can write right away'); - expect(view.model.features.get('hidden')).toBe(false); - expect(view.model.features.get('mam_enabled')).toBe(false); - expect(view.model.features.get('membersonly')).toBe(false); - expect(view.model.features.get('moderated')).toBe(false); - expect(view.model.features.get('nonanonymous')).toBe(true); - expect(view.model.features.get('open')).toBe(true); - expect(view.model.features.get('passwordprotected')).toBe(true); - expect(view.model.features.get('persistent')).toBe(false); - expect(view.model.features.get('publicroom')).toBe(true); - expect(view.model.features.get('semianonymous')).toBe(false); - expect(view.model.features.get('temporary')).toBe(true); - expect(view.model.features.get('unmoderated')).toBe(true); - expect(view.model.features.get('unsecured')).toBe(false); - expect(view.el.querySelector('.chatbox-title__text').textContent.trim()).toBe('Room'); - - view.el.querySelector('.configure-chatroom-button').click(); - - const IQs = _converse.connection.IQ_stanzas; - let iq = await u.waitUntil(() => _.filter( - IQs, - iq => iq.querySelector( - `iq[to="${jid}"] query[xmlns="${Strophe.NS.MUC_OWNER}"]` - )).pop()); - - const response_el = u.toStanza( - ` - - - Configuration for room@conference.example.org - Complete and submit this form to configure the room. - - http://jabber.org/protocol/muc#roomconfig - - - Room information - - - Room - - - A brief description of the room - This room is used in tests - - - Indicate the primary language spoken in this room - en - - - Rooms are automatically deleted when they are empty, unless this option is enabled - 1 - - - Enable this to allow people to find the room - 1 - - Access to the room - - - Enable this to only allow access for room owners, admins and members - - - Permissions in the room - - - Choose whether anyone, or only moderators, may set the room's subject - - - In moderated rooms occupants must be given permission to speak by a room moderator - - - - - anyone - - Other options - - Specify the maximum number of previous messages that should be sent to users when they join the room - 50 - - - Specify the number of previous messages sent to new users when they join the room - 20 - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(response_el)); - const el = await u.waitUntil(() => document.querySelector('.chatroom-form legend')); - expect(el.textContent.trim()).toBe("Configuration for room@conference.example.org"); - sizzle('[name="muc#roomconfig_membersonly"]', view.el).pop().click(); - sizzle('[name="muc#roomconfig_roomname"]', view.el).pop().value = "New room name" - view.el.querySelector('.chatroom-form input[type="submit"]').click(); - - iq = await u.waitUntil(() => _.filter(IQs, iq => u.matchesSelector(iq, `iq[to="${jid}"][type="set"]`)).pop()); - const result = $iq({ - "xmlns": "jabber:client", - "type": "result", - "to": "romeo@montague.lit/orchard", - "from": "lounge@muc.montague.lit", - "id": iq.getAttribute('id') - }); - - IQs.length = 0; // Empty the array - _converse.connection._dataRecv(test_utils.createRequest(result)); - - iq = await u.waitUntil(() => _.filter( - IQs, - iq => iq.querySelector( - `iq[to="${jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` - )).pop()); - - const features_stanza = $iq({ - 'from': jid, - 'id': iq.getAttribute('id'), + /* + * + * + * + * + * + * + * + * + * + * + * + */ + const features_stanza = $iq({ + 'from': muc_jid, + 'id': stanza.getAttribute('id'), 'to': 'romeo@montague.lit/desktop', 'type': 'result' - }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + }) + .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) .c('identity', { 'category': 'conference', - 'name': 'New room name', + 'name': 'A Dark Cave', 'type': 'text' - }).up(); - features = [ - 'http://jabber.org/protocol/muc', - 'jabber:iq:register', - 'muc_passwordprotected', - 'muc_hidden', - 'muc_temporary', - 'muc_membersonly', - 'muc_unmoderated', - 'muc_nonanonymous' - ]; - features.forEach(f => features_stanza.c('feature', {'var': f}).up()); - features_stanza.c('x', { 'xmlns':'jabber:x:data', 'type':'result'}) - .c('field', {'var':'FORM_TYPE', 'type':'hidden'}) - .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up() - .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'}) - .c('value').t('This is the description').up().up() - .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'}) - .c('value').t(0); + }).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + .c('feature', {'var': 'muc_passwordprotected'}).up() + .c('feature', {'var': 'muc_hidden'}).up() + .c('feature', {'var': 'muc_temporary'}).up() + .c('feature', {'var': 'muc_open'}).up() + .c('feature', {'var': 'muc_unmoderated'}).up() + .c('feature', {'var': 'muc_nonanonymous'}); + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + let view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); + view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); + expect(view.model.features.get('fetched')).toBeTruthy(); + expect(view.model.features.get('passwordprotected')).toBe(true); + expect(view.model.features.get('hidden')).toBe(true); + expect(view.model.features.get('temporary')).toBe(true); + expect(view.model.features.get('open')).toBe(true); + expect(view.model.features.get('unmoderated')).toBe(true); + expect(view.model.features.get('nonanonymous')).toBe(true); + done(); + })); - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); + it("updates the shown features when the groupchat configuration has changed", + mock.initConverse( + ['rosterGroupsFetched'], {'view_mode': 'fullscreen'}, + async function (done, _converse) { - await u.waitUntil(() => new Promise(success => view.model.features.on('change', success))); - features_list = modal.el.querySelector('.features-list'); - features_shown = features_list.textContent.split('\n').map(s => s.trim()).filter(s => s); - expect(features_shown.join(' ')).toBe( - 'Password protected - This groupchat requires a password before entry '+ - 'Hidden - This groupchat is not publicly searchable '+ - 'Members only - This groupchat is restricted to members only '+ - 'Temporary - This groupchat will disappear once the last person leaves '+ - 'Not anonymous - All other groupchat participants can see your XMPP address '+ - 'Not moderated - Participants entering this groupchat can write right away'); - expect(view.model.features.get('hidden')).toBe(true); - expect(view.model.features.get('mam_enabled')).toBe(false); - expect(view.model.features.get('membersonly')).toBe(true); - expect(view.model.features.get('moderated')).toBe(false); - expect(view.model.features.get('nonanonymous')).toBe(true); - expect(view.model.features.get('open')).toBe(false); - expect(view.model.features.get('passwordprotected')).toBe(true); - expect(view.model.features.get('persistent')).toBe(false); - expect(view.model.features.get('publicroom')).toBe(false); - expect(view.model.features.get('semianonymous')).toBe(false); - expect(view.model.features.get('temporary')).toBe(true); - expect(view.model.features.get('unmoderated')).toBe(true); - expect(view.model.features.get('unsecured')).toBe(false); - await u.waitUntil(() => view.el.querySelector('.chatbox-title__text')?.textContent.trim() === 'New room name'); - done(); - })); + let features = [ + 'http://jabber.org/protocol/muc', + 'jabber:iq:register', + 'muc_passwordprotected', + 'muc_publicroom', + 'muc_temporary', + 'muc_open', + 'muc_unmoderated', + 'muc_nonanonymous' + ]; + await mock.openAndEnterChatRoom(_converse, 'room@conference.example.org', 'romeo', features); + const jid = 'room@conference.example.org'; + const view = _converse.chatboxviews.get(jid); - it("indicates when a room is no longer anonymous", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + const info_el = view.el.querySelector(".show-room-details-modal"); + info_el.click(); + const modal = view.model.room_details_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); - let IQ_id; - const sendIQ = _converse.connection.sendIQ; + let features_list = modal.el.querySelector('.features-list'); + let features_shown = features_list.textContent.split('\n').map(s => s.trim()).filter(s => s); - await test_utils.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'some1'); - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { - IQ_id = sendIQ.bind(this)(iq, callback, errback); - }); + expect(features_shown.join(' ')).toBe( + 'Password protected - This groupchat requires a password before entry '+ + 'Open - Anyone can join this groupchat '+ + 'Temporary - This groupchat will disappear once the last person leaves '+ + 'Not anonymous - All other groupchat participants can see your XMPP address '+ + 'Not moderated - Participants entering this groupchat can write right away'); + expect(view.model.features.get('hidden')).toBe(false); + expect(view.model.features.get('mam_enabled')).toBe(false); + expect(view.model.features.get('membersonly')).toBe(false); + expect(view.model.features.get('moderated')).toBe(false); + expect(view.model.features.get('nonanonymous')).toBe(true); + expect(view.model.features.get('open')).toBe(true); + expect(view.model.features.get('passwordprotected')).toBe(true); + expect(view.model.features.get('persistent')).toBe(false); + expect(view.model.features.get('publicroom')).toBe(true); + expect(view.model.features.get('semianonymous')).toBe(false); + expect(view.model.features.get('temporary')).toBe(true); + expect(view.model.features.get('unmoderated')).toBe(true); + expect(view.model.features.get('unsecured')).toBe(false); + expect(view.el.querySelector('.chatbox-title__text').textContent.trim()).toBe('Room'); - // 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)); + view.el.querySelector('.configure-chatroom-button').click(); - const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); - /* - * - * - * - * - * - */ - const message = $msg({ - type:'groupchat', - to: 'romeo@montague.lit/_converse.js-27854181', - from: 'coven@chat.shakespeare.lit' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('status', {code: '104'}).up() - .c('status', {code: '172'}); - _converse.connection._dataRecv(test_utils.createRequest(message)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length); - const chat_body = view.el.querySelector('.chatroom-body'); - expect(sizzle('.message:last', chat_body).pop().textContent.trim()) - .toBe('This groupchat is now no longer anonymous'); - done(); - })); + const IQs = _converse.connection.IQ_stanzas; + let iq = await u.waitUntil(() => _.filter( + IQs, + iq => iq.querySelector( + `iq[to="${jid}"] query[xmlns="${Strophe.NS.MUC_OWNER}"]` + )).pop()); - it("informs users if they have been kicked out of the groupchat", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + const response_el = u.toStanza( + ` + + + Configuration for room@conference.example.org + Complete and submit this form to configure the room. + + http://jabber.org/protocol/muc#roomconfig + + + Room information + + + Room + + + A brief description of the room + This room is used in tests + + + Indicate the primary language spoken in this room + en + + + Rooms are automatically deleted when they are empty, unless this option is enabled + 1 + + + Enable this to allow people to find the room + 1 + + Access to the room + + + Enable this to only allow access for room owners, admins and members + + + Permissions in the room + + + Choose whether anyone, or only moderators, may set the room's subject + + + In moderated rooms occupants must be given permission to speak by a room moderator + + + + + anyone + + Other options + + Specify the maximum number of previous messages that should be sent to users when they join the room + 50 + + + Specify the number of previous messages sent to new users when they join the room + 20 + + + + `); + _converse.connection._dataRecv(mock.createRequest(response_el)); + const el = await u.waitUntil(() => document.querySelector('.chatroom-form legend')); + expect(el.textContent.trim()).toBe("Configuration for room@conference.example.org"); + sizzle('[name="muc#roomconfig_membersonly"]', view.el).pop().click(); + sizzle('[name="muc#roomconfig_roomname"]', view.el).pop().value = "New room name" + view.el.querySelector('.chatroom-form input[type="submit"]').click(); - /* - * - * - * - * Avaunt, you cullion! - * - * - * - * - * - */ - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - var presence = $pres().attrs({ - from:'lounge@montague.lit/romeo', - to:'romeo@montague.lit/pda', - type:'unavailable' - }) - .c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - affiliation: 'none', - jid: 'romeo@montague.lit/pda', - role: 'none' - }) - .c('actor').attrs({nick: 'Fluellen'}).up() - .c('reason').t('Avaunt, you cullion!').up() - .up() - .c('status').attrs({code:'110'}).up() - .c('status').attrs({code:'307'}).nodeTree; + iq = await u.waitUntil(() => _.filter(IQs, iq => u.matchesSelector(iq, `iq[to="${jid}"][type="set"]`)).pop()); + const result = $iq({ + "xmlns": "jabber:client", + "type": "result", + "to": "romeo@montague.lit/orchard", + "from": "lounge@muc.montague.lit", + "id": iq.getAttribute('id') + }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); + IQs.length = 0; // Empty the array + _converse.connection._dataRecv(mock.createRequest(result)); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - expect(u.isVisible(view.el.querySelector('.chat-area'))).toBeFalsy(); - expect(u.isVisible(view.el.querySelector('.occupants'))).toBeFalsy(); - const chat_body = view.el.querySelector('.chatroom-body'); - expect(chat_body.querySelectorAll('.disconnect-msg').length).toBe(3); - expect(chat_body.querySelector('.disconnect-msg:first-child').textContent.trim()).toBe( - 'You have been kicked from this groupchat'); - expect(chat_body.querySelector('.disconnect-msg:nth-child(2)').textContent.trim()).toBe( - 'This action was done by Fluellen.'); - expect(chat_body.querySelector('.disconnect-msg:nth-child(3)').textContent.trim()).toBe( - 'The reason given is: "Avaunt, you cullion!".'); - done(); - })); + iq = await u.waitUntil(() => _.filter( + IQs, + iq => iq.querySelector( + `iq[to="${jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + + const features_stanza = $iq({ + 'from': jid, + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', { + 'category': 'conference', + 'name': 'New room name', + 'type': 'text' + }).up(); + features = [ + 'http://jabber.org/protocol/muc', + 'jabber:iq:register', + 'muc_passwordprotected', + 'muc_hidden', + 'muc_temporary', + 'muc_membersonly', + 'muc_unmoderated', + 'muc_nonanonymous' + ]; + features.forEach(f => features_stanza.c('feature', {'var': f}).up()); + features_stanza.c('x', { 'xmlns':'jabber:x:data', 'type':'result'}) + .c('field', {'var':'FORM_TYPE', 'type':'hidden'}) + .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up() + .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'}) + .c('value').t('This is the description').up().up() + .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'}) + .c('value').t(0); + + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + + await u.waitUntil(() => new Promise(success => view.model.features.on('change', success))); + features_list = modal.el.querySelector('.features-list'); + features_shown = features_list.textContent.split('\n').map(s => s.trim()).filter(s => s); + expect(features_shown.join(' ')).toBe( + 'Password protected - This groupchat requires a password before entry '+ + 'Hidden - This groupchat is not publicly searchable '+ + 'Members only - This groupchat is restricted to members only '+ + 'Temporary - This groupchat will disappear once the last person leaves '+ + 'Not anonymous - All other groupchat participants can see your XMPP address '+ + 'Not moderated - Participants entering this groupchat can write right away'); + expect(view.model.features.get('hidden')).toBe(true); + expect(view.model.features.get('mam_enabled')).toBe(false); + expect(view.model.features.get('membersonly')).toBe(true); + expect(view.model.features.get('moderated')).toBe(false); + expect(view.model.features.get('nonanonymous')).toBe(true); + expect(view.model.features.get('open')).toBe(false); + expect(view.model.features.get('passwordprotected')).toBe(true); + expect(view.model.features.get('persistent')).toBe(false); + expect(view.model.features.get('publicroom')).toBe(false); + expect(view.model.features.get('semianonymous')).toBe(false); + expect(view.model.features.get('temporary')).toBe(true); + expect(view.model.features.get('unmoderated')).toBe(true); + expect(view.model.features.get('unsecured')).toBe(false); + await u.waitUntil(() => view.el.querySelector('.chatbox-title__text')?.textContent.trim() === 'New room name'); + done(); + })); + + it("indicates when a room is no longer anonymous", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + let IQ_id; + const sendIQ = _converse.connection.sendIQ; + + await mock.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'some1'); + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) { + IQ_id = sendIQ.bind(this)(iq, callback, errback); + }); + + // 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(mock.createRequest(features_stanza)); + + const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); + /* + * + * + * + * + * + */ + const message = $msg({ + type:'groupchat', + to: 'romeo@montague.lit/_converse.js-27854181', + from: 'coven@chat.shakespeare.lit' + }).c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('status', {code: '104'}).up() + .c('status', {code: '172'}); + _converse.connection._dataRecv(mock.createRequest(message)); + await u.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length); + const chat_body = view.el.querySelector('.chatroom-body'); + expect(sizzle('.message:last', chat_body).pop().textContent.trim()) + .toBe('This groupchat is now no longer anonymous'); + done(); + })); + + it("informs users if they have been kicked out of the groupchat", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + /* + * + * + * + * Avaunt, you cullion! + * + * + * + * + * + */ + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + var presence = $pres().attrs({ + from:'lounge@montague.lit/romeo', + to:'romeo@montague.lit/pda', + type:'unavailable' + }) + .c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: 'none', + jid: 'romeo@montague.lit/pda', + role: 'none' + }) + .c('actor').attrs({nick: 'Fluellen'}).up() + .c('reason').t('Avaunt, you cullion!').up() + .up() + .c('status').attrs({code:'110'}).up() + .c('status').attrs({code:'307'}).nodeTree; + + _converse.connection._dataRecv(mock.createRequest(presence)); + + const view = _converse.chatboxviews.get('lounge@montague.lit'); + expect(u.isVisible(view.el.querySelector('.chat-area'))).toBeFalsy(); + expect(u.isVisible(view.el.querySelector('.occupants'))).toBeFalsy(); + const chat_body = view.el.querySelector('.chatroom-body'); + expect(chat_body.querySelectorAll('.disconnect-msg').length).toBe(3); + expect(chat_body.querySelector('.disconnect-msg:first-child').textContent.trim()).toBe( + 'You have been kicked from this groupchat'); + expect(chat_body.querySelector('.disconnect-msg:nth-child(2)').textContent.trim()).toBe( + 'This action was done by Fluellen.'); + expect(chat_body.querySelector('.disconnect-msg:nth-child(3)').textContent.trim()).toBe( + 'The reason given is: "Avaunt, you cullion!".'); + done(); + })); - it("can be saved to, and retrieved from, browserStorage", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("can be saved to, and retrieved from, browserStorage", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - await test_utils.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); - // We instantiate a new ChatBoxes collection, which by default - // will be empty. - await test_utils.openControlBox(_converse); - 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.once('chatBoxesFetched', resolve)); + await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); + // We instantiate a new ChatBoxes collection, which by default + // will be empty. + await mock.openControlBox(_converse); + 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.once('chatBoxesFetched', resolve)); - expect(newchatboxes.length).toEqual(2); - // Check that the chatrooms 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 view.el.querySelector('.toggle-chatbox-button')); - button.click(); + spyOn(view, 'onMinimized').and.callThrough(); + spyOn(view, 'onMaximized').and.callThrough(); + spyOn(_converse.api, "trigger").and.callThrough(); + view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + const button = await u.waitUntil(() => view.el.querySelector('.toggle-chatbox-button')); + button.click(); - expect(view.onMinimized).toHaveBeenCalled(); - expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object)); - expect(u.isVisible(view.el)).toBeFalsy(); - expect(view.model.get('minimized')).toBeTruthy(); - expect(view.onMinimized).toHaveBeenCalled(); - await u.waitUntil(() => trimmed_chatboxes.get(view.model.get('id'))); - const trimmedview = trimmed_chatboxes.get(view.model.get('id')); - trimmedview.el.querySelector("a.restore-chat").click(); - expect(view.onMaximized).toHaveBeenCalled(); - expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object)); - expect(view.model.get('minimized')).toBeFalsy(); - expect(_converse.api.trigger.calls.count(), 3); - done(); + expect(view.onMinimized).toHaveBeenCalled(); + expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMinimized', jasmine.any(Object)); + expect(u.isVisible(view.el)).toBeFalsy(); + expect(view.model.get('minimized')).toBeTruthy(); + expect(view.onMinimized).toHaveBeenCalled(); + await u.waitUntil(() => trimmed_chatboxes.get(view.model.get('id'))); + const trimmedview = trimmed_chatboxes.get(view.model.get('id')); + trimmedview.el.querySelector("a.restore-chat").click(); + expect(view.onMaximized).toHaveBeenCalled(); + expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object)); + expect(view.model.get('minimized')).toBeFalsy(); + expect(_converse.api.trigger.calls.count(), 3); + done(); - })); + })); - it("can be closed again by clicking a DOM element with class 'close-chatbox-button'", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("can be closed again by clicking a DOM element with class 'close-chatbox-button'", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - await test_utils.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - spyOn(view, 'close').and.callThrough(); - spyOn(_converse.api, "trigger").and.callThrough(); - spyOn(view.model, 'leave'); - view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true)); - const button = await u.waitUntil(() => view.el.querySelector('.close-chatbox-button')); - button.click(); - await u.waitUntil(() => view.close.calls.count()); - expect(view.model.leave).toHaveBeenCalled(); - await u.waitUntil(() => _converse.api.trigger.calls.count()); - expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); - done(); - })); + await mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + spyOn(view, 'close').and.callThrough(); + spyOn(_converse.api, "trigger").and.callThrough(); + spyOn(view.model, 'leave'); + view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true)); + const button = await u.waitUntil(() => view.el.querySelector('.close-chatbox-button')); + button.click(); + await u.waitUntil(() => view.close.calls.count()); + expect(view.model.leave).toHaveBeenCalled(); + await u.waitUntil(() => _converse.api.trigger.calls.count()); + expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); + done(); + })); - it("informs users of role and affiliation changes", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("informs users of role and affiliation changes", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - let presence = $pres({ - 'from': 'lounge@montague.lit/annoyingGuy', - 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'member', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext.trim()).toEqual("romeo and annoyingGuy have entered the groupchat"); - - presence = $pres({ - 'from': 'lounge@montague.lit/annoyingGuy', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'member', - 'role': 'visitor' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - const info_msg = await u.waitUntil(() => view.el.querySelector('.chat-info__message')); - expect(info_msg.textContent.trim()).toBe("annoyingGuy has been muted"); - - presence = $pres({ - 'from': 'lounge@montague.lit/annoyingGuy', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'member', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => - Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === - "annoyingGuy has been given a voice" - ); - - // Check that we don't see an info message concerning the role, - // if the affiliation has changed. - presence = $pres({ - 'from': 'lounge@montague.lit/annoyingGuy', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'none', - 'role': 'visitor' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => - Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === - "annoyingGuy is no longer a member of this groupchat" - ); - done(); - })); - - it("notifies users of role and affiliation changes for members not currently in the groupchat", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - - let message = $msg({ - from: 'lounge@montague.lit', - id: '2CF9013B-E8A8-42A1-9633-85AD7CA12F40', - to: 'romeo@montague.lit' + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + let presence = $pres({ + 'from': 'lounge@montague.lit/annoyingGuy', + 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', + 'to': 'romeo@montague.lit/desktop' }) .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'absentguy@montague.lit', - 'affiliation': 'member', - 'role': 'none' - }); - _converse.connection._dataRecv(test_utils.createRequest(message)); - await u.waitUntil(() => view.model.occupants.length > 1); - expect(view.model.occupants.length).toBe(2); - expect(view.model.occupants.findWhere({'jid': 'absentguy@montague.lit'}).get('affiliation')).toBe('member'); - - message = $msg({ - from: 'lounge@montague.lit', - id: '2CF9013B-E8A8-42A1-9633-85AD7CA12F41', - to: 'romeo@montague.lit' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'absentguy@montague.lit', - 'affiliation': 'none', - 'role': 'none' - }); - _converse.connection._dataRecv(test_utils.createRequest(message)); - expect(view.model.occupants.length).toBe(2); - expect(view.model.occupants.findWhere({'jid': 'absentguy@montague.lit'}).get('affiliation')).toBe('none'); - - done(); - })); - }); - - - describe("Each chat groupchat can take special commands", function () { - - it("takes /help to show the available commands", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - spyOn(window, 'confirm').and.callFake(() => true); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - let textarea = view.el.querySelector('.chat-textarea'); - const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; - textarea.value = '/help'; - view.onKeyDown(enter); - - let info_messages = sizzle('.chat-info:not(.chat-event)', view.el); - expect(info_messages.length).toBe(20); - expect(info_messages.pop().textContent.trim()).toBe('/voice: Allow muted user to post messages'); - expect(info_messages.pop().textContent.trim()).toBe('/topic: Set groupchat subject (alias for /subject)'); - expect(info_messages.pop().textContent.trim()).toBe('/subject: Set groupchat subject'); - expect(info_messages.pop().textContent.trim()).toBe('/revoke: Revoke the user\'s current affiliation'); - expect(info_messages.pop().textContent.trim()).toBe('/register: Register your nickname'); - expect(info_messages.pop().textContent.trim()).toBe('/owner: Grant ownership of this groupchat'); - expect(info_messages.pop().textContent.trim()).toBe('/op: Grant moderator role to user'); - expect(info_messages.pop().textContent.trim()).toBe('/nick: Change your nickname'); - expect(info_messages.pop().textContent.trim()).toBe('/mute: Remove user\'s ability to post messages'); - expect(info_messages.pop().textContent.trim()).toBe('/modtools: Opens up the moderator tools GUI'); - expect(info_messages.pop().textContent.trim()).toBe('/member: Grant membership to a user'); - expect(info_messages.pop().textContent.trim()).toBe('/me: Write in 3rd person'); - expect(info_messages.pop().textContent.trim()).toBe('/kick: Kick user from groupchat'); - expect(info_messages.pop().textContent.trim()).toBe('/help: Show this menu'); - expect(info_messages.pop().textContent.trim()).toBe('/destroy: Remove this groupchat'); - expect(info_messages.pop().textContent.trim()).toBe('/deop: Change user role to participant'); - expect(info_messages.pop().textContent.trim()).toBe('/clear: Clear the chat area'); - expect(info_messages.pop().textContent.trim()).toBe('/ban: Ban user by changing their affiliation to outcast'); - expect(info_messages.pop().textContent.trim()).toBe('/admin: Change user\'s affiliation to admin'); - expect(info_messages.pop().textContent.trim()).toBe('You can run the following commands'); - - const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid}); - occupant.set('affiliation', 'admin'); - textarea = view.el.querySelector('.chat-textarea'); - textarea.value = '/clear'; - view.onKeyDown(enter); - await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0); - - textarea.value = '/help'; - view.onKeyDown(enter); - info_messages = sizzle('.chat-info:not(.chat-event)', view.el); - expect(info_messages.length).toBe(19); - let commands = info_messages.map(m => m.textContent.replace(/:.*$/, '')); - expect(commands).toEqual([ - "You can run the following commands", - "/admin", "/ban", "/clear", "/deop", "/destroy", - "/help", "/kick", "/me", "/member", "/modtools", "/mute", "/nick", - "/op", "/register", "/revoke", "/subject", "/topic", "/voice" - ]); - occupant.set('affiliation', 'member'); - textarea.value = '/clear'; - view.onKeyDown(enter); - await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0); - - textarea.value = '/help'; - view.onKeyDown(enter); - info_messages = sizzle('.chat-info', view.el).slice(1); - expect(info_messages.length).toBe(9); - commands = info_messages.map(m => m.textContent.replace(/:.*$/, '')); - expect(commands).toEqual(["/clear", "/help", "/kick", "/me", "/modtools", "/mute", "/nick", "/register", "/voice"]); - - occupant.set('role', 'participant'); - textarea = view.el.querySelector('.chat-textarea'); - textarea.value = '/clear'; - view.onKeyDown(enter); - await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0); - - textarea.value = '/help'; - view.onKeyDown(enter); - info_messages = sizzle('.chat-info', view.el).slice(1); - expect(info_messages.length).toBe(5); - commands = info_messages.map(m => m.textContent.replace(/:.*$/, '')); - expect(commands).toEqual(["/clear", "/help", "/me", "/nick", "/register"]); - - // Test that /topic is available if all users may change the subject - // Note: we're making a shortcut here, this value should never be set manually - view.model.config.set('changesubject', true); - textarea.value = '/clear'; - view.onKeyDown(enter); - await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0); - - textarea.value = '/help'; - view.onKeyDown(enter); - info_messages = sizzle('.chat-info', view.el).slice(1); - expect(info_messages.length).toBe(7); - commands = info_messages.map(m => m.textContent.replace(/:.*$/, '')); - expect(commands).toEqual(["/clear", "/help", "/me", "/nick", "/register", "/subject", "/topic"]); - done(); - })); - - it("takes /help to show the available commands and commands can be disabled by config", - mock.initConverse( - ['rosterGroupsFetched'], {muc_disable_slash_commands: ['mute', 'voice']}, - async function (done, _converse) { - - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - var textarea = view.el.querySelector('.chat-textarea'); - const enter = { 'target': textarea, 'preventDefault': function () {}, 'keyCode': 13 }; - spyOn(window, 'confirm').and.callFake(() => true); - textarea.value = '/clear'; - view.onKeyDown(enter); - textarea.value = '/help'; - view.onKeyDown(enter); - - const info_messages = sizzle('.chat-info:not(.chat-event)', view.el); - expect(info_messages.length).toBe(18); - expect(info_messages.pop().textContent.trim()).toBe('/topic: Set groupchat subject (alias for /subject)'); - expect(info_messages.pop().textContent.trim()).toBe('/subject: Set groupchat subject'); - expect(info_messages.pop().textContent.trim()).toBe('/revoke: Revoke the user\'s current affiliation'); - expect(info_messages.pop().textContent.trim()).toBe('/register: Register your nickname'); - expect(info_messages.pop().textContent.trim()).toBe('/owner: Grant ownership of this groupchat'); - expect(info_messages.pop().textContent.trim()).toBe('/op: Grant moderator role to user'); - expect(info_messages.pop().textContent.trim()).toBe('/nick: Change your nickname'); - expect(info_messages.pop().textContent.trim()).toBe('/modtools: Opens up the moderator tools GUI'); - expect(info_messages.pop().textContent.trim()).toBe('/member: Grant membership to a user'); - expect(info_messages.pop().textContent.trim()).toBe('/me: Write in 3rd person'); - expect(info_messages.pop().textContent.trim()).toBe('/kick: Kick user from groupchat'); - expect(info_messages.pop().textContent.trim()).toBe('/help: Show this menu'); - expect(info_messages.pop().textContent.trim()).toBe('/destroy: Remove this groupchat'); - expect(info_messages.pop().textContent.trim()).toBe('/deop: Change user role to participant'); - expect(info_messages.pop().textContent.trim()).toBe('/clear: Clear the chat area'); - expect(info_messages.pop().textContent.trim()).toBe('/ban: Ban user by changing their affiliation to outcast'); - expect(info_messages.pop().textContent.trim()).toBe('/admin: Change user\'s affiliation to admin'); - expect(info_messages.pop().textContent.trim()).toBe('You can run the following commands'); - done(); - })); - - it("takes /member to make an occupant a member", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - let iq_stanza; - await test_utils.openAndEnterChatRoom(_converse, 'lounge@muc.montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@muc.montague.lit'); - /* We don't show join/leave messages for existing occupants. We - * know about them because we receive their presences before we - * receive our own. - */ - const presence = $pres({ - to: 'romeo@montague.lit/orchard', - from: 'lounge@muc.montague.lit/marc' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { - 'affiliation': 'none', - 'jid': 'marc@montague.lit/_converse.js-290929789', + 'jid': 'annoyingguy@montague.lit', + 'affiliation': 'member', 'role': 'participant' }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.model.occupants.length).toBe(2); + _converse.connection._dataRecv(mock.createRequest(presence)); + const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext.trim()).toEqual("romeo and annoyingGuy have entered the groupchat"); - const textarea = view.el.querySelector('.chat-textarea'); - let sent_stanza; - spyOn(_converse.connection, 'send').and.callFake((stanza) => { - sent_stanza = stanza; + presence = $pres({ + 'from': 'lounge@montague.lit/annoyingGuy', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'annoyingguy@montague.lit', + 'affiliation': 'member', + 'role': 'visitor' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + const info_msg = await u.waitUntil(() => view.el.querySelector('.chat-info__message')); + expect(info_msg.textContent.trim()).toBe("annoyingGuy has been muted"); + + presence = $pres({ + 'from': 'lounge@montague.lit/annoyingGuy', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'annoyingguy@montague.lit', + 'affiliation': 'member', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => + Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === + "annoyingGuy has been given a voice" + ); + + // Check that we don't see an info message concerning the role, + // if the affiliation has changed. + presence = $pres({ + 'from': 'lounge@montague.lit/annoyingGuy', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'annoyingguy@montague.lit', + 'affiliation': 'none', + 'role': 'visitor' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => + Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === + "annoyingGuy is no longer a member of this groupchat" + ); + done(); + })); + + it("notifies users of role and affiliation changes for members not currently in the groupchat", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + + let message = $msg({ + from: 'lounge@montague.lit', + id: '2CF9013B-E8A8-42A1-9633-85AD7CA12F40', + to: 'romeo@montague.lit' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'absentguy@montague.lit', + 'affiliation': 'member', + 'role': 'none' + }); + _converse.connection._dataRecv(mock.createRequest(message)); + await u.waitUntil(() => view.model.occupants.length > 1); + expect(view.model.occupants.length).toBe(2); + expect(view.model.occupants.findWhere({'jid': 'absentguy@montague.lit'}).get('affiliation')).toBe('member'); + + message = $msg({ + from: 'lounge@montague.lit', + id: '2CF9013B-E8A8-42A1-9633-85AD7CA12F41', + to: 'romeo@montague.lit' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'absentguy@montague.lit', + 'affiliation': 'none', + 'role': 'none' + }); + _converse.connection._dataRecv(mock.createRequest(message)); + expect(view.model.occupants.length).toBe(2); + expect(view.model.occupants.findWhere({'jid': 'absentguy@montague.lit'}).get('affiliation')).toBe('none'); + + done(); + })); + }); + + + describe("Each chat groupchat can take special commands", function () { + + it("takes /help to show the available commands", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + spyOn(window, 'confirm').and.callFake(() => true); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + let textarea = view.el.querySelector('.chat-textarea'); + const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; + textarea.value = '/help'; + view.onKeyDown(enter); + + let info_messages = sizzle('.chat-info:not(.chat-event)', view.el); + expect(info_messages.length).toBe(20); + expect(info_messages.pop().textContent.trim()).toBe('/voice: Allow muted user to post messages'); + expect(info_messages.pop().textContent.trim()).toBe('/topic: Set groupchat subject (alias for /subject)'); + expect(info_messages.pop().textContent.trim()).toBe('/subject: Set groupchat subject'); + expect(info_messages.pop().textContent.trim()).toBe('/revoke: Revoke the user\'s current affiliation'); + expect(info_messages.pop().textContent.trim()).toBe('/register: Register your nickname'); + expect(info_messages.pop().textContent.trim()).toBe('/owner: Grant ownership of this groupchat'); + expect(info_messages.pop().textContent.trim()).toBe('/op: Grant moderator role to user'); + expect(info_messages.pop().textContent.trim()).toBe('/nick: Change your nickname'); + expect(info_messages.pop().textContent.trim()).toBe('/mute: Remove user\'s ability to post messages'); + expect(info_messages.pop().textContent.trim()).toBe('/modtools: Opens up the moderator tools GUI'); + expect(info_messages.pop().textContent.trim()).toBe('/member: Grant membership to a user'); + expect(info_messages.pop().textContent.trim()).toBe('/me: Write in 3rd person'); + expect(info_messages.pop().textContent.trim()).toBe('/kick: Kick user from groupchat'); + expect(info_messages.pop().textContent.trim()).toBe('/help: Show this menu'); + expect(info_messages.pop().textContent.trim()).toBe('/destroy: Remove this groupchat'); + expect(info_messages.pop().textContent.trim()).toBe('/deop: Change user role to participant'); + expect(info_messages.pop().textContent.trim()).toBe('/clear: Clear the chat area'); + expect(info_messages.pop().textContent.trim()).toBe('/ban: Ban user by changing their affiliation to outcast'); + expect(info_messages.pop().textContent.trim()).toBe('/admin: Change user\'s affiliation to admin'); + expect(info_messages.pop().textContent.trim()).toBe('You can run the following commands'); + + const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid}); + occupant.set('affiliation', 'admin'); + textarea = view.el.querySelector('.chat-textarea'); + textarea.value = '/clear'; + view.onKeyDown(enter); + await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0); + + textarea.value = '/help'; + view.onKeyDown(enter); + info_messages = sizzle('.chat-info:not(.chat-event)', view.el); + expect(info_messages.length).toBe(19); + let commands = info_messages.map(m => m.textContent.replace(/:.*$/, '')); + expect(commands).toEqual([ + "You can run the following commands", + "/admin", "/ban", "/clear", "/deop", "/destroy", + "/help", "/kick", "/me", "/member", "/modtools", "/mute", "/nick", + "/op", "/register", "/revoke", "/subject", "/topic", "/voice" + ]); + occupant.set('affiliation', 'member'); + textarea.value = '/clear'; + view.onKeyDown(enter); + await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0); + + textarea.value = '/help'; + view.onKeyDown(enter); + info_messages = sizzle('.chat-info', view.el).slice(1); + expect(info_messages.length).toBe(9); + commands = info_messages.map(m => m.textContent.replace(/:.*$/, '')); + expect(commands).toEqual(["/clear", "/help", "/kick", "/me", "/modtools", "/mute", "/nick", "/register", "/voice"]); + + occupant.set('role', 'participant'); + textarea = view.el.querySelector('.chat-textarea'); + textarea.value = '/clear'; + view.onKeyDown(enter); + await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0); + + textarea.value = '/help'; + view.onKeyDown(enter); + info_messages = sizzle('.chat-info', view.el).slice(1); + expect(info_messages.length).toBe(5); + commands = info_messages.map(m => m.textContent.replace(/:.*$/, '')); + expect(commands).toEqual(["/clear", "/help", "/me", "/nick", "/register"]); + + // Test that /topic is available if all users may change the subject + // Note: we're making a shortcut here, this value should never be set manually + view.model.config.set('changesubject', true); + textarea.value = '/clear'; + view.onKeyDown(enter); + await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0); + + textarea.value = '/help'; + view.onKeyDown(enter); + info_messages = sizzle('.chat-info', view.el).slice(1); + expect(info_messages.length).toBe(7); + commands = info_messages.map(m => m.textContent.replace(/:.*$/, '')); + expect(commands).toEqual(["/clear", "/help", "/me", "/nick", "/register", "/subject", "/topic"]); + done(); + })); + + it("takes /help to show the available commands and commands can be disabled by config", + mock.initConverse( + ['rosterGroupsFetched'], {muc_disable_slash_commands: ['mute', 'voice']}, + async function (done, _converse) { + + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + var textarea = view.el.querySelector('.chat-textarea'); + const enter = { 'target': textarea, 'preventDefault': function () {}, 'keyCode': 13 }; + spyOn(window, 'confirm').and.callFake(() => true); + textarea.value = '/clear'; + view.onKeyDown(enter); + textarea.value = '/help'; + view.onKeyDown(enter); + + const info_messages = sizzle('.chat-info:not(.chat-event)', view.el); + expect(info_messages.length).toBe(18); + expect(info_messages.pop().textContent.trim()).toBe('/topic: Set groupchat subject (alias for /subject)'); + expect(info_messages.pop().textContent.trim()).toBe('/subject: Set groupchat subject'); + expect(info_messages.pop().textContent.trim()).toBe('/revoke: Revoke the user\'s current affiliation'); + expect(info_messages.pop().textContent.trim()).toBe('/register: Register your nickname'); + expect(info_messages.pop().textContent.trim()).toBe('/owner: Grant ownership of this groupchat'); + expect(info_messages.pop().textContent.trim()).toBe('/op: Grant moderator role to user'); + expect(info_messages.pop().textContent.trim()).toBe('/nick: Change your nickname'); + expect(info_messages.pop().textContent.trim()).toBe('/modtools: Opens up the moderator tools GUI'); + expect(info_messages.pop().textContent.trim()).toBe('/member: Grant membership to a user'); + expect(info_messages.pop().textContent.trim()).toBe('/me: Write in 3rd person'); + expect(info_messages.pop().textContent.trim()).toBe('/kick: Kick user from groupchat'); + expect(info_messages.pop().textContent.trim()).toBe('/help: Show this menu'); + expect(info_messages.pop().textContent.trim()).toBe('/destroy: Remove this groupchat'); + expect(info_messages.pop().textContent.trim()).toBe('/deop: Change user role to participant'); + expect(info_messages.pop().textContent.trim()).toBe('/clear: Clear the chat area'); + expect(info_messages.pop().textContent.trim()).toBe('/ban: Ban user by changing their affiliation to outcast'); + expect(info_messages.pop().textContent.trim()).toBe('/admin: Change user\'s affiliation to admin'); + expect(info_messages.pop().textContent.trim()).toBe('You can run the following commands'); + done(); + })); + + it("takes /member to make an occupant a member", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + let iq_stanza; + await mock.openAndEnterChatRoom(_converse, 'lounge@muc.montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@muc.montague.lit'); + /* We don't show join/leave messages for existing occupants. We + * know about them because we receive their presences before we + * receive our own. + */ + const presence = $pres({ + to: 'romeo@montague.lit/orchard', + from: 'lounge@muc.montague.lit/marc' + }).c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'marc@montague.lit/_converse.js-290929789', + 'role': 'participant' }); + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.model.occupants.length).toBe(2); - // First check that an error message appears when a - // non-existent nick is used. - textarea.value = '/member chris Welcome to the club!'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - expect(_converse.connection.send).not.toHaveBeenCalled(); - await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length); - expect(view.el.querySelector('.chat-error').textContent.trim()) - .toBe('Error: couldn\'t find a groupchat participant based on your arguments'); + const textarea = view.el.querySelector('.chat-textarea'); + let sent_stanza; + spyOn(_converse.connection, 'send').and.callFake((stanza) => { + sent_stanza = stanza; + }); - // Now test with an existing nick - textarea.value = '/member marc Welcome to the club!'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - expect(_converse.connection.send).toHaveBeenCalled(); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ``+ - ``+ - `Welcome to the club!`+ - ``+ - ``+ - ``); + // First check that an error message appears when a + // non-existent nick is used. + textarea.value = '/member chris Welcome to the club!'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + expect(_converse.connection.send).not.toHaveBeenCalled(); + await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length); + expect(view.el.querySelector('.chat-error').textContent.trim()) + .toBe('Error: couldn\'t find a groupchat participant based on your arguments'); - let result = $iq({ - "xmlns": "jabber:client", - "type": "result", - "to": "romeo@montague.lit/orchard", - "from": "lounge@muc.montague.lit", - "id": sent_stanza.getAttribute('id') - }); - _converse.connection.IQ_stanzas = []; - _converse.connection._dataRecv(test_utils.createRequest(result)); - iq_stanza = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="member"]')).pop() - ); + // Now test with an existing nick + textarea.value = '/member marc Welcome to the club!'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + expect(_converse.connection.send).toHaveBeenCalled(); + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + ``+ + ``+ + `Welcome to the club!`+ + ``+ + ``+ + ``); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``) - expect(view.model.occupants.length).toBe(2); + let result = $iq({ + "xmlns": "jabber:client", + "type": "result", + "to": "romeo@montague.lit/orchard", + "from": "lounge@muc.montague.lit", + "id": sent_stanza.getAttribute('id') + }); + _converse.connection.IQ_stanzas = []; + _converse.connection._dataRecv(mock.createRequest(result)); + iq_stanza = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="member"]')).pop() + ); - result = $iq({ - "xmlns": "jabber:client", - "type": "result", - "to": "romeo@montague.lit/orchard", - "from": "lounge@muc.montague.lit", - "id": iq_stanza.getAttribute("id") - }).c("query", {"xmlns": "http://jabber.org/protocol/muc#admin"}) - .c("item", {"jid": "marc", "affiliation": "member"}); - _converse.connection._dataRecv(test_utils.createRequest(result)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``) + expect(view.model.occupants.length).toBe(2); - expect(view.model.occupants.length).toBe(2); - iq_stanza = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="owner"]')).pop() - ); + result = $iq({ + "xmlns": "jabber:client", + "type": "result", + "to": "romeo@montague.lit/orchard", + "from": "lounge@muc.montague.lit", + "id": iq_stanza.getAttribute("id") + }).c("query", {"xmlns": "http://jabber.org/protocol/muc#admin"}) + .c("item", {"jid": "marc", "affiliation": "member"}); + _converse.connection._dataRecv(mock.createRequest(result)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``) - expect(view.model.occupants.length).toBe(2); + expect(view.model.occupants.length).toBe(2); + iq_stanza = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="owner"]')).pop() + ); - result = $iq({ - "xmlns": "jabber:client", - "type": "result", - "to": "romeo@montague.lit/orchard", - "from": "lounge@muc.montague.lit", - "id": iq_stanza.getAttribute("id") - }).c("query", {"xmlns": "http://jabber.org/protocol/muc#admin"}) - .c("item", {"jid": "romeo@montague.lit", "affiliation": "owner"}); - _converse.connection._dataRecv(test_utils.createRequest(result)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``) + expect(view.model.occupants.length).toBe(2); - expect(view.model.occupants.length).toBe(2); - iq_stanza = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="admin"]')).pop() - ); + result = $iq({ + "xmlns": "jabber:client", + "type": "result", + "to": "romeo@montague.lit/orchard", + "from": "lounge@muc.montague.lit", + "id": iq_stanza.getAttribute("id") + }).c("query", {"xmlns": "http://jabber.org/protocol/muc#admin"}) + .c("item", {"jid": "romeo@montague.lit", "affiliation": "owner"}); + _converse.connection._dataRecv(mock.createRequest(result)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``) - expect(view.model.occupants.length).toBe(2); + expect(view.model.occupants.length).toBe(2); + iq_stanza = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="admin"]')).pop() + ); - result = $iq({ - "xmlns": "jabber:client", - "type": "result", - "to": "romeo@montague.lit/orchard", - "from": "lounge@muc.montague.lit", - "id": iq_stanza.getAttribute("id") - }).c("query", {"xmlns": "http://jabber.org/protocol/muc#admin"}) - _converse.connection._dataRecv(test_utils.createRequest(result)); - await u.waitUntil(() => view.el.querySelectorAll('.occupant').length, 500); - await u.waitUntil(() => view.el.querySelectorAll('.badge').length > 1); - expect(view.model.occupants.length).toBe(2); - expect(view.el.querySelectorAll('.occupant').length).toBe(2); - done(); - })); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``) + expect(view.model.occupants.length).toBe(2); - it("takes /topic to set the groupchat topic", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + result = $iq({ + "xmlns": "jabber:client", + "type": "result", + "to": "romeo@montague.lit/orchard", + "from": "lounge@muc.montague.lit", + "id": iq_stanza.getAttribute("id") + }).c("query", {"xmlns": "http://jabber.org/protocol/muc#admin"}) + _converse.connection._dataRecv(mock.createRequest(result)); + await u.waitUntil(() => view.el.querySelectorAll('.occupant').length, 500); + await u.waitUntil(() => view.el.querySelectorAll('.badge').length > 1); + expect(view.model.occupants.length).toBe(2); + expect(view.el.querySelectorAll('.occupant').length).toBe(2); + done(); + })); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - spyOn(view, 'clearMessages'); - let sent_stanza; - spyOn(_converse.connection, 'send').and.callFake(function (stanza) { - sent_stanza = stanza; - }); - // Check the alias /topic - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = '/topic This is the groupchat subject'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - expect(_converse.connection.send).toHaveBeenCalled(); - expect(sent_stanza.textContent.trim()).toBe('This is the groupchat subject'); + it("takes /topic to set the groupchat topic", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - // Check /subject - textarea.value = '/subject This is a new subject'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + spyOn(view, 'clearMessages'); + let sent_stanza; + spyOn(_converse.connection, 'send').and.callFake(function (stanza) { + sent_stanza = stanza; + }); + // Check the alias /topic + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = '/topic This is the groupchat subject'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + expect(_converse.connection.send).toHaveBeenCalled(); + expect(sent_stanza.textContent.trim()).toBe('This is the groupchat subject'); - expect(sent_stanza.textContent.trim()).toBe('This is a new subject'); - expect(Strophe.serialize(sent_stanza).toLocaleString()).toBe( - ''+ - 'This is a new subject'+ - ''); + // Check /subject + textarea.value = '/subject This is a new subject'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); - // Check case insensitivity - textarea.value = '/Subject This is yet another subject'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - expect(sent_stanza.textContent.trim()).toBe('This is yet another subject'); - expect(Strophe.serialize(sent_stanza).toLocaleString()).toBe( - ''+ - 'This is yet another subject'+ - ''); + expect(sent_stanza.textContent.trim()).toBe('This is a new subject'); + expect(Strophe.serialize(sent_stanza).toLocaleString()).toBe( + ''+ + 'This is a new subject'+ + ''); - // Check unsetting the topic - textarea.value = '/topic'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - expect(Strophe.serialize(sent_stanza).toLocaleString()).toBe( - ''+ - ''+ - ''); - done(); - })); + // Check case insensitivity + textarea.value = '/Subject This is yet another subject'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + expect(sent_stanza.textContent.trim()).toBe('This is yet another subject'); + expect(Strophe.serialize(sent_stanza).toLocaleString()).toBe( + ''+ + 'This is yet another subject'+ + ''); - it("takes /clear to clear messages", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + // Check unsetting the topic + textarea.value = '/topic'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + expect(Strophe.serialize(sent_stanza).toLocaleString()).toBe( + ''+ + ''+ + ''); + done(); + })); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - spyOn(view, 'clearMessages'); - const textarea = view.el.querySelector('.chat-textarea') - textarea.value = '/clear'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - expect(view.clearMessages).toHaveBeenCalled(); - done(); - })); + it("takes /clear to clear messages", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - it("takes /owner to make a user an owner", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + spyOn(view, 'clearMessages'); + const textarea = view.el.querySelector('.chat-textarea') + textarea.value = '/clear'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + expect(view.clearMessages).toHaveBeenCalled(); + done(); + })); - 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); - }); + it("takes /owner to make a user an owner", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - spyOn(view.model, 'setAffiliation').and.callThrough(); - spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough(); + 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); + }); - let presence = $pres({ - 'from': 'lounge@montague.lit/annoyingGuy', - 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'member', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + spyOn(view.model, 'setAffiliation').and.callThrough(); + spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough(); - var textarea = view.el.querySelector('.chat-textarea') - textarea.value = '/owner'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled(); - const err_msg = await u.waitUntil(() => view.el.querySelector('.chat-error')); - expect(err_msg.textContent.trim()).toBe( - "Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason."); + let presence = $pres({ + 'from': 'lounge@montague.lit/annoyingGuy', + 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'annoyingguy@montague.lit', + 'affiliation': 'member', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); - expect(view.model.setAffiliation).not.toHaveBeenCalled(); - // XXX: Calling onFormSubmitted directly, trying - // again via triggering Event doesn't work for some weird - // reason. - textarea.value = '/owner nobody You\'re responsible'; - view.onFormSubmitted(new Event('submit')); - await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length === 2); - expect(Array.from(view.el.querySelectorAll('.chat-error')).pop().textContent.trim()).toBe( - "Error: couldn't find a groupchat participant based on your arguments"); + var textarea = view.el.querySelector('.chat-textarea') + textarea.value = '/owner'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled(); + const err_msg = await u.waitUntil(() => view.el.querySelector('.chat-error')); + expect(err_msg.textContent.trim()).toBe( + "Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason."); - expect(view.model.setAffiliation).not.toHaveBeenCalled(); + expect(view.model.setAffiliation).not.toHaveBeenCalled(); + // XXX: Calling onFormSubmitted directly, trying + // again via triggering Event doesn't work for some weird + // reason. + textarea.value = '/owner nobody You\'re responsible'; + view.onFormSubmitted(new Event('submit')); + await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length === 2); + expect(Array.from(view.el.querySelectorAll('.chat-error')).pop().textContent.trim()).toBe( + "Error: couldn't find a groupchat participant based on your arguments"); - // Call now with the correct of arguments. - // XXX: Calling onFormSubmitted directly, trying - // again via triggering Event doesn't work for some weird - // reason. - textarea.value = '/owner annoyingGuy You\'re responsible'; - view.onFormSubmitted(new Event('submit')); + expect(view.model.setAffiliation).not.toHaveBeenCalled(); - expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3); - expect(view.model.setAffiliation).toHaveBeenCalled(); - // Check that the member list now gets updated - expect(sent_IQ.toLocaleString()).toBe( - ``+ - ``+ - ``+ - `You're responsible`+ - ``+ - ``+ - ``); + // Call now with the correct of arguments. + // XXX: Calling onFormSubmitted directly, trying + // again via triggering Event doesn't work for some weird + // reason. + textarea.value = '/owner annoyingGuy You\'re responsible'; + view.onFormSubmitted(new Event('submit')); - presence = $pres({ - 'from': 'lounge@montague.lit/annoyingGuy', - 'id':'27C55F89-1C6A-459A-9EB5-77690145D628', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'owner', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => - Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === - "annoyingGuy is now an owner of this groupchat" - ); - done(); - })); + expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3); + expect(view.model.setAffiliation).toHaveBeenCalled(); + // Check that the member list now gets updated + expect(sent_IQ.toLocaleString()).toBe( + ``+ + ``+ + ``+ + `You're responsible`+ + ``+ + ``+ + ``); - it("takes /ban to ban a user", - mock.initConverse( - ['rosterGroupsFetched'], {}, - 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); - }); - - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - spyOn(view.model, 'setAffiliation').and.callThrough(); - spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough(); - - let presence = $pres({ - 'from': 'lounge@montague.lit/annoyingGuy', - 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'member', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - const textarea = view.el.querySelector('.chat-textarea') - textarea.value = '/ban'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled(); - await u.waitUntil(() => view.el.querySelector('.message:last-child')?.textContent?.trim() === - "Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason."); - - expect(view.model.setAffiliation).not.toHaveBeenCalled(); - // Call now with the correct amount of arguments. - // XXX: Calling onFormSubmitted directly, trying - // again via triggering Event doesn't work for some weird - // reason. - textarea.value = '/ban annoyingGuy You\'re annoying'; - view.onFormSubmitted(new Event('submit')); - - expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); - expect(view.model.setAffiliation).toHaveBeenCalled(); - // Check that the member list now gets updated - expect(sent_IQ.toLocaleString()).toBe( - ``+ - ``+ - ``+ - `You're annoying`+ - ``+ - ``+ - ``); - - presence = $pres({ + presence = $pres({ 'from': 'lounge@montague.lit/annoyingGuy', 'id':'27C55F89-1C6A-459A-9EB5-77690145D628', 'to': 'romeo@montague.lit/desktop' - }).c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) .c('item', { 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'outcast', + 'affiliation': 'owner', 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => + Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === + "annoyingGuy is now an owner of this groupchat" + ); + done(); + })); + + it("takes /ban to ban a user", + mock.initConverse( + ['rosterGroupsFetched'], {}, + 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); + }); + + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + spyOn(view.model, 'setAffiliation').and.callThrough(); + spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough(); + + let presence = $pres({ + 'from': 'lounge@montague.lit/annoyingGuy', + 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'annoyingguy@montague.lit', + 'affiliation': 'member', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + + const textarea = view.el.querySelector('.chat-textarea') + textarea.value = '/ban'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled(); + await u.waitUntil(() => view.el.querySelector('.message:last-child')?.textContent?.trim() === + "Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason."); + + expect(view.model.setAffiliation).not.toHaveBeenCalled(); + // Call now with the correct amount of arguments. + // XXX: Calling onFormSubmitted directly, trying + // again via triggering Event doesn't work for some weird + // reason. + textarea.value = '/ban annoyingGuy You\'re annoying'; + view.onFormSubmitted(new Event('submit')); + + expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); + expect(view.model.setAffiliation).toHaveBeenCalled(); + // Check that the member list now gets updated + expect(sent_IQ.toLocaleString()).toBe( + ``+ + ``+ + ``+ + `You're annoying`+ + ``+ + ``+ + ``); + + presence = $pres({ + 'from': 'lounge@montague.lit/annoyingGuy', + 'id':'27C55F89-1C6A-459A-9EB5-77690145D628', + 'to': 'romeo@montague.lit/desktop' + }).c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'annoyingguy@montague.lit', + 'affiliation': 'outcast', + 'role': 'participant' + }).c('actor', {'nick': 'romeo'}).up() + .c('reason').t("You're annoying").up().up() + .c('status', {'code': '301'}); + + _converse.connection._dataRecv(mock.createRequest(presence)); + + await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2); + expect(view.el.querySelectorAll('.chat-info__message')[1].textContent.trim()).toBe("annoyingGuy has been banned by romeo"); + expect(view.el.querySelector('.chat-info:last-child q').textContent.trim()).toBe("You're annoying"); + presence = $pres({ + 'from': 'lounge@montague.lit/joe2', + 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'joe2@montague.lit', + 'affiliation': 'member', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + + textarea.value = '/ban joe22'; + view.onFormSubmitted(new Event('submit')); + await u.waitUntil(() => view.el.querySelector('.message:last-child')?.textContent?.trim() === + "Error: couldn't find a groupchat participant based on your arguments"); + done(); + })); + + + it("takes a /kick command to kick a user", + mock.initConverse( + ['rosterGroupsFetched'], {}, + 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); + }); + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + spyOn(view.model, 'setRole').and.callThrough(); + spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough(); + + let presence = $pres({ + 'from': 'lounge@montague.lit/annoying guy', + 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'annoyingguy@montague.lit', + 'affiliation': 'none', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + + const textarea = view.el.querySelector('.chat-textarea') + textarea.value = '/kick'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled(); + await u.waitUntil(() => view.el.querySelector('.message:last-child')?.textContent?.trim() === + "Error: the \"kick\" command takes two arguments, the user's nickname and optionally a reason."); + expect(view.model.setRole).not.toHaveBeenCalled(); + // Call now with the correct amount of arguments. + // XXX: Calling onFormSubmitted directly, trying + // again via triggering Event doesn't work for some weird + // reason. + textarea.value = '/kick @annoying guy You\'re annoying'; + view.onFormSubmitted(new Event('submit')); + + expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); + expect(view.model.setRole).toHaveBeenCalled(); + expect(sent_IQ.toLocaleString()).toBe( + ``+ + ``+ + ``+ + `You're annoying`+ + ``+ + ``+ + ``); + + /* + * + * + * + * + * + */ + presence = $pres({ + 'from': 'lounge@montague.lit/annoying guy', + 'to': 'romeo@montague.lit/desktop', + 'type': 'unavailable' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'affiliation': 'none', + 'role': 'none' }).c('actor', {'nick': 'romeo'}).up() - .c('reason').t("You're annoying").up().up() - .c('status', {'code': '301'}); - - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2); - expect(view.el.querySelectorAll('.chat-info__message')[1].textContent.trim()).toBe("annoyingGuy has been banned by romeo"); - expect(view.el.querySelector('.chat-info:last-child q').textContent.trim()).toBe("You're annoying"); - presence = $pres({ - 'from': 'lounge@montague.lit/joe2', - 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'joe2@montague.lit', - 'affiliation': 'member', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - textarea.value = '/ban joe22'; - view.onFormSubmitted(new Event('submit')); - await u.waitUntil(() => view.el.querySelector('.message:last-child')?.textContent?.trim() === - "Error: couldn't find a groupchat participant based on your arguments"); - done(); - })); - - - it("takes a /kick command to kick a user", - mock.initConverse( - ['rosterGroupsFetched'], {}, - 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); - }); - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - spyOn(view.model, 'setRole').and.callThrough(); - spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough(); - - let presence = $pres({ - 'from': 'lounge@montague.lit/annoying guy', - 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'none', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - const textarea = view.el.querySelector('.chat-textarea') - textarea.value = '/kick'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled(); - await u.waitUntil(() => view.el.querySelector('.message:last-child')?.textContent?.trim() === - "Error: the \"kick\" command takes two arguments, the user's nickname and optionally a reason."); - expect(view.model.setRole).not.toHaveBeenCalled(); - // Call now with the correct amount of arguments. - // XXX: Calling onFormSubmitted directly, trying - // again via triggering Event doesn't work for some weird - // reason. - textarea.value = '/kick @annoying guy You\'re annoying'; - view.onFormSubmitted(new Event('submit')); - - expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); - expect(view.model.setRole).toHaveBeenCalled(); - expect(sent_IQ.toLocaleString()).toBe( - ``+ - ``+ - ``+ - `You're annoying`+ - ``+ - ``+ - ``); - - /* - * - * - * - * - * - */ - presence = $pres({ - 'from': 'lounge@montague.lit/annoying guy', - 'to': 'romeo@montague.lit/desktop', - 'type': 'unavailable' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'affiliation': 'none', - 'role': 'none' - }).c('actor', {'nick': 'romeo'}).up() - .c('reason').t("You're annoying").up().up() - .c('status', {'code': '307'}); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2); - expect(view.el.querySelectorAll('.chat-info__message')[1].textContent.trim()).toBe("annoying guy has been kicked out by romeo"); - expect(view.el.querySelector('.chat-info:last-child q').textContent.trim()).toBe("You're annoying"); - done(); - })); - - - it("takes /op and /deop to make a user a moderator or not", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - 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); - }); - spyOn(view.model, 'setRole').and.callThrough(); - spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough(); - - // New user enters the groupchat - /* - * - * - * - * - */ - let presence = $pres({ - 'from': 'lounge@montague.lit/trustworthyguy', - 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'trustworthyguy@montague.lit', - 'affiliation': 'member', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext.trim()).toEqual("romeo and trustworthyguy have entered the groupchat"); - - const textarea = view.el.querySelector('.chat-textarea') - textarea.value = '/op'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - - expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled(); - await u.waitUntil(() => view.el.querySelector('.message:last-child')?.textContent?.trim() === - "Error: the \"op\" command takes two arguments, the user's nickname and optionally a reason."); - - expect(view.model.setRole).not.toHaveBeenCalled(); - // Call now with the correct amount of arguments. - // XXX: Calling onFormSubmitted directly, trying - // again via triggering Event doesn't work for some weird - // reason. - textarea.value = '/op trustworthyguy You\'re trustworthy'; - view.onFormSubmitted(new Event('submit')); - - expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); - expect(view.model.setRole).toHaveBeenCalled(); - expect(sent_IQ.toLocaleString()).toBe( - ``+ - ``+ - ``+ - `You're trustworthy`+ - ``+ - ``+ - ``); - - /* - * - * - * - * - */ - presence = $pres({ - 'from': 'lounge@montague.lit/trustworthyguy', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'trustworthyguy@montague.lit', - 'affiliation': 'member', - 'role': 'moderator' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => - Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === - "trustworthyguy is now a moderator" - ); - - // Call now with the correct amount of arguments. - // XXX: Calling onFormSubmitted directly, trying - // again via triggering Event doesn't work for some weird - // reason. - textarea.value = '/deop trustworthyguy Perhaps not'; - view.onFormSubmitted(new Event('submit')); - - expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3); - expect(view.model.setRole).toHaveBeenCalled(); - expect(sent_IQ.toLocaleString()).toBe( - ``+ - ``+ - ``+ - `Perhaps not`+ - ``+ - ``+ - ``); - - /* - * - * - * - * - */ - presence = $pres({ - 'from': 'lounge@montague.lit/trustworthyguy', - 'to': 'romeo@montague.lit/desktop' - }).c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'trustworthyguy@montague.lit', - 'affiliation': 'member', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => - Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === - "trustworthyguy is no longer a moderator" - ); - done(); - })); - - it("takes /mute and /voice to mute and unmute a user", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - var sent_IQ, IQ_id; - var 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); - }); - spyOn(view.model, 'setRole').and.callThrough(); - spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough(); - - // New user enters the groupchat - /* - * - * - * - * - */ - let presence = $pres({ - 'from': 'lounge@montague.lit/annoyingGuy', - 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'member', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext.trim()).toEqual("romeo and annoyingGuy have entered the groupchat"); - - const textarea = view.el.querySelector('.chat-textarea') - textarea.value = '/mute'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - - expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled(); - await u.waitUntil(() => view.el.querySelector('.message:last-child')?.textContent?.trim() === - "Error: the \"mute\" command takes two arguments, the user's nickname and optionally a reason."); - expect(view.model.setRole).not.toHaveBeenCalled(); - // Call now with the correct amount of arguments. - // XXX: Calling onFormSubmitted directly, trying - // again via triggering Event doesn't work for some weird - // reason. - textarea.value = '/mute annoyingGuy You\'re annoying'; - view.onFormSubmitted(new Event('submit')); - - expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); - expect(view.model.setRole).toHaveBeenCalled(); - expect(sent_IQ.toLocaleString()).toBe( - ``+ - ``+ - ``+ - `You're annoying`+ - ``+ - ``+ - ``); - - /* - * - * - * - * - */ - presence = $pres({ - 'from': 'lounge@montague.lit/annoyingGuy', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'member', - 'role': 'visitor' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => - Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === - "annoyingGuy has been muted" - ); - - // Call now with the correct of arguments. - // XXX: Calling onFormSubmitted directly, trying - // again via triggering Event doesn't work for some weird - // reason. - textarea.value = '/voice annoyingGuy Now you can talk again'; - view.onFormSubmitted(new Event('submit')); - - expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3); - expect(view.model.setRole).toHaveBeenCalled(); - expect(sent_IQ.toLocaleString()).toBe( - ``+ - ``+ - ``+ - `Now you can talk again`+ - ``+ - ``+ - ``); - - /* - * - * - * - * - */ - presence = $pres({ - 'from': 'lounge@montague.lit/annoyingGuy', - 'to': 'romeo@montague.lit/desktop' - }) - .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) - .c('item', { - 'jid': 'annoyingguy@montague.lit', - 'affiliation': 'member', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => - Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === - "annoyingGuy has been given a voice" - ); - done(); - })); - - it("takes /destroy to destroy a muc", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - const new_muc_jid = 'foyer@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - let view = _converse.api.chatviews.get(muc_jid); - spyOn(_converse.api, 'confirm').and.callThrough(); - let textarea = view.el.querySelector('.chat-textarea'); - textarea.value = '/destroy'; - view.onFormSubmitted(new Event('submit')); - let modal = await u.waitUntil(() => document.querySelector('.modal-dialog')); - await u.waitUntil(() => u.isVisible(modal)); - - let challenge_el = modal.querySelector('[name="challenge"]'); - challenge_el.value = muc_jid+'e'; - const reason_el = modal.querySelector('[name="reason"]'); - reason_el.value = 'Moved to a new location'; - const newjid_el = modal.querySelector('[name="newjid"]'); - newjid_el.value = new_muc_jid; - let submit = modal.querySelector('[type="submit"]'); - submit.click(); - expect(u.isVisible(modal)).toBeTruthy(); - expect(u.hasClass('error', challenge_el)).toBeTruthy(); - challenge_el.value = muc_jid; - submit.click(); - - let sent_IQs = _converse.connection.IQ_stanzas; - let sent_IQ = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('destroy')).pop()); - expect(Strophe.serialize(sent_IQ)).toBe( - ``+ - ``+ - ``+ - ``+ - `Moved to a new location`+ - ``+ - ``+ - ``+ - ``); - - let result_stanza = $iq({ - 'type': 'result', - 'id': sent_IQ.getAttribute('id'), - 'from': view.model.get('jid'), - 'to': _converse.connection.jid - }); - spyOn(_converse.api, "trigger").and.callThrough(); - expect(_converse.chatboxes.length).toBe(2); - _converse.connection._dataRecv(test_utils.createRequest(result_stanza)); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED)); - await u.waitUntil(() => _converse.chatboxes.length === 1); - expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); - - // Try again without reason or new JID - _converse.connection.IQ_stanzas = []; - sent_IQs = _converse.connection.IQ_stanzas; - await test_utils.openAndEnterChatRoom(_converse, new_muc_jid, 'romeo'); - view = _converse.api.chatviews.get(new_muc_jid); - textarea = view.el.querySelector('.chat-textarea'); - textarea.value = '/destroy'; - view.onFormSubmitted(new Event('submit')); - modal = await u.waitUntil(() => document.querySelector('.modal-dialog')); - await u.waitUntil(() => u.isVisible(modal)); - - challenge_el = modal.querySelector('[name="challenge"]'); - challenge_el.value = new_muc_jid; - submit = modal.querySelector('[type="submit"]'); - submit.click(); - - sent_IQ = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('destroy')).pop()); - expect(Strophe.serialize(sent_IQ)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - result_stanza = $iq({ - 'type': 'result', - 'id': sent_IQ.getAttribute('id'), - 'from': view.model.get('jid'), - 'to': _converse.connection.jid - }); - expect(_converse.chatboxes.length).toBe(2); - _converse.connection._dataRecv(test_utils.createRequest(result_stanza)); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED)); - await u.waitUntil(() => _converse.chatboxes.length === 1); - done(); - })); - }); - - describe("When attempting to enter a groupchat", function () { - - it("will use the nickname set in the global settings if the user doesn't have a VCard nickname", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'nickname': 'Benedict-Cucumberpatch'}, - async function (done, _converse) { - - await test_utils.openChatRoomViaModal(_converse, 'roomy@muc.montague.lit'); - const view = _converse.chatboxviews.get('roomy@muc.montague.lit'); - expect(view.model.get('nick')).toBe('Benedict-Cucumberpatch'); - done(); - })); - - it("will show an error message if the groupchat requires a password", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'protected'; - await test_utils.openChatRoomViaModal(_converse, muc_jid, 'romeo'); - const view = _converse.chatboxviews.get(muc_jid); - spyOn(view, 'renderPasswordForm').and.callThrough(); - - const presence = $pres().attrs({ - 'from': `${muc_jid}/romeo`, - 'id': u.getUniqueId(), - 'to': 'romeo@montague.lit/pda', - 'type': 'error' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@montague.lit', type:'auth'}) - .c('not-authorized').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}); - - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - const chat_body = view.el.querySelector('.chatroom-body'); - expect(view.renderPasswordForm).toHaveBeenCalled(); - expect(chat_body.querySelectorAll('form.chatroom-form').length).toBe(1); - expect(chat_body.querySelector('.chatroom-form label').textContent.trim()) - .toBe('This groupchat requires a password'); - - // Let's submit the form - spyOn(view.model, 'join'); - const input_el = view.el.querySelector('[name="password"]'); - input_el.value = 'secret'; - view.el.querySelector('input[type=submit]').click(); - expect(view.model.join).toHaveBeenCalledWith('romeo', 'secret'); - done(); - })); - - it("will show an error message if the groupchat is members-only and the user not included", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'members-only@muc.montague.lit' - await test_utils.openChatRoomViaModal(_converse, muc_jid, 'romeo'); - const view = _converse.chatboxviews.get(muc_jid); - const iq = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => iq.querySelector( - `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` - )).pop()); - - // State that the chat is members-only via the features IQ - const features_stanza = $iq({ - 'from': muc_jid, - 'id': iq.getAttribute('id'), - 'to': 'romeo@montague.lit/desktop', - 'type': 'result' - }) - .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', { - 'category': 'conference', - 'name': 'A Dark Cave', - 'type': 'text' - }).up() - .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() - .c('feature', {'var': 'muc_hidden'}).up() - .c('feature', {'var': 'muc_temporary'}).up() - .c('feature', {'var': 'muc_membersonly'}).up(); - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); - await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING); - - const presence = $pres().attrs({ - from: `${muc_jid}/romeo`, - id: u.getUniqueId(), - to: 'romeo@montague.lit/pda', - type: 'error' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@montague.lit', type:'auth'}) - .c('registration-required').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) - .toBe('You are not on the member list of this groupchat.'); - done(); - })); - - it("will show an error message if the user has been banned", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'off-limits@muc.montague.lit' - await test_utils.openChatRoomViaModal(_converse, muc_jid, 'romeo'); - - const iq = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => iq.querySelector( - `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` - )).pop()); - - const features_stanza = $iq({ - 'from': muc_jid, - 'id': iq.getAttribute('id'), - 'to': 'romeo@montague.lit/desktop', - 'type': 'result' - }) - .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() - .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() - .c('feature', {'var': 'muc_hidden'}).up() - .c('feature', {'var': 'muc_temporary'}).up() - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); - - const view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING); - - const presence = $pres().attrs({ - from: `${muc_jid}/romeo`, - id: u.getUniqueId(), - to: 'romeo@montague.lit/pda', - type: 'error' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@montague.lit', type:'auth'}) - .c('forbidden').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) - .toBe('You have been banned from this groupchat.'); - done(); - })); - - it("will render a nickname form if a nickname conflict happens and muc_nickname_from_jid=false", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'conflicted@muc.montague.lit'; - await test_utils.openChatRoomViaModal(_converse, muc_jid, 'romeo'); - var presence = $pres().attrs({ - from: `${muc_jid}/romeo`, - id: u.getUniqueId(), - to: 'romeo@montague.lit/pda', - type: 'error' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) - .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - - const view = _converse.chatboxviews.get(muc_jid); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(sizzle('.chatroom-body form.chatroom-form label:first', view.el).pop().textContent.trim()) - .toBe('Please choose your nickname'); - - const input = sizzle('.chatroom-body form.chatroom-form input:first', view.el).pop(); - input.value = 'nicky'; - view.el.querySelector('input[type=submit]').click(); - done(); - })); - - - it("will automatically choose a new nickname if a nickname conflict happens and muc_nickname_from_jid=true", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'conflicting@muc.montague.lit' - await test_utils.openChatRoomViaModal(_converse, muc_jid, 'romeo'); - /* - * - * - * - * - * - */ - _converse.muc_nickname_from_jid = true; - - const attrs = { + .c('reason').t("You're annoying").up().up() + .c('status', {'code': '307'}); + _converse.connection._dataRecv(mock.createRequest(presence)); + + await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2); + expect(view.el.querySelectorAll('.chat-info__message')[1].textContent.trim()).toBe("annoying guy has been kicked out by romeo"); + expect(view.el.querySelector('.chat-info:last-child q').textContent.trim()).toBe("You're annoying"); + done(); + })); + + + it("takes /op and /deop to make a user a moderator or not", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + 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); + }); + spyOn(view.model, 'setRole').and.callThrough(); + spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough(); + + // New user enters the groupchat + /* + * + * + * + * + */ + let presence = $pres({ + 'from': 'lounge@montague.lit/trustworthyguy', + 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'trustworthyguy@montague.lit', + 'affiliation': 'member', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext.trim()).toEqual("romeo and trustworthyguy have entered the groupchat"); + + const textarea = view.el.querySelector('.chat-textarea') + textarea.value = '/op'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + + expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled(); + await u.waitUntil(() => view.el.querySelector('.message:last-child')?.textContent?.trim() === + "Error: the \"op\" command takes two arguments, the user's nickname and optionally a reason."); + + expect(view.model.setRole).not.toHaveBeenCalled(); + // Call now with the correct amount of arguments. + // XXX: Calling onFormSubmitted directly, trying + // again via triggering Event doesn't work for some weird + // reason. + textarea.value = '/op trustworthyguy You\'re trustworthy'; + view.onFormSubmitted(new Event('submit')); + + expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); + expect(view.model.setRole).toHaveBeenCalled(); + expect(sent_IQ.toLocaleString()).toBe( + ``+ + ``+ + ``+ + `You're trustworthy`+ + ``+ + ``+ + ``); + + /* + * + * + * + * + */ + presence = $pres({ + 'from': 'lounge@montague.lit/trustworthyguy', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'trustworthyguy@montague.lit', + 'affiliation': 'member', + 'role': 'moderator' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => + Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === + "trustworthyguy is now a moderator" + ); + + // Call now with the correct amount of arguments. + // XXX: Calling onFormSubmitted directly, trying + // again via triggering Event doesn't work for some weird + // reason. + textarea.value = '/deop trustworthyguy Perhaps not'; + view.onFormSubmitted(new Event('submit')); + + expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3); + expect(view.model.setRole).toHaveBeenCalled(); + expect(sent_IQ.toLocaleString()).toBe( + ``+ + ``+ + ``+ + `Perhaps not`+ + ``+ + ``+ + ``); + + /* + * + * + * + * + */ + presence = $pres({ + 'from': 'lounge@montague.lit/trustworthyguy', + 'to': 'romeo@montague.lit/desktop' + }).c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'trustworthyguy@montague.lit', + 'affiliation': 'member', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => + Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === + "trustworthyguy is no longer a moderator" + ); + done(); + })); + + it("takes /mute and /voice to mute and unmute a user", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + var sent_IQ, IQ_id; + var 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); + }); + spyOn(view.model, 'setRole').and.callThrough(); + spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough(); + + // New user enters the groupchat + /* + * + * + * + * + */ + let presence = $pres({ + 'from': 'lounge@montague.lit/annoyingGuy', + 'id':'27C55F89-1C6A-459A-9EB5-77690145D624', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'annoyingguy@montague.lit', + 'affiliation': 'member', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext.trim()).toEqual("romeo and annoyingGuy have entered the groupchat"); + + const textarea = view.el.querySelector('.chat-textarea') + textarea.value = '/mute'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + + expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled(); + await u.waitUntil(() => view.el.querySelector('.message:last-child')?.textContent?.trim() === + "Error: the \"mute\" command takes two arguments, the user's nickname and optionally a reason."); + expect(view.model.setRole).not.toHaveBeenCalled(); + // Call now with the correct amount of arguments. + // XXX: Calling onFormSubmitted directly, trying + // again via triggering Event doesn't work for some weird + // reason. + textarea.value = '/mute annoyingGuy You\'re annoying'; + view.onFormSubmitted(new Event('submit')); + + expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); + expect(view.model.setRole).toHaveBeenCalled(); + expect(sent_IQ.toLocaleString()).toBe( + ``+ + ``+ + ``+ + `You're annoying`+ + ``+ + ``+ + ``); + + /* + * + * + * + * + */ + presence = $pres({ + 'from': 'lounge@montague.lit/annoyingGuy', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'annoyingguy@montague.lit', + 'affiliation': 'member', + 'role': 'visitor' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => + Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === + "annoyingGuy has been muted" + ); + + // Call now with the correct of arguments. + // XXX: Calling onFormSubmitted directly, trying + // again via triggering Event doesn't work for some weird + // reason. + textarea.value = '/voice annoyingGuy Now you can talk again'; + view.onFormSubmitted(new Event('submit')); + + expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3); + expect(view.model.setRole).toHaveBeenCalled(); + expect(sent_IQ.toLocaleString()).toBe( + ``+ + ``+ + ``+ + `Now you can talk again`+ + ``+ + ``+ + ``); + + /* + * + * + * + * + */ + presence = $pres({ + 'from': 'lounge@montague.lit/annoyingGuy', + 'to': 'romeo@montague.lit/desktop' + }) + .c('x', { 'xmlns': 'http://jabber.org/protocol/muc#user'}) + .c('item', { + 'jid': 'annoyingguy@montague.lit', + 'affiliation': 'member', + 'role': 'participant' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => + Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === + "annoyingGuy has been given a voice" + ); + done(); + })); + + it("takes /destroy to destroy a muc", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + const new_muc_jid = 'foyer@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + let view = _converse.api.chatviews.get(muc_jid); + spyOn(_converse.api, 'confirm').and.callThrough(); + let textarea = view.el.querySelector('.chat-textarea'); + textarea.value = '/destroy'; + view.onFormSubmitted(new Event('submit')); + let modal = await u.waitUntil(() => document.querySelector('.modal-dialog')); + await u.waitUntil(() => u.isVisible(modal)); + + let challenge_el = modal.querySelector('[name="challenge"]'); + challenge_el.value = muc_jid+'e'; + const reason_el = modal.querySelector('[name="reason"]'); + reason_el.value = 'Moved to a new location'; + const newjid_el = modal.querySelector('[name="newjid"]'); + newjid_el.value = new_muc_jid; + let submit = modal.querySelector('[type="submit"]'); + submit.click(); + expect(u.isVisible(modal)).toBeTruthy(); + expect(u.hasClass('error', challenge_el)).toBeTruthy(); + challenge_el.value = muc_jid; + submit.click(); + + let sent_IQs = _converse.connection.IQ_stanzas; + let sent_IQ = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('destroy')).pop()); + expect(Strophe.serialize(sent_IQ)).toBe( + ``+ + ``+ + ``+ + ``+ + `Moved to a new location`+ + ``+ + ``+ + ``+ + ``); + + let result_stanza = $iq({ + 'type': 'result', + 'id': sent_IQ.getAttribute('id'), + 'from': view.model.get('jid'), + 'to': _converse.connection.jid + }); + spyOn(_converse.api, "trigger").and.callThrough(); + expect(_converse.chatboxes.length).toBe(2); + _converse.connection._dataRecv(mock.createRequest(result_stanza)); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED)); + await u.waitUntil(() => _converse.chatboxes.length === 1); + expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); + + // Try again without reason or new JID + _converse.connection.IQ_stanzas = []; + sent_IQs = _converse.connection.IQ_stanzas; + await mock.openAndEnterChatRoom(_converse, new_muc_jid, 'romeo'); + view = _converse.api.chatviews.get(new_muc_jid); + textarea = view.el.querySelector('.chat-textarea'); + textarea.value = '/destroy'; + view.onFormSubmitted(new Event('submit')); + modal = await u.waitUntil(() => document.querySelector('.modal-dialog')); + await u.waitUntil(() => u.isVisible(modal)); + + challenge_el = modal.querySelector('[name="challenge"]'); + challenge_el.value = new_muc_jid; + submit = modal.querySelector('[type="submit"]'); + submit.click(); + + sent_IQ = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('destroy')).pop()); + expect(Strophe.serialize(sent_IQ)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + result_stanza = $iq({ + 'type': 'result', + 'id': sent_IQ.getAttribute('id'), + 'from': view.model.get('jid'), + 'to': _converse.connection.jid + }); + expect(_converse.chatboxes.length).toBe(2); + _converse.connection._dataRecv(mock.createRequest(result_stanza)); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED)); + await u.waitUntil(() => _converse.chatboxes.length === 1); + done(); + })); + }); + + describe("When attempting to enter a groupchat", function () { + + it("will use the nickname set in the global settings if the user doesn't have a VCard nickname", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'nickname': 'Benedict-Cucumberpatch'}, + async function (done, _converse) { + + await mock.openChatRoomViaModal(_converse, 'roomy@muc.montague.lit'); + const view = _converse.chatboxviews.get('roomy@muc.montague.lit'); + expect(view.model.get('nick')).toBe('Benedict-Cucumberpatch'); + done(); + })); + + it("will show an error message if the groupchat requires a password", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'protected'; + await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); + const view = _converse.chatboxviews.get(muc_jid); + spyOn(view, 'renderPasswordForm').and.callThrough(); + + const presence = $pres().attrs({ 'from': `${muc_jid}/romeo`, 'id': u.getUniqueId(), 'to': 'romeo@montague.lit/pda', 'type': 'error' - }; - let presence = $pres().attrs(attrs) - .c('x').attrs({'xmlns':'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({'by': muc_jid, 'type':'cancel'}) - .c('conflict').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({by:'lounge@montague.lit', type:'auth'}) + .c('not-authorized').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}); - const view = _converse.chatboxviews.get(muc_jid); - spyOn(view.model, 'join').and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(presence)); - // Simulate repeatedly that there's already someone in the groupchat - // with that nickname - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.model.join).toHaveBeenCalledWith('romeo-2'); + const chat_body = view.el.querySelector('.chatroom-body'); + expect(view.renderPasswordForm).toHaveBeenCalled(); + expect(chat_body.querySelectorAll('form.chatroom-form').length).toBe(1); + expect(chat_body.querySelector('.chatroom-form label').textContent.trim()) + .toBe('This groupchat requires a password'); - attrs.from = `${muc_jid}/romeo-2`; - attrs.id = u.getUniqueId(); - presence = $pres().attrs(attrs) - .c('x').attrs({'xmlns':'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({'by': muc_jid, type:'cancel'}) - .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(presence)); + // Let's submit the form + spyOn(view.model, 'join'); + const input_el = view.el.querySelector('[name="password"]'); + input_el.value = 'secret'; + view.el.querySelector('input[type=submit]').click(); + expect(view.model.join).toHaveBeenCalledWith('romeo', 'secret'); + done(); + })); - expect(view.model.join).toHaveBeenCalledWith('romeo-3'); + it("will show an error message if the groupchat is members-only and the user not included", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - attrs.from = `${muc_jid}/romeo-3`; - attrs.id = new Date().getTime(); - presence = $pres().attrs(attrs) - .c('x').attrs({'xmlns': 'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({'by': muc_jid, 'type': 'cancel'}) - .c('conflict').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.model.join).toHaveBeenCalledWith('romeo-4'); - done(); - })); + const muc_jid = 'members-only@muc.montague.lit' + await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); + const view = _converse.chatboxviews.get(muc_jid); + const iq = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); - it("will show an error message if the user is not allowed to have created the groupchat", + // State that the chat is members-only via the features IQ + const features_stanza = $iq({ + 'from': muc_jid, + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }) + .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', { + 'category': 'conference', + 'name': 'A Dark Cave', + 'type': 'text' + }).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + .c('feature', {'var': 'muc_hidden'}).up() + .c('feature', {'var': 'muc_temporary'}).up() + .c('feature', {'var': 'muc_membersonly'}).up(); + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING); + + const presence = $pres().attrs({ + from: `${muc_jid}/romeo`, + id: u.getUniqueId(), + to: 'romeo@montague.lit/pda', + type: 'error' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({by:'lounge@montague.lit', type:'auth'}) + .c('registration-required').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; + + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) + .toBe('You are not on the member list of this groupchat.'); + done(); + })); + + it("will show an error message if the user has been banned", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'off-limits@muc.montague.lit' + await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); + + const iq = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + + const features_stanza = $iq({ + 'from': muc_jid, + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }) + .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + .c('feature', {'var': 'muc_hidden'}).up() + .c('feature', {'var': 'muc_temporary'}).up() + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING); + + const presence = $pres().attrs({ + from: `${muc_jid}/romeo`, + id: u.getUniqueId(), + to: 'romeo@montague.lit/pda', + type: 'error' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({by:'lounge@montague.lit', type:'auth'}) + .c('forbidden').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; + + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) + .toBe('You have been banned from this groupchat.'); + done(); + })); + + it("will render a nickname form if a nickname conflict happens and muc_nickname_from_jid=false", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'conflicted@muc.montague.lit'; + await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); + var presence = $pres().attrs({ + from: `${muc_jid}/romeo`, + id: u.getUniqueId(), + to: 'romeo@montague.lit/pda', + type: 'error' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) + .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; + + const view = _converse.chatboxviews.get(muc_jid); + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(sizzle('.chatroom-body form.chatroom-form label:first', view.el).pop().textContent.trim()) + .toBe('Please choose your nickname'); + + const input = sizzle('.chatroom-body form.chatroom-form input:first', view.el).pop(); + input.value = 'nicky'; + view.el.querySelector('input[type=submit]').click(); + done(); + })); + + + it("will automatically choose a new nickname if a nickname conflict happens and muc_nickname_from_jid=true", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'conflicting@muc.montague.lit' + await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); + /* + * + * + * + * + * + */ + _converse.muc_nickname_from_jid = true; + + const attrs = { + 'from': `${muc_jid}/romeo`, + 'id': u.getUniqueId(), + 'to': 'romeo@montague.lit/pda', + 'type': 'error' + }; + let presence = $pres().attrs(attrs) + .c('x').attrs({'xmlns':'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({'by': muc_jid, 'type':'cancel'}) + .c('conflict').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; + + const view = _converse.chatboxviews.get(muc_jid); + spyOn(view.model, 'join').and.callThrough(); + + // Simulate repeatedly that there's already someone in the groupchat + // with that nickname + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.model.join).toHaveBeenCalledWith('romeo-2'); + + attrs.from = `${muc_jid}/romeo-2`; + attrs.id = u.getUniqueId(); + presence = $pres().attrs(attrs) + .c('x').attrs({'xmlns':'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({'by': muc_jid, type:'cancel'}) + .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; + _converse.connection._dataRecv(mock.createRequest(presence)); + + expect(view.model.join).toHaveBeenCalledWith('romeo-3'); + + attrs.from = `${muc_jid}/romeo-3`; + attrs.id = new Date().getTime(); + presence = $pres().attrs(attrs) + .c('x').attrs({'xmlns': 'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({'by': muc_jid, 'type': 'cancel'}) + .c('conflict').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.model.join).toHaveBeenCalledWith('romeo-4'); + done(); + })); + + it("will show an error message if the user is not allowed to have created the groupchat", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'impermissable@muc.montague.lit' + await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo') + + // We pretend this is a new room, so no disco info is returned. + const iq = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + const features_stanza = $iq({ + 'from': 'room@conference.example.org', + 'id': iq.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(muc_jid); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); + + const presence = $pres().attrs({ + from: `${muc_jid}/romeo`, + id: u.getUniqueId(), + to:'romeo@montague.lit/pda', + type:'error' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) + .c('not-allowed').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) + .toBe('You are not allowed to create new groupchats.'); + done(); + })); + + it("will show an error message if the user's nickname doesn't conform to groupchat policy", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'conformist@muc.montague.lit' + await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); + + const iq = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + const features_stanza = $iq({ + 'from': muc_jid, + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); + + const presence = $pres().attrs({ + from: `${muc_jid}/romeo`, + id: u.getUniqueId(), + to:'romeo@montague.lit/pda', + type:'error' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) + .c('not-acceptable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; + + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) + .toBe("Your nickname doesn't conform to this groupchat's policies."); + done(); + })); + + it("will show an error message if the groupchat doesn't yet exist", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'nonexistent@muc.montague.lit' + await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); + + const iq = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + const features_stanza = $iq({ + 'from': muc_jid, + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); + + const presence = $pres().attrs({ + from: `${muc_jid}/romeo`, + id: u.getUniqueId(), + to: 'romeo@montague.lit/pda', + type:'error' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) + .c('item-not-found').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; + + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) + .toBe("This groupchat does not (yet) exist."); + done(); + })); + + it("will show an error message if the groupchat has reached its maximum number of participants", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'maxed-out@muc.montague.lit' + await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo') + + const iq = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + const features_stanza = $iq({ + 'from': muc_jid, + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); + + const presence = $pres().attrs({ + from: `${muc_jid}/romeo`, + id: u.getUniqueId(), + to:'romeo@montague.lit/pda', + type:'error' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) + .c('service-unavailable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; + + _converse.connection._dataRecv(mock.createRequest(presence)); + expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) + .toBe("This groupchat has reached its maximum number of participants."); + done(); + })); + }); + + describe("Someone being invited to a groupchat", function () { + + it("will first be added to the member list if the groupchat is members only", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 0); + spyOn(_converse.ChatRoomOccupants.prototype, 'fetchMembers').and.callThrough(); + const sent_IQs = _converse.connection.IQ_stanzas; + const muc_jid = 'coven@chat.shakespeare.lit'; + + const room_creation_promise = _converse.api.rooms.open(muc_jid, {'nick': 'romeo'}); + + // Check that the groupchat queried for the features. + let stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`)).pop()); + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``); + + // State that the chat is members-only via the features IQ + const view = _converse.chatboxviews.get(muc_jid); + const features_stanza = $iq({ + from: 'coven@chat.shakespeare.lit', + 'id': stanza.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }) + .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', { + 'category': 'conference', + 'name': 'A Dark Cave', + 'type': 'text' + }).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + .c('feature', {'var': 'muc_hidden'}).up() + .c('feature', {'var': 'muc_temporary'}).up() + .c('feature', {'var': 'muc_membersonly'}).up(); + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); + expect(view.model.features.get('membersonly')).toBeTruthy(); + + await room_creation_promise; + + await mock.createContacts(_converse, 'current'); + + let sent_stanza, sent_id; + spyOn(_converse.connection, 'send').and.callFake(function (stanza) { + if (stanza.nodeTree && stanza.nodeTree.nodeName === 'message') { + sent_id = stanza.nodeTree.getAttribute('id'); + sent_stanza = stanza; + } + }); + const invitee_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const reason = "Please join this groupchat"; + view.model.directInvite(invitee_jid, reason); + + // 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( + ``+ + ``+ + ``); + + // Now the service sends the member lists to the user + const member_list_stanza = $iq({ + 'from': 'coven@chat.shakespeare.lit', + '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)); + + const admin_list_stanza = $iq({ + 'from': 'coven@chat.shakespeare.lit', + 'id': admin_iq.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) + .c('item', { + 'affiliation': 'admin', + 'jid': 'wiccarocks@shakespeare.lit', + 'nick': 'secondwitch' + }); + _converse.connection._dataRecv(mock.createRequest(admin_list_stanza)); + + const owner_list_stanza = $iq({ + 'from': 'coven@chat.shakespeare.lit', + 'id': owner_iq.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) + .c('item', { + 'affiliation': 'owner', + 'jid': 'crone1@shakespeare.lit', + }); + _converse.connection._dataRecv(mock.createRequest(owner_list_stanza)); + + // Converse puts the user on the member list + stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/muc#admin"]`)).pop()); + expect(stanza.outerHTML, + ``+ + ``+ + ``+ + `Please join this groupchat`+ + ``+ + ``+ + ``); + + const result = $iq({ + 'from': 'coven@chat.shakespeare.lit', + 'id': stanza.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }); + _converse.connection._dataRecv(mock.createRequest(result)); + + await u.waitUntil(() => view.model.occupants.fetchMembers.calls.count()); + + // Finally check that the user gets invited. + expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec) + ``+ + ``+ + `` + ); + done(); + })); + }); + + describe("The affiliations delta", function () { + + it("can be computed in various ways", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.openChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'romeo'); + var exclude_existing = false; + var remove_absentees = false; + var new_list = []; + var old_list = []; + const muc_utils = converse.env.muc_utils; + var delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); + expect(delta.length).toBe(0); + + new_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; + old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; + delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); + expect(delta.length).toBe(0); + + // When remove_absentees is false, then affiliations in the old + // list which are not in the new one won't be removed. + old_list = [{'jid': 'oldhag666@shakespeare.lit', 'affiliation': 'owner'}, + {'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; + delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); + expect(delta.length).toBe(0); + + // With exclude_existing set to false, any changed affiliations + // will be included in the delta (i.e. existing affiliations are included in the comparison). + old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'owner'}]; + delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); + expect(delta.length).toBe(1); + expect(delta[0].jid).toBe('wiccarocks@shakespeare.lit'); + expect(delta[0].affiliation).toBe('member'); + + // To also remove affiliations from the old list which are not + // in the new list, we set remove_absentees to true + remove_absentees = true; + old_list = [{'jid': 'oldhag666@shakespeare.lit', 'affiliation': 'owner'}, + {'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; + delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); + expect(delta.length).toBe(1); + expect(delta[0].jid).toBe('oldhag666@shakespeare.lit'); + expect(delta[0].affiliation).toBe('none'); + + delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, [], old_list); + expect(delta.length).toBe(2); + expect(delta[0].jid).toBe('oldhag666@shakespeare.lit'); + expect(delta[0].affiliation).toBe('none'); + expect(delta[1].jid).toBe('wiccarocks@shakespeare.lit'); + expect(delta[1].affiliation).toBe('none'); + + // To only add a user if they don't already have an + // affiliation, we set 'exclude_existing' to true + exclude_existing = true; + old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'owner'}]; + delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); + expect(delta.length).toBe(0); + + old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'admin'}]; + delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); + expect(delta.length).toBe(0); + done(); + })); + }); + + describe("The \"Groupchats\" Add modal", function () { + + it("can be opened from a link in the \"Groupchats\" section of the controlbox", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 0); + + const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; + roomspanel.el.querySelector('.show-add-muc-modal').click(); + mock.closeControlBox(_converse); + const modal = roomspanel.add_room_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000) + + let label_name = modal.el.querySelector('label[for="chatroom"]'); + expect(label_name.textContent.trim()).toBe('Groupchat address:'); + let name_input = modal.el.querySelector('input[name="chatroom"]'); + expect(name_input.placeholder).toBe('name@conference.example.org'); + + const label_nick = modal.el.querySelector('label[for="nickname"]'); + expect(label_nick.textContent.trim()).toBe('Nickname:'); + const nick_input = modal.el.querySelector('input[name="nickname"]'); + expect(nick_input.value).toBe(''); + nick_input.value = 'romeo'; + + expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat'); + spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); + roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + modal.el.querySelector('input[name="chatroom"]').value = 'lounce@muc.montague.lit'; + modal.el.querySelector('form input[type="submit"]').click(); + await u.waitUntil(() => _converse.chatboxes.length); + await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1); + + roomspanel.model.set('muc_domain', 'muc.example.org'); + roomspanel.el.querySelector('.show-add-muc-modal').click(); + label_name = modal.el.querySelector('label[for="chatroom"]'); + expect(label_name.textContent.trim()).toBe('Groupchat address:'); + name_input = modal.el.querySelector('input[name="chatroom"]'); + expect(name_input.placeholder).toBe('name@muc.example.org'); + done(); + })); + + it("doesn't show the nickname field if locked_muc_nickname is true", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'locked_muc_nickname': true, 'muc_nickname_from_jid': true}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 0); + const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; + roomspanel.el.querySelector('.show-add-muc-modal').click(); + mock.closeControlBox(_converse); + const modal = roomspanel.add_room_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000) + const name_input = modal.el.querySelector('input[name="chatroom"]'); + name_input.value = 'lounge@montague.lit'; + expect(modal.el.querySelector('label[for="nickname"]')).toBe(null); + expect(modal.el.querySelector('input[name="nickname"]')).toBe(null); + modal.el.querySelector('form input[type="submit"]').click(); + await u.waitUntil(() => _converse.chatboxes.length > 1); + const chatroom = _converse.chatboxes.get('lounge@montague.lit'); + expect(chatroom.get('nick')).toBe('romeo'); + done(); + })); + + it("uses the JID node if muc_nickname_from_jid is set to true", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_nickname_from_jid': true}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 0); + const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; + roomspanel.el.querySelector('.show-add-muc-modal').click(); + mock.closeControlBox(_converse); + const modal = roomspanel.add_room_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000) + const label_nick = modal.el.querySelector('label[for="nickname"]'); + expect(label_nick.textContent.trim()).toBe('Nickname:'); + const nick_input = modal.el.querySelector('input[name="nickname"]'); + expect(nick_input.value).toBe('romeo'); + done(); + })); + + it("uses the nickname passed in to converse.initialize", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'nickname': 'st.nick'}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 0); + const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; + roomspanel.el.querySelector('.show-add-muc-modal').click(); + mock.closeControlBox(_converse); + const modal = roomspanel.add_room_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000) + const label_nick = modal.el.querySelector('label[for="nickname"]'); + expect(label_nick.textContent.trim()).toBe('Nickname:'); + const nick_input = modal.el.querySelector('input[name="nickname"]'); + expect(nick_input.value).toBe('st.nick'); + done(); + })); + + it("doesn't require the domain when muc_domain is set", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org'}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; + roomspanel.el.querySelector('.show-add-muc-modal').click(); + const modal = roomspanel.add_room_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000) + expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat'); + spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); + roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + const label_name = modal.el.querySelector('label[for="chatroom"]'); + expect(label_name.textContent.trim()).toBe('Groupchat name:'); + let name_input = modal.el.querySelector('input[name="chatroom"]'); + expect(name_input.placeholder).toBe('name@muc.example.org'); + name_input.value = 'lounge'; + let nick_input = modal.el.querySelector('input[name="nickname"]'); + nick_input.value = 'max'; + + modal.el.querySelector('form input[type="submit"]').click(); + await u.waitUntil(() => _converse.chatboxes.length); + await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1); + expect(_.includes(_converse.chatboxes.models.map(m => m.get('id')), 'lounge@muc.example.org')).toBe(true); + + // However, you can still open MUCs with different domains + roomspanel.el.querySelector('.show-add-muc-modal').click(); + await u.waitUntil(() => u.isVisible(modal.el), 1000); + name_input = modal.el.querySelector('input[name="chatroom"]'); + name_input.value = 'lounge@conference.example.org'; + nick_input = modal.el.querySelector('input[name="nickname"]'); + nick_input.value = 'max'; + modal.el.querySelector('form input[type="submit"]').click(); + await u.waitUntil(() => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2); + await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 2); + expect(_.includes(_converse.chatboxes.models.map(m => m.get('id')), 'lounge@conference.example.org')).toBe(true); + done(); + })); + + it("only uses the muc_domain is locked_muc_domain is true", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org', 'locked_muc_domain': true}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; + roomspanel.el.querySelector('.show-add-muc-modal').click(); + const modal = roomspanel.add_room_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000) + expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat'); + spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); + roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + const label_name = modal.el.querySelector('label[for="chatroom"]'); + expect(label_name.textContent.trim()).toBe('Groupchat name:'); + let name_input = modal.el.querySelector('input[name="chatroom"]'); + expect(name_input.placeholder).toBe(''); + name_input.value = 'lounge'; + let nick_input = modal.el.querySelector('input[name="nickname"]'); + nick_input.value = 'max'; + modal.el.querySelector('form input[type="submit"]').click(); + await u.waitUntil(() => _converse.chatboxes.length); + await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1); + expect(_.includes(_converse.chatboxes.models.map(m => m.get('id')), 'lounge@muc.example.org')).toBe(true); + + // However, you can still open MUCs with different domains + roomspanel.el.querySelector('.show-add-muc-modal').click(); + await u.waitUntil(() => u.isVisible(modal.el), 1000); + name_input = modal.el.querySelector('input[name="chatroom"]'); + name_input.value = 'lounge@conference'; + nick_input = modal.el.querySelector('input[name="nickname"]'); + nick_input.value = 'max'; + modal.el.querySelector('form input[type="submit"]').click(); + await u.waitUntil(() => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2); + await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 2); + expect(_.includes(_converse.chatboxes.models.map(m => m.get('id')), 'lounge\\40conference@muc.example.org')).toBe(true); + done(); + })); + }); + + describe("The \"Groupchats\" List modal", function () { + + it("can be opened from a link in the \"Groupchats\" section of the controlbox", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; + roomspanel.el.querySelector('.show-list-muc-modal').click(); + mock.closeControlBox(_converse); + const modal = roomspanel.list_rooms_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); + roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + + // See: https://xmpp.org/extensions/xep-0045.html#disco-rooms + expect(modal.el.querySelectorAll('.available-chatrooms li').length).toBe(0); + + const server_input = modal.el.querySelector('input[name="server"]'); + expect(server_input.placeholder).toBe('conference.example.org'); + server_input.value = 'chat.shakespeare.lit'; + modal.el.querySelector('input[type="submit"]').click(); + await u.waitUntil(() => _converse.chatboxes.length); + + const IQ_stanzas = _converse.connection.IQ_stanzas; + const sent_stanza = await u.waitUntil( + () => IQ_stanzas.filter(s => sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"]`, s).length).pop() + ); + const id = sent_stanza.getAttribute('id'); + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + ``+ + `` + ); + const iq = $iq({ + 'from':'muc.montague.lit', + 'to':'romeo@montague.lit/pda', + 'id': id, + 'type':'result' + }).c('query') + .c('item', { jid:'heath@chat.shakespeare.lit', name:'A Lonely Heath'}).up() + .c('item', { jid:'coven@chat.shakespeare.lit', name:'A Dark Cave'}).up() + .c('item', { jid:'forres@chat.shakespeare.lit', name:'The Palace'}).up() + .c('item', { jid:'inverness@chat.shakespeare.lit', name:'Macbeth's Castle'}).up() + .c('item', { jid:'orchard@chat.shakespeare.lit', name:'Capulet\'s Orchard'}).up() + .c('item', { jid:'friar@chat.shakespeare.lit', name:'Friar Laurence\'s cell'}).up() + .c('item', { jid:'hall@chat.shakespeare.lit', name:'Hall in Capulet\'s house'}).up() + .c('item', { jid:'chamber@chat.shakespeare.lit', name:'Juliet\'s chamber'}).up() + .c('item', { jid:'public@chat.shakespeare.lit', name:'A public place'}).up() + .c('item', { jid:'street@chat.shakespeare.lit', name:'A street'}).nodeTree; + _converse.connection._dataRecv(mock.createRequest(iq)); + + await u.waitUntil(() => modal.el.querySelectorAll('.available-chatrooms li').length === 11); + const rooms = modal.el.querySelectorAll('.available-chatrooms li'); + expect(rooms[0].textContent.trim()).toBe("Groupchats found:"); + expect(rooms[1].textContent.trim()).toBe("A Lonely Heath"); + expect(rooms[2].textContent.trim()).toBe("A Dark Cave"); + expect(rooms[3].textContent.trim()).toBe("The Palace"); + expect(rooms[4].textContent.trim()).toBe("Macbeth's Castle"); + expect(rooms[5].textContent.trim()).toBe('Capulet\'s Orchard'); + expect(rooms[6].textContent.trim()).toBe('Friar Laurence\'s cell'); + expect(rooms[7].textContent.trim()).toBe('Hall in Capulet\'s house'); + expect(rooms[8].textContent.trim()).toBe('Juliet\'s chamber'); + expect(rooms[9].textContent.trim()).toBe('A public place'); + expect(rooms[10].textContent.trim()).toBe('A street'); + + rooms[4].querySelector('.open-room').click(); + await u.waitUntil(() => _converse.chatboxes.length > 1); + expect(sizzle('.chatroom', _converse.el).filter(u.isVisible).length).toBe(1); // There should now be an open chatroom + var view = _converse.chatboxviews.get('inverness@chat.shakespeare.lit'); + expect(view.el.querySelector('.chatbox-title__text').textContent.trim()).toBe("Macbeth's Castle"); + done(); + })); + + it("is pre-filled with the muc_domain", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], + {'muc_domain': 'muc.example.org'}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; + roomspanel.el.querySelector('.show-list-muc-modal').click(); + mock.closeControlBox(_converse); + const modal = roomspanel.list_rooms_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + const server_input = modal.el.querySelector('input[name="server"]'); + expect(server_input.value).toBe('muc.example.org'); + done(); + })); + + it("doesn't let you set the MUC domain if it's locked", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], + {'muc_domain': 'chat.shakespeare.lit', 'locked_muc_domain': true}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; + roomspanel.el.querySelector('.show-list-muc-modal').click(); + mock.closeControlBox(_converse); + const modal = roomspanel.list_rooms_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); + roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + + expect(modal.el.querySelector('input[name="server"]')).toBe(null); + expect(modal.el.querySelector('input[type="submit"]')).toBe(null); + await u.waitUntil(() => _converse.chatboxes.length); + const sent_stanza = await u.waitUntil(() => + _converse.connection.sent_stanzas.filter( + s => sizzle(`query[xmlns="http://jabber.org/protocol/disco#items"]`, s).length).pop() + ); + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + ``+ + `` + ); + const iq = $iq({ + from:'muc.montague.lit', + to:'romeo@montague.lit/pda', + id: sent_stanza.getAttribute('id'), + type:'result' + }).c('query') + .c('item', { jid:'heath@chat.shakespeare.lit', name:'A Lonely Heath'}).up() + .c('item', { jid:'coven@chat.shakespeare.lit', name:'A Dark Cave'}).up() + .c('item', { jid:'forres@chat.shakespeare.lit', name:'The Palace'}).up() + _converse.connection._dataRecv(mock.createRequest(iq)); + + await u.waitUntil(() => modal.el.querySelectorAll('.available-chatrooms li').length === 4); + const rooms = modal.el.querySelectorAll('.available-chatrooms li'); + expect(rooms[0].textContent.trim()).toBe("Groupchats found:"); + expect(rooms[1].textContent.trim()).toBe("A Lonely Heath"); + expect(rooms[2].textContent.trim()).toBe("A Dark Cave"); + expect(rooms[3].textContent.trim()).toBe("The Palace"); + done(); + })); + }); + + describe("The \"Groupchats\" section", function () { + + it("shows the number of unread mentions received", + mock.initConverse( + ['rosterGroupsFetched'], {'allow_bookmarks': false}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; + expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(0); + + const muc_jid = 'kitchen@conference.shakespeare.lit'; + const message = 'fires: Your attention is required'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'fires'); + const view = _converse.api.chatviews.get(muc_jid); + await u.waitUntil(() => roomspanel.el.querySelectorAll('.available-room').length); + expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(1); + expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(0); + + view.model.set({'minimized': true}); + + const nick = mock.chatroom_names[0]; + + await view.model.queueMessage($msg({ + from: muc_jid+'/'+nick, + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t(message).tree()); + await u.waitUntil(() => view.model.messages.length); + expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(1); + expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(1); + expect(roomspanel.el.querySelector('.msgs-indicator').textContent.trim()).toBe('1'); + + await view.model.queueMessage($msg({ + 'from': muc_jid+'/'+nick, + 'id': u.getUniqueId(), + 'to': 'romeo@montague.lit', + 'type': 'groupchat' + }).c('body').t(message).tree()); + await u.waitUntil(() => view.model.messages.length > 1); + expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(1); + expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(1); + expect(roomspanel.el.querySelector('.msgs-indicator').textContent.trim()).toBe('2'); + view.model.set({'minimized': false}); + expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(1); + expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(0); + done(); + })); + }); + + describe("A XEP-0085 Chat Status Notification", function () { + + it("is is not sent out to a MUC if the user is a visitor in a moderated room", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + spyOn(_converse.ChatRoom.prototype, 'sendChatState').and.callThrough(); + + const muc_jid = 'lounge@montague.lit'; + const features = [ + 'http://jabber.org/protocol/muc', + 'jabber:iq:register', + 'muc_passwordprotected', + 'muc_hidden', + 'muc_temporary', + 'muc_membersonly', + 'muc_moderated', + 'muc_anonymous' + ] + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + + const view = _converse.api.chatviews.get(muc_jid); + view.model.setChatState(_converse.ACTIVE); + + expect(view.model.sendChatState).toHaveBeenCalled(); + const last_stanza = _converse.connection.sent_stanzas.pop(); + expect(Strophe.serialize(last_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + // Romeo loses his voice + const presence = $pres({ + to: 'romeo@montague.lit/orchard', + from: `${muc_jid}/romeo` + }).c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', {'affiliation': 'none', 'role': 'visitor'}).up() + .c('status', {code: '110'}); + _converse.connection._dataRecv(mock.createRequest(presence)); + + const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid}); + expect(occupant.get('role')).toBe('visitor'); + + spyOn(_converse.connection, 'send'); + view.model.setChatState(_converse.INACTIVE); + expect(view.model.sendChatState.calls.count()).toBe(2); + expect(_converse.connection.send).not.toHaveBeenCalled(); + done(); + })); + + + describe("A composing notification", function () { + + it("will be shown if received", mock.initConverse( ['rosterGroupsFetched'], {}, async function (done, _converse) { - const muc_jid = 'impermissable@muc.montague.lit' - await test_utils.openChatRoomViaModal(_converse, muc_jid, 'romeo') - - // We pretend this is a new room, so no disco info is returned. - const iq = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => iq.querySelector( - `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` - )).pop()); - const features_stanza = $iq({ - 'from': 'room@conference.example.org', - 'id': iq.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(muc_jid); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); - - const presence = $pres().attrs({ - from: `${muc_jid}/romeo`, - id: u.getUniqueId(), - to:'romeo@montague.lit/pda', - type:'error' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) - .c('not-allowed').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) - .toBe('You are not allowed to create new groupchats.'); - done(); - })); - - it("will show an error message if the user's nickname doesn't conform to groupchat policy", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'conformist@muc.montague.lit' - await test_utils.openChatRoomViaModal(_converse, muc_jid, 'romeo'); - - const iq = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => iq.querySelector( - `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` - )).pop()); - const features_stanza = $iq({ - 'from': muc_jid, - 'id': iq.getAttribute('id'), - 'to': 'romeo@montague.lit/desktop', - 'type': 'result' - }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() - .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); - - const view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); - - const presence = $pres().attrs({ - from: `${muc_jid}/romeo`, - id: u.getUniqueId(), - to:'romeo@montague.lit/pda', - type:'error' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) - .c('not-acceptable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) - .toBe("Your nickname doesn't conform to this groupchat's policies."); - done(); - })); - - it("will show an error message if the groupchat doesn't yet exist", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'nonexistent@muc.montague.lit' - await test_utils.openChatRoomViaModal(_converse, muc_jid, 'romeo'); - - const iq = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => iq.querySelector( - `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` - )).pop()); - const features_stanza = $iq({ - 'from': muc_jid, - 'id': iq.getAttribute('id'), - 'to': 'romeo@montague.lit/desktop', - 'type': 'result' - }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() - .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); - - const view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); - - const presence = $pres().attrs({ - from: `${muc_jid}/romeo`, - id: u.getUniqueId(), - to: 'romeo@montague.lit/pda', - type:'error' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) - .c('item-not-found').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) - .toBe("This groupchat does not (yet) exist."); - done(); - })); - - it("will show an error message if the groupchat has reached its maximum number of participants", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'maxed-out@muc.montague.lit' - await test_utils.openChatRoomViaModal(_converse, muc_jid, 'romeo') - - const iq = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => iq.querySelector( - `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` - )).pop()); - const features_stanza = $iq({ - 'from': muc_jid, - 'id': iq.getAttribute('id'), - 'to': 'romeo@montague.lit/desktop', - 'type': 'result' - }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() - .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); - - const view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); - - const presence = $pres().attrs({ - from: `${muc_jid}/romeo`, - id: u.getUniqueId(), - to:'romeo@montague.lit/pda', - type:'error' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) - .c('service-unavailable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) - .toBe("This groupchat has reached its maximum number of participants."); - done(); - })); - }); - - describe("Someone being invited to a groupchat", function () { - - it("will first be added to the member list if the groupchat is members only", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 0); - spyOn(_converse.ChatRoomOccupants.prototype, 'fetchMembers').and.callThrough(); - const sent_IQs = _converse.connection.IQ_stanzas; const muc_jid = 'coven@chat.shakespeare.lit'; + const members = [ + {'affiliation': 'member', 'nick': 'majortom', 'jid': 'majortom@example.org'}, + {'affiliation': 'admin', 'nick': 'groundcontrol', 'jid': 'groundcontrol@example.org'} + ]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'some1', [], members); + const view = _converse.api.chatviews.get(muc_jid); - const room_creation_promise = _converse.api.rooms.open(muc_jid, {'nick': 'romeo'}); + let csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext.trim()).toEqual("some1 has entered the groupchat"); - // Check that the groupchat queried for the features. - let stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`)).pop()); - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``); - - // State that the chat is members-only via the features IQ - const view = _converse.chatboxviews.get(muc_jid); - const features_stanza = $iq({ - from: 'coven@chat.shakespeare.lit', - 'id': stanza.getAttribute('id'), - 'to': 'romeo@montague.lit/desktop', - 'type': 'result' + let presence = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + from: 'coven@chat.shakespeare.lit/newguy' }) - .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', { - 'category': 'conference', - 'name': 'A Dark Cave', - 'type': 'text' - }).up() - .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() - .c('feature', {'var': 'muc_hidden'}).up() - .c('feature', {'var': 'muc_temporary'}).up() - .c('feature', {'var': 'muc_membersonly'}).up(); - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); - expect(view.model.features.get('membersonly')).toBeTruthy(); + .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"); - await room_creation_promise; + 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, newguy and nomorenicks have entered the groupchat"); - await test_utils.createContacts(_converse, 'current'); + // Manually clear so that we can more easily test + view.model.notifications.set('entered', []); + await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent); - let sent_stanza, sent_id; - spyOn(_converse.connection, 'send').and.callFake(function (stanza) { - if (stanza.nodeTree && stanza.nodeTree.nodeName === 'message') { - sent_id = stanza.nodeTree.getAttribute('id'); - sent_stanza = stanza; + // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions + + const timeout_functions = []; + spyOn(window, 'setTimeout').and.callFake(f => { + if (f.toString() === "() => this.removeNotification(actor, state)") { + timeout_functions.push(f) } }); - const invitee_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const reason = "Please join this groupchat"; - view.model.directInvite(invitee_jid, reason); - // 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( - ``+ - ``+ - ``); - - // Now the service sends the member lists to the user - const member_list_stanza = $iq({ - 'from': 'coven@chat.shakespeare.lit', - '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)); - - const admin_list_stanza = $iq({ - 'from': 'coven@chat.shakespeare.lit', - 'id': admin_iq.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) - .c('item', { - 'affiliation': 'admin', - 'jid': 'wiccarocks@shakespeare.lit', - 'nick': 'secondwitch' - }); - _converse.connection._dataRecv(test_utils.createRequest(admin_list_stanza)); - - const owner_list_stanza = $iq({ - 'from': 'coven@chat.shakespeare.lit', - 'id': owner_iq.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}) - .c('item', { - 'affiliation': 'owner', - 'jid': 'crone1@shakespeare.lit', - }); - _converse.connection._dataRecv(test_utils.createRequest(owner_list_stanza)); - - // Converse puts the user on the member list - stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/muc#admin"]`)).pop()); - expect(stanza.outerHTML, - ``+ - ``+ - ``+ - `Please join this groupchat`+ - ``+ - ``+ - ``); - - const result = $iq({ - 'from': 'coven@chat.shakespeare.lit', - 'id': stanza.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }); - _converse.connection._dataRecv(test_utils.createRequest(result)); - - await u.waitUntil(() => view.model.occupants.fetchMembers.calls.count()); - - // Finally check that the user gets invited. - expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec) - ``+ - ``+ - `` - ); - done(); - })); - }); - - describe("The affiliations delta", function () { - - it("can be computed in various ways", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.openChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'romeo'); - var exclude_existing = false; - var remove_absentees = false; - var new_list = []; - var old_list = []; - const muc_utils = converse.env.muc_utils; - var delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); - expect(delta.length).toBe(0); - - new_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; - old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; - delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); - expect(delta.length).toBe(0); - - // When remove_absentees is false, then affiliations in the old - // list which are not in the new one won't be removed. - old_list = [{'jid': 'oldhag666@shakespeare.lit', 'affiliation': 'owner'}, - {'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; - delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); - expect(delta.length).toBe(0); - - // With exclude_existing set to false, any changed affiliations - // will be included in the delta (i.e. existing affiliations are included in the comparison). - old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'owner'}]; - delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); - expect(delta.length).toBe(1); - expect(delta[0].jid).toBe('wiccarocks@shakespeare.lit'); - expect(delta[0].affiliation).toBe('member'); - - // To also remove affiliations from the old list which are not - // in the new list, we set remove_absentees to true - remove_absentees = true; - old_list = [{'jid': 'oldhag666@shakespeare.lit', 'affiliation': 'owner'}, - {'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'member'}]; - delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); - expect(delta.length).toBe(1); - expect(delta[0].jid).toBe('oldhag666@shakespeare.lit'); - expect(delta[0].affiliation).toBe('none'); - - delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, [], old_list); - expect(delta.length).toBe(2); - expect(delta[0].jid).toBe('oldhag666@shakespeare.lit'); - expect(delta[0].affiliation).toBe('none'); - expect(delta[1].jid).toBe('wiccarocks@shakespeare.lit'); - expect(delta[1].affiliation).toBe('none'); - - // To only add a user if they don't already have an - // affiliation, we set 'exclude_existing' to true - exclude_existing = true; - old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'owner'}]; - delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); - expect(delta.length).toBe(0); - - old_list = [{'jid': 'wiccarocks@shakespeare.lit', 'affiliation': 'admin'}]; - delta = muc_utils.computeAffiliationsDelta(exclude_existing, remove_absentees, new_list, old_list); - expect(delta.length).toBe(0); - done(); - })); - }); - - describe("The \"Groupchats\" Add modal", function () { - - it("can be opened from a link in the \"Groupchats\" section of the controlbox", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current', 0); - - const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; - roomspanel.el.querySelector('.show-add-muc-modal').click(); - test_utils.closeControlBox(_converse); - const modal = roomspanel.add_room_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000) - - let label_name = modal.el.querySelector('label[for="chatroom"]'); - expect(label_name.textContent.trim()).toBe('Groupchat address:'); - let name_input = modal.el.querySelector('input[name="chatroom"]'); - expect(name_input.placeholder).toBe('name@conference.example.org'); - - const label_nick = modal.el.querySelector('label[for="nickname"]'); - expect(label_nick.textContent.trim()).toBe('Nickname:'); - const nick_input = modal.el.querySelector('input[name="nickname"]'); - expect(nick_input.value).toBe(''); - nick_input.value = 'romeo'; - - expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat'); - spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); - roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - modal.el.querySelector('input[name="chatroom"]').value = 'lounce@muc.montague.lit'; - modal.el.querySelector('form input[type="submit"]').click(); - await u.waitUntil(() => _converse.chatboxes.length); - await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1); - - roomspanel.model.set('muc_domain', 'muc.example.org'); - roomspanel.el.querySelector('.show-add-muc-modal').click(); - label_name = modal.el.querySelector('label[for="chatroom"]'); - expect(label_name.textContent.trim()).toBe('Groupchat address:'); - name_input = modal.el.querySelector('input[name="chatroom"]'); - expect(name_input.placeholder).toBe('name@muc.example.org'); - done(); - })); - - it("doesn't show the nickname field if locked_muc_nickname is true", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'locked_muc_nickname': true, 'muc_nickname_from_jid': true}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current', 0); - const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; - roomspanel.el.querySelector('.show-add-muc-modal').click(); - test_utils.closeControlBox(_converse); - const modal = roomspanel.add_room_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000) - const name_input = modal.el.querySelector('input[name="chatroom"]'); - name_input.value = 'lounge@montague.lit'; - expect(modal.el.querySelector('label[for="nickname"]')).toBe(null); - expect(modal.el.querySelector('input[name="nickname"]')).toBe(null); - modal.el.querySelector('form input[type="submit"]').click(); - await u.waitUntil(() => _converse.chatboxes.length > 1); - const chatroom = _converse.chatboxes.get('lounge@montague.lit'); - expect(chatroom.get('nick')).toBe('romeo'); - done(); - })); - - it("uses the JID node if muc_nickname_from_jid is set to true", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_nickname_from_jid': true}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current', 0); - const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; - roomspanel.el.querySelector('.show-add-muc-modal').click(); - test_utils.closeControlBox(_converse); - const modal = roomspanel.add_room_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000) - const label_nick = modal.el.querySelector('label[for="nickname"]'); - expect(label_nick.textContent.trim()).toBe('Nickname:'); - const nick_input = modal.el.querySelector('input[name="nickname"]'); - expect(nick_input.value).toBe('romeo'); - done(); - })); - - it("uses the nickname passed in to converse.initialize", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'nickname': 'st.nick'}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current', 0); - const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; - roomspanel.el.querySelector('.show-add-muc-modal').click(); - test_utils.closeControlBox(_converse); - const modal = roomspanel.add_room_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000) - const label_nick = modal.el.querySelector('label[for="nickname"]'); - expect(label_nick.textContent.trim()).toBe('Nickname:'); - const nick_input = modal.el.querySelector('input[name="nickname"]'); - expect(nick_input.value).toBe('st.nick'); - done(); - })); - - it("doesn't require the domain when muc_domain is set", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org'}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; - roomspanel.el.querySelector('.show-add-muc-modal').click(); - const modal = roomspanel.add_room_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000) - expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat'); - spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); - roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - const label_name = modal.el.querySelector('label[for="chatroom"]'); - expect(label_name.textContent.trim()).toBe('Groupchat name:'); - let name_input = modal.el.querySelector('input[name="chatroom"]'); - expect(name_input.placeholder).toBe('name@muc.example.org'); - name_input.value = 'lounge'; - let nick_input = modal.el.querySelector('input[name="nickname"]'); - nick_input.value = 'max'; - - modal.el.querySelector('form input[type="submit"]').click(); - await u.waitUntil(() => _converse.chatboxes.length); - await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1); - expect(_.includes(_converse.chatboxes.models.map(m => m.get('id')), 'lounge@muc.example.org')).toBe(true); - - // However, you can still open MUCs with different domains - roomspanel.el.querySelector('.show-add-muc-modal').click(); - await u.waitUntil(() => u.isVisible(modal.el), 1000); - name_input = modal.el.querySelector('input[name="chatroom"]'); - name_input.value = 'lounge@conference.example.org'; - nick_input = modal.el.querySelector('input[name="nickname"]'); - nick_input.value = 'max'; - modal.el.querySelector('form input[type="submit"]').click(); - await u.waitUntil(() => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2); - await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 2); - expect(_.includes(_converse.chatboxes.models.map(m => m.get('id')), 'lounge@conference.example.org')).toBe(true); - done(); - })); - - it("only uses the muc_domain is locked_muc_domain is true", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'muc_domain': 'muc.example.org', 'locked_muc_domain': true}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; - roomspanel.el.querySelector('.show-add-muc-modal').click(); - const modal = roomspanel.add_room_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000) - expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat'); - spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); - roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - const label_name = modal.el.querySelector('label[for="chatroom"]'); - expect(label_name.textContent.trim()).toBe('Groupchat name:'); - let name_input = modal.el.querySelector('input[name="chatroom"]'); - expect(name_input.placeholder).toBe(''); - name_input.value = 'lounge'; - let nick_input = modal.el.querySelector('input[name="nickname"]'); - nick_input.value = 'max'; - modal.el.querySelector('form input[type="submit"]').click(); - await u.waitUntil(() => _converse.chatboxes.length); - await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1); - expect(_.includes(_converse.chatboxes.models.map(m => m.get('id')), 'lounge@muc.example.org')).toBe(true); - - // However, you can still open MUCs with different domains - roomspanel.el.querySelector('.show-add-muc-modal').click(); - await u.waitUntil(() => u.isVisible(modal.el), 1000); - name_input = modal.el.querySelector('input[name="chatroom"]'); - name_input.value = 'lounge@conference'; - nick_input = modal.el.querySelector('input[name="nickname"]'); - nick_input.value = 'max'; - modal.el.querySelector('form input[type="submit"]').click(); - await u.waitUntil(() => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2); - await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 2); - expect(_.includes(_converse.chatboxes.models.map(m => m.get('id')), 'lounge\\40conference@muc.example.org')).toBe(true); - done(); - })); - }); - - describe("The \"Groupchats\" List modal", function () { - - it("can be opened from a link in the \"Groupchats\" section of the controlbox", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; - roomspanel.el.querySelector('.show-list-muc-modal').click(); - test_utils.closeControlBox(_converse); - const modal = roomspanel.list_rooms_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); - roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - - // See: https://xmpp.org/extensions/xep-0045.html#disco-rooms - expect(modal.el.querySelectorAll('.available-chatrooms li').length).toBe(0); - - const server_input = modal.el.querySelector('input[name="server"]'); - expect(server_input.placeholder).toBe('conference.example.org'); - server_input.value = 'chat.shakespeare.lit'; - modal.el.querySelector('input[type="submit"]').click(); - await u.waitUntil(() => _converse.chatboxes.length); - - const IQ_stanzas = _converse.connection.IQ_stanzas; - const sent_stanza = await u.waitUntil( - () => IQ_stanzas.filter(s => sizzle(`query[xmlns="${Strophe.NS.DISCO_ITEMS}"]`, s).length).pop() - ); - const id = sent_stanza.getAttribute('id'); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ``+ - `` - ); - const iq = $iq({ - 'from':'muc.montague.lit', - 'to':'romeo@montague.lit/pda', - 'id': id, - 'type':'result' - }).c('query') - .c('item', { jid:'heath@chat.shakespeare.lit', name:'A Lonely Heath'}).up() - .c('item', { jid:'coven@chat.shakespeare.lit', name:'A Dark Cave'}).up() - .c('item', { jid:'forres@chat.shakespeare.lit', name:'The Palace'}).up() - .c('item', { jid:'inverness@chat.shakespeare.lit', name:'Macbeth's Castle'}).up() - .c('item', { jid:'orchard@chat.shakespeare.lit', name:'Capulet\'s Orchard'}).up() - .c('item', { jid:'friar@chat.shakespeare.lit', name:'Friar Laurence\'s cell'}).up() - .c('item', { jid:'hall@chat.shakespeare.lit', name:'Hall in Capulet\'s house'}).up() - .c('item', { jid:'chamber@chat.shakespeare.lit', name:'Juliet\'s chamber'}).up() - .c('item', { jid:'public@chat.shakespeare.lit', name:'A public place'}).up() - .c('item', { jid:'street@chat.shakespeare.lit', name:'A street'}).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(iq)); - - await u.waitUntil(() => modal.el.querySelectorAll('.available-chatrooms li').length === 11); - const rooms = modal.el.querySelectorAll('.available-chatrooms li'); - expect(rooms[0].textContent.trim()).toBe("Groupchats found:"); - expect(rooms[1].textContent.trim()).toBe("A Lonely Heath"); - expect(rooms[2].textContent.trim()).toBe("A Dark Cave"); - expect(rooms[3].textContent.trim()).toBe("The Palace"); - expect(rooms[4].textContent.trim()).toBe("Macbeth's Castle"); - expect(rooms[5].textContent.trim()).toBe('Capulet\'s Orchard'); - expect(rooms[6].textContent.trim()).toBe('Friar Laurence\'s cell'); - expect(rooms[7].textContent.trim()).toBe('Hall in Capulet\'s house'); - expect(rooms[8].textContent.trim()).toBe('Juliet\'s chamber'); - expect(rooms[9].textContent.trim()).toBe('A public place'); - expect(rooms[10].textContent.trim()).toBe('A street'); - - rooms[4].querySelector('.open-room').click(); - await u.waitUntil(() => _converse.chatboxes.length > 1); - expect(sizzle('.chatroom', _converse.el).filter(u.isVisible).length).toBe(1); // There should now be an open chatroom - var view = _converse.chatboxviews.get('inverness@chat.shakespeare.lit'); - expect(view.el.querySelector('.chatbox-title__text').textContent.trim()).toBe("Macbeth's Castle"); - done(); - })); - - it("is pre-filled with the muc_domain", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], - {'muc_domain': 'muc.example.org'}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; - roomspanel.el.querySelector('.show-list-muc-modal').click(); - test_utils.closeControlBox(_converse); - const modal = roomspanel.list_rooms_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - const server_input = modal.el.querySelector('input[name="server"]'); - expect(server_input.value).toBe('muc.example.org'); - done(); - })); - - it("doesn't let you set the MUC domain if it's locked", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], - {'muc_domain': 'chat.shakespeare.lit', 'locked_muc_domain': true}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; - roomspanel.el.querySelector('.show-list-muc-modal').click(); - test_utils.closeControlBox(_converse); - const modal = roomspanel.list_rooms_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); - roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - - expect(modal.el.querySelector('input[name="server"]')).toBe(null); - expect(modal.el.querySelector('input[type="submit"]')).toBe(null); - await u.waitUntil(() => _converse.chatboxes.length); - const sent_stanza = await u.waitUntil(() => - _converse.connection.sent_stanzas.filter( - s => sizzle(`query[xmlns="http://jabber.org/protocol/disco#items"]`, s).length).pop() - ); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ``+ - `` - ); - const iq = $iq({ - from:'muc.montague.lit', - to:'romeo@montague.lit/pda', - id: sent_stanza.getAttribute('id'), - type:'result' - }).c('query') - .c('item', { jid:'heath@chat.shakespeare.lit', name:'A Lonely Heath'}).up() - .c('item', { jid:'coven@chat.shakespeare.lit', name:'A Dark Cave'}).up() - .c('item', { jid:'forres@chat.shakespeare.lit', name:'The Palace'}).up() - _converse.connection._dataRecv(test_utils.createRequest(iq)); - - await u.waitUntil(() => modal.el.querySelectorAll('.available-chatrooms li').length === 4); - const rooms = modal.el.querySelectorAll('.available-chatrooms li'); - expect(rooms[0].textContent.trim()).toBe("Groupchats found:"); - expect(rooms[1].textContent.trim()).toBe("A Lonely Heath"); - expect(rooms[2].textContent.trim()).toBe("A Dark Cave"); - expect(rooms[3].textContent.trim()).toBe("The Palace"); - done(); - })); - }); - - describe("The \"Groupchats\" section", function () { - - it("shows the number of unread mentions received", - mock.initConverse( - ['rosterGroupsFetched'], {'allow_bookmarks': false}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel; - expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(0); - - const muc_jid = 'kitchen@conference.shakespeare.lit'; - const message = 'fires: Your attention is required'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'fires'); - const view = _converse.api.chatviews.get(muc_jid); - await u.waitUntil(() => roomspanel.el.querySelectorAll('.available-room').length); - expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(1); - expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(0); - - view.model.set({'minimized': true}); - - const nick = mock.chatroom_names[0]; - - await view.model.queueMessage($msg({ - from: muc_jid+'/'+nick, + // state + let msg = $msg({ + from: muc_jid+'/newguy', id: u.getUniqueId(), to: 'romeo@montague.lit', type: 'groupchat' - }).c('body').t(message).tree()); - await u.waitUntil(() => view.model.messages.length); - expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(1); - expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(1); - expect(roomspanel.el.querySelector('.msgs-indicator').textContent.trim()).toBe('1'); + }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); - await view.model.queueMessage($msg({ - 'from': muc_jid+'/'+nick, - 'id': u.getUniqueId(), - 'to': 'romeo@montague.lit', - 'type': 'groupchat' - }).c('body').t(message).tree()); - await u.waitUntil(() => view.model.messages.length > 1); - expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(1); - expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(1); - expect(roomspanel.el.querySelector('.msgs-indicator').textContent.trim()).toBe('2'); - view.model.set({'minimized': false}); - expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(1); - expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(0); + csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext.trim()).toEqual('newguy is typing'); + expect(timeout_functions.length).toBe(1); + + expect(view.el.querySelector('.chat-content__notifications').textContent.trim()).toEqual('newguy is typing'); + + // state for a different occupant + msg = $msg({ + from: muc_jid+'/nomorenicks', + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + await view.model.queueMessage(msg); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy and nomorenicks are typing'); + + // state for a different occupant + msg = $msg({ + from: muc_jid+'/majortom', + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + await view.model.queueMessage(msg); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy, nomorenicks and majortom are typing'); + + // state for a different occupant + msg = $msg({ + from: muc_jid+'/groundcontrol', + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + await view.model.queueMessage(msg); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy, nomorenicks and others are typing'); + + // Check that new messages appear under the chat state notifications + msg = $msg({ + from: `${muc_jid}/some1`, + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('hello world').tree(); + await view.model.queueMessage(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + + const messages = view.el.querySelectorAll('.message'); + expect(messages.length).toBe(2); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelector('.chat-msg .chat-msg__text').textContent.trim()).toBe('hello world'); + + // Test that the composing notifications get removed via timeout. + timeout_functions[0](); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'nomorenicks, majortom and groundcontrol are typing'); done(); })); }); - describe("A XEP-0085 Chat Status Notification", function () { + describe("A paused notification", function () { - it("is is not sent out to a MUC if the user is a visitor in a moderated room", + it("will be shown if received", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { - spyOn(_converse.ChatRoom.prototype, 'sendChatState').and.callThrough(); + const muc_jid = 'coven@chat.shakespeare.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'some1'); + const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); - const muc_jid = 'lounge@montague.lit'; - const features = [ - 'http://jabber.org/protocol/muc', - 'jabber:iq:register', - 'muc_passwordprotected', - 'muc_hidden', - 'muc_temporary', - 'muc_membersonly', - 'muc_moderated', - 'muc_anonymous' - ] - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - - const view = _converse.api.chatviews.get(muc_jid); - view.model.setChatState(_converse.ACTIVE); - - expect(view.model.sendChatState).toHaveBeenCalled(); - const last_stanza = _converse.connection.sent_stanzas.pop(); - expect(Strophe.serialize(last_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - // Romeo loses his voice - const presence = $pres({ - to: 'romeo@montague.lit/orchard', - from: `${muc_jid}/romeo` + /* + * + * + * + * + * + */ + let 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': 'none', 'role': 'visitor'}).up() + .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(presence)); + const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(csntext.trim()).toEqual("some1 has entered the groupchat"); - const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid}); - expect(occupant.get('role')).toBe('visitor'); - - spyOn(_converse.connection, 'send'); - view.model.setChatState(_converse.INACTIVE); - expect(view.model.sendChatState.calls.count()).toBe(2); - expect(_converse.connection.send).not.toHaveBeenCalled(); - done(); - })); - - - describe("A composing notification", function () { - - it("will be shown if received", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'coven@chat.shakespeare.lit'; - const members = [ - {'affiliation': 'member', 'nick': 'majortom', 'jid': 'majortom@example.org'}, - {'affiliation': 'admin', 'nick': 'groundcontrol', 'jid': 'groundcontrol@example.org'} - ]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'some1', [], members); - const view = _converse.api.chatviews.get(muc_jid); - - let csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext.trim()).toEqual("some1 has entered the groupchat"); - - let 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"); - - 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, newguy and nomorenicks have entered the groupchat"); - - // Manually clear so that we can more easily test - view.model.notifications.set('entered', []); - await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent); - - // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions - - const timeout_functions = []; - spyOn(window, 'setTimeout').and.callFake(f => { - if (f.toString() === "() => this.removeNotification(actor, state)") { - timeout_functions.push(f) - } + 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"); - // state - let msg = $msg({ - from: muc_jid+'/newguy', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); + 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, newguy and nomorenicks have entered the groupchat"); - csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext.trim()).toEqual('newguy is typing'); - expect(timeout_functions.length).toBe(1); + // Manually clear so that we can more easily test + view.model.notifications.set('entered', []); + await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent); - expect(view.el.querySelector('.chat-content__notifications').textContent.trim()).toEqual('newguy is typing'); + // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions - // state for a different occupant - msg = $msg({ - from: muc_jid+'/nomorenicks', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - await view.model.queueMessage(msg); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy and nomorenicks are typing'); - - // state for a different occupant - msg = $msg({ - from: muc_jid+'/majortom', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - await view.model.queueMessage(msg); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy, nomorenicks and majortom are typing'); - - // state for a different occupant - msg = $msg({ - from: muc_jid+'/groundcontrol', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - await view.model.queueMessage(msg); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'newguy, nomorenicks and others are typing'); - - // Check that new messages appear under the chat state notifications - msg = $msg({ - from: `${muc_jid}/some1`, + // state + var msg = $msg({ + from: muc_jid+'/newguy', id: u.getUniqueId(), to: 'romeo@montague.lit', type: 'groupchat' - }).c('body').t('hello world').tree(); - await view.model.queueMessage(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); + }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + await view.model.queueMessage(msg); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); + expect(view.el.querySelector('.chat-content__notifications').textContent.trim()).toBe('newguy is typing'); - const messages = view.el.querySelectorAll('.message'); - expect(messages.length).toBe(2); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelector('.chat-msg .chat-msg__text').textContent.trim()).toBe('hello world'); + // state for a different occupant + msg = $msg({ + from: muc_jid+'/nomorenicks', + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + await view.model.queueMessage(msg); - // Test that the composing notifications get removed via timeout. - timeout_functions[0](); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() === 'nomorenicks, majortom and groundcontrol are typing'); - done(); - })); - }); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() == 'newguy and nomorenicks are typing'); - describe("A paused notification", function () { - - it("will be shown if received", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'coven@chat.shakespeare.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'some1'); - const view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); - - /* - * - * - * - * - * - */ - let 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)); - const csntext = await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(csntext.trim()).toEqual("some1 has entered the groupchat"); - - 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"); - - 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, newguy and nomorenicks have entered the groupchat"); - - // Manually clear so that we can more easily test - view.model.notifications.set('entered', []); - await u.waitUntil(() => !view.el.querySelector('.chat-content__notifications').textContent); - - // See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions - - // state - var msg = $msg({ - from: muc_jid+'/newguy', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - await view.model.queueMessage(msg); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent); - expect(view.el.querySelector('.chat-content__notifications').textContent.trim()).toBe('newguy is typing'); - - // state for a different occupant - msg = $msg({ - from: muc_jid+'/nomorenicks', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - await view.model.queueMessage(msg); - - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() == 'newguy and nomorenicks are typing'); - - // state from occupant who typed first - msg = $msg({ - from: muc_jid+'/newguy', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - await view.model.queueMessage(msg); - await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() == 'nomorenicks is typing\n newguy has stopped typing'); - done(); - })); - }); - }); - - describe("A muted user", function () { - - it("will receive a user-friendly error message when trying to send a message", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'trollbox@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'troll'); - const view = _converse.api.chatviews.get(muc_jid); - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = 'Hello world'; - view.onFormSubmitted(new Event('submit')); - await new Promise(resolve => view.once('messageInserted', resolve)); - - let stanza = u.toStanza(` - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelector('.chat-error').textContent.trim()).toBe( - "Your message was not delivered because you weren't allowed to send it."); - - textarea.value = 'Hello again'; - view.onFormSubmitted(new Event('submit')); - await new Promise(resolve => view.once('messageInserted', resolve)); - - stanza = u.toStanza(` - - - - Thou shalt not! - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - - expect(view.el.querySelector('.message:last-child').textContent.trim()).toBe( - 'Your message was not delivered because you weren\'t allowed to send it. '+ - 'The message from the server is: "Thou shalt not!"') - done(); - })); - - it("will see an explanatory message instead of a textarea", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const features = [ - 'http://jabber.org/protocol/muc', - 'jabber:iq:register', - Strophe.NS.SID, - 'muc_moderated', - ] - const muc_jid = 'trollbox@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'troll', features); - const view = _converse.api.chatviews.get(muc_jid); - expect(_.isNull(view.el.querySelector('.chat-textarea'))).toBe(false); - - let stanza = u.toStanza(` - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - expect(view.el.querySelector('.chat-textarea')).toBe(null); - let bottom_panel = view.el.querySelector('.muc-bottom-panel'); - expect(bottom_panel.textContent.trim()).toBe("You're not allowed to send messages in this room"); - - // This only applies to moderated rooms, so let's check that - // the textarea becomes visible when the room's - // configuration changes to be non-moderated - view.model.features.set('moderated', false); - expect(view.el.querySelector('.muc-bottom-panel')).toBe(null); - let textarea = view.el.querySelector('.chat-textarea'); - expect(textarea === null).toBe(false); - - view.model.features.set('moderated', true); - expect(view.el.querySelector('.chat-textarea')).toBe(null); - bottom_panel = view.el.querySelector('.muc-bottom-panel'); - expect(bottom_panel.textContent.trim()).toBe("You're not allowed to send messages in this room"); - - // Check now that things get restored when the user is given a voice - await u.waitUntil(() => - Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === - "troll is no longer an owner of this groupchat" - ); - - stanza = u.toStanza(` - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - bottom_panel = view.el.querySelector('.muc-bottom-panel'); - expect(bottom_panel).toBe(null); - - textarea = view.el.querySelector('.chat-textarea'); - expect(textarea === null).toBe(false); - - // Check now that things get restored when the user is given a voice - await u.waitUntil(() => - Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === - "troll has been given a voice" - ); - expect(view.el.querySelectorAll('.chat-info__message').length).toBe(2); + // state from occupant who typed first + msg = $msg({ + from: muc_jid+'/newguy', + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + await view.model.queueMessage(msg); + await u.waitUntil(() => view.el.querySelector('.chat-content__notifications').textContent.trim() == 'nomorenicks is typing\n newguy has stopped typing'); done(); })); }); }); + + describe("A muted user", function () { + + it("will receive a user-friendly error message when trying to send a message", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'trollbox@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'troll'); + const view = _converse.api.chatviews.get(muc_jid); + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = 'Hello world'; + view.onFormSubmitted(new Event('submit')); + await new Promise(resolve => view.once('messageInserted', resolve)); + + let stanza = u.toStanza(` + + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.el.querySelector('.chat-error').textContent.trim()).toBe( + "Your message was not delivered because you weren't allowed to send it."); + + textarea.value = 'Hello again'; + view.onFormSubmitted(new Event('submit')); + await new Promise(resolve => view.once('messageInserted', resolve)); + + stanza = u.toStanza(` + + + + Thou shalt not! + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + + expect(view.el.querySelector('.message:last-child').textContent.trim()).toBe( + 'Your message was not delivered because you weren\'t allowed to send it. '+ + 'The message from the server is: "Thou shalt not!"') + done(); + })); + + it("will see an explanatory message instead of a textarea", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const features = [ + 'http://jabber.org/protocol/muc', + 'jabber:iq:register', + Strophe.NS.SID, + 'muc_moderated', + ] + const muc_jid = 'trollbox@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'troll', features); + const view = _converse.api.chatviews.get(muc_jid); + expect(_.isNull(view.el.querySelector('.chat-textarea'))).toBe(false); + + let stanza = u.toStanza(` + + + + + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + expect(view.el.querySelector('.chat-textarea')).toBe(null); + let bottom_panel = view.el.querySelector('.muc-bottom-panel'); + expect(bottom_panel.textContent.trim()).toBe("You're not allowed to send messages in this room"); + + // This only applies to moderated rooms, so let's check that + // the textarea becomes visible when the room's + // configuration changes to be non-moderated + view.model.features.set('moderated', false); + expect(view.el.querySelector('.muc-bottom-panel')).toBe(null); + let textarea = view.el.querySelector('.chat-textarea'); + expect(textarea === null).toBe(false); + + view.model.features.set('moderated', true); + expect(view.el.querySelector('.chat-textarea')).toBe(null); + bottom_panel = view.el.querySelector('.muc-bottom-panel'); + expect(bottom_panel.textContent.trim()).toBe("You're not allowed to send messages in this room"); + + // Check now that things get restored when the user is given a voice + await u.waitUntil(() => + Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === + "troll is no longer an owner of this groupchat" + ); + + stanza = u.toStanza(` + + + + + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + bottom_panel = view.el.querySelector('.muc-bottom-panel'); + expect(bottom_panel).toBe(null); + + textarea = view.el.querySelector('.chat-textarea'); + expect(textarea === null).toBe(false); + + // Check now that things get restored when the user is given a voice + await u.waitUntil(() => + Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === + "troll has been given a voice" + ); + expect(view.el.querySelectorAll('.chat-info__message').length).toBe(2); + done(); + })); + }); }); diff --git a/spec/muc_messages.js b/spec/muc_messages.js index 645450931..ad99451b0 100644 --- a/spec/muc_messages.js +++ b/spec/muc_messages.js @@ -1,1252 +1,1250 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const { Promise, Strophe, $msg, $pres, sizzle, stanza_utils } = converse.env; - const u = converse.env.utils; +/*global mock */ - describe("A Groupchat Message", function () { +const { Promise, Strophe, $msg, $pres, sizzle, stanza_utils } = converse.env; +const u = converse.env.utils; - describe("an info message", function () { +describe("A Groupchat Message", function () { - it("is not rendered as a followup message", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + describe("an info message", function () { - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - let presence = u.toStanza(` - - - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1); - - presence = u.toStanza(` - - - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2); - - const messages = view.el.querySelectorAll('.chat-info'); - expect(u.hasClass('chat-msg--followup', messages[0])).toBe(false); - expect(u.hasClass('chat-msg--followup', messages[1])).toBe(false); - done(); - })); - - it("is not shown if its a duplicate", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - const presence = u.toStanza(` - - - - - - - - `); - // XXX: We wait for createInfoMessages to complete, if we don't - // we still get two info messages due to messages - // created from presences not being queued and run - // sequentially (i.e. by waiting for promises to resolve) - // like we do with message stanzas. - spyOn(view.model, 'createInfoMessages').and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.model.createInfoMessages.calls.count()); - await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1); - - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.model.createInfoMessages.calls.count() === 2); - expect(view.el.querySelectorAll('.chat-info').length).toBe(1); - done(); - })); - }); - - - it("is rejected if it's an unencapsulated forwarded message", + it("is not rendered as a followup message", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const impersonated_jid = `${muc_jid}/alice`; - const received_stanza = u.toStanza(` - - - - - Yet I should kill thee with much cherishing. + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + let presence = u.toStanza(` + + + + + + + + `); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1); + + presence = u.toStanza(` + + + + + + + + `); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2); + + const messages = view.el.querySelectorAll('.chat-info'); + expect(u.hasClass('chat-msg--followup', messages[0])).toBe(false); + expect(u.hasClass('chat-msg--followup', messages[1])).toBe(false); + done(); + })); + + it("is not shown if its a duplicate", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + const presence = u.toStanza(` + + + + + + + + `); + // XXX: We wait for createInfoMessages to complete, if we don't + // we still get two info messages due to messages + // created from presences not being queued and run + // sequentially (i.e. by waiting for promises to resolve) + // like we do with message stanzas. + spyOn(view.model, 'createInfoMessages').and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.model.createInfoMessages.calls.count()); + await u.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 1); + + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.model.createInfoMessages.calls.count() === 2); + expect(view.el.querySelectorAll('.chat-info').length).toBe(1); + done(); + })); + }); + + + it("is rejected if it's an unencapsulated forwarded message", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const impersonated_jid = `${muc_jid}/alice`; + const received_stanza = u.toStanza(` + + + + + Yet I should kill thee with much cherishing. + + + + `); + const view = _converse.api.chatviews.get(muc_jid); + spyOn(view.model, 'onMessage').and.callThrough(); + + + await view.model.queueMessage(received_stanza); + spyOn(converse.env.log, 'warn'); + _converse.connection._dataRecv(mock.createRequest(received_stanza)); + await u.waitUntil(() => view.model.onMessage.calls.count()); + expect(converse.env.log.warn).toHaveBeenCalledWith( + 'onMessage: Ignoring unencapsulated forwarded groupchat message' + ); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); + expect(view.model.messages.length).toBe(0); + done(); + })); + + it("can contain a chat state notification and will still be shown", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); } + const message = 'romeo: Your attention is required'; + const 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) + .c('active', {'xmlns': "http://jabber.org/protocol/chatstates"}) + .tree(); + await view.model.queueMessage(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.el.querySelector('.chat-msg')).not.toBe(null); + done(); + })); + + it("is specially marked when you are mentioned in it", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); } + const message = 'romeo: Your attention is required'; + const 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 new Promise(resolve => view.once('messageInserted', resolve)); + expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy(); + done(); + })); + + it("can not be expected to have a unique id attribute", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); } + const id = u.getUniqueId(); + let msg = $msg({ + from: 'lounge@montague.lit/some1', + id: id, + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('First message').tree(); + await view.model.queueMessage(msg); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); + + msg = $msg({ + from: 'lounge@montague.lit/some2', + id: id, + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('Another message').tree(); + await view.model.queueMessage(msg); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2); + expect(view.model.messages.length).toBe(2); + done(); + })); + + it("is ignored if it has the same archive-id of an already received one", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'room@muc.example.com'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + spyOn(view.model, 'getDuplicateMessage').and.callThrough(); + let stanza = u.toStanza(` + + Typical body text + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.messages.length === 1); + await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 1); + let result = await view.model.getDuplicateMessage.calls.all()[0].returnValue; + expect(result).toBe(undefined); + + stanza = u.toStanza(` + + + + + + Typical body text - - `); - const view = _converse.api.chatviews.get(muc_jid); - spyOn(view.model, 'onMessage').and.callThrough(); + + `); + + spyOn(view.model, 'updateMessage'); + await view.model.queueMessage(stanza); + await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 2); + result = await view.model.getDuplicateMessage.calls.all()[1].returnValue; + expect(result instanceof _converse.Message).toBe(true); + expect(view.model.messages.length).toBe(1); + await u.waitUntil(() => view.model.updateMessage.calls.count()); + done(); + })); + + it("is ignored if it has the same stanza-id of an already received one", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'room@muc.example.com'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + spyOn(view.model, 'getStanzaIdQueryAttrs').and.callThrough(); + let stanza = u.toStanza(` + + Typical body text + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.messages.length === 1); + await u.waitUntil(() => view.model.getStanzaIdQueryAttrs.calls.count() === 1); + let result = await view.model.getStanzaIdQueryAttrs.calls.all()[0].returnValue; + expect(result instanceof Array).toBe(true); + expect(result[0] instanceof Object).toBe(true); + expect(result[0]['stanza_id room@muc.example.com']).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad"); + + stanza = u.toStanza(` + + Typical body text + + `); + spyOn(view.model, 'updateMessage'); + spyOn(view.model, 'getDuplicateMessage').and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); + result = await view.model.getDuplicateMessage.calls.all()[0].returnValue; + expect(result instanceof _converse.Message).toBe(true); + expect(view.model.messages.length).toBe(1); + await u.waitUntil(() => view.model.updateMessage.calls.count()); + 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 mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + const muc_jid = 'xsf@muc.xmpp.org'; + const sender_jid = `${muc_jid}/romeo`; + const impersonated_jid = `${muc_jid}/i_am_groot` + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const stanza = $pres({ + to: 'romeo@montague.lit/_converse.js-29092160', + from: sender_jid + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }).tree(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + /* + * + * + * + * + * I am groot. + * + * + * + * + */ + const msg = $msg({ + 'from': sender_jid, + 'id': _converse.connection.getUniqueId(), + 'to': _converse.connection.jid, + 'type': 'groupchat', + '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': muc_jid, + 'type': 'groupchat' + }).c('body').t('I am groot').tree(); + const view = _converse.api.chatviews.get(muc_jid); + spyOn(converse.env.log, 'warn'); + await view.model.queueMessage(msg); + expect(converse.env.log.warn).toHaveBeenCalledWith( + 'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+ + 'according to the XEP groupchat messages SHOULD NOT be carbon copied' + ); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); + expect(view.model.messages.length).toBe(0); + done(); + })); + + it("keeps track of the sender's role and affiliation", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + let msg = $msg({ + from: 'lounge@montague.lit/romeo', + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('I wrote this message!').tree(); + await view.model.queueMessage(msg); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length); + expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner'); + expect(view.model.messages.last().occupant.get('role')).toBe('moderator'); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat moderator owner'); + let presence = $pres({ + to:'romeo@montague.lit/orchard', + from:'lounge@montague.lit/romeo', + id: u.getUniqueId() + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: 'member', + jid: 'romeo@montague.lit/orchard', + role: 'participant' + }).up() + .c('status').attrs({code:'110'}).up() + .c('status').attrs({code:'210'}).nodeTree; + _converse.connection._dataRecv(mock.createRequest(presence)); + + msg = $msg({ + from: 'lounge@montague.lit/romeo', + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('Another message!').tree(); + await view.model.queueMessage(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.model.messages.last().occupant.get('affiliation')).toBe('member'); + expect(view.model.messages.last().occupant.get('role')).toBe('participant'); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); + expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat participant member'); + + presence = $pres({ + to:'romeo@montague.lit/orchard', + from:'lounge@montague.lit/romeo', + id: u.getUniqueId() + }).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:'210'}).nodeTree; + _converse.connection._dataRecv(mock.createRequest(presence)); + + view.model.sendMessage('hello world'); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 3); + + const occupant = await u.waitUntil(() => view.model.messages.filter(m => m.get('type') === 'groupchat')[2].occupant); + expect(occupant.get('affiliation')).toBe('owner'); + expect(occupant.get('role')).toBe('moderator'); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(3); + await u.waitUntil(() => sizzle('.chat-msg', view.el).pop().classList.value.trim() === 'message chat-msg groupchat moderator owner'); + + const add_events = view.model.occupants._events.add.length; + msg = $msg({ + from: 'lounge@montague.lit/some1', + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('Message from someone not in the MUC right now').tree(); + await view.model.queueMessage(msg); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.model.messages.last().occupant).toBeUndefined(); + // Check that there's a new "add" event handler, for when the occupant appears. + expect(view.model.occupants._events.add.length).toBe(add_events+1); + + // Check that the occupant gets added/removed to the message as it + // gets removed or added. + presence = $pres({ + to:'romeo@montague.lit/orchard', + from:'lounge@montague.lit/some1', + id: u.getUniqueId() + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({jid: 'some1@montague.lit/orchard'}); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.model.messages.last().occupant); + expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now'); + expect(view.model.messages.last().occupant.get('nick')).toBe('some1'); + // Check that the "add" event handler was removed. + expect(view.model.occupants._events.add.length).toBe(add_events); + + presence = $pres({ + to:'romeo@montague.lit/orchard', + type: 'unavailable', + from:'lounge@montague.lit/some1', + id: u.getUniqueId() + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({jid: 'some1@montague.lit/orchard'}); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => !view.model.messages.last().occupant); + expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now'); + expect(view.model.messages.last().occupant).toBeUndefined(); + // Check that there's a new "add" event handler, for when the occupant appears. + expect(view.model.occupants._events.add.length).toBe(add_events+1); + + presence = $pres({ + to:'romeo@montague.lit/orchard', + from:'lounge@montague.lit/some1', + id: u.getUniqueId() + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({jid: 'some1@montague.lit/orchard'}); + _converse.connection._dataRecv(mock.createRequest(presence)); + await u.waitUntil(() => view.model.messages.last().occupant); + expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now'); + expect(view.model.messages.last().occupant.get('nick')).toBe('some1'); + // Check that the "add" event handler was removed. + expect(view.model.occupants._events.add.length).toBe(add_events); + done(); + })); - await view.model.queueMessage(received_stanza); - spyOn(converse.env.log, 'warn'); - _converse.connection._dataRecv(test_utils.createRequest(received_stanza)); - await u.waitUntil(() => view.model.onMessage.calls.count()); - expect(converse.env.log.warn).toHaveBeenCalledWith( - 'onMessage: Ignoring unencapsulated forwarded groupchat message' - ); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); - expect(view.model.messages.length).toBe(0); - done(); - })); + it("keeps track whether you are the sender or not", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - it("can contain a chat state notification and will still be shown", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); } - const message = 'romeo: Your attention is required'; - const 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) - .c('active', {'xmlns': "http://jabber.org/protocol/chatstates"}) - .tree(); - await view.model.queueMessage(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelector('.chat-msg')).not.toBe(null); - done(); - })); - - it("is specially marked when you are mentioned in it", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); } - const message = 'romeo: Your attention is required'; - const 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 new Promise(resolve => view.once('messageInserted', resolve)); - expect(u.hasClass('mentioned', view.el.querySelector('.chat-msg'))).toBeTruthy(); - done(); - })); - - it("can not be expected to have a unique id attribute", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); } - const id = u.getUniqueId(); - let msg = $msg({ - from: 'lounge@montague.lit/some1', - id: id, - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('First message').tree(); - await view.model.queueMessage(msg); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); - - msg = $msg({ - from: 'lounge@montague.lit/some2', - id: id, - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('Another message').tree(); - await view.model.queueMessage(msg); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2); - expect(view.model.messages.length).toBe(2); - done(); - })); - - it("is ignored if it has the same archive-id of an already received one", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'room@muc.example.com'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - spyOn(view.model, 'getDuplicateMessage').and.callThrough(); - let stanza = u.toStanza(` - - Typical body text - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.messages.length === 1); - await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 1); - let result = await view.model.getDuplicateMessage.calls.all()[0].returnValue; - expect(result).toBe(undefined); - - stanza = u.toStanza(` - - - - - - Typical body text - - - - `); - - spyOn(view.model, 'updateMessage'); - await view.model.queueMessage(stanza); - await u.waitUntil(() => view.model.getDuplicateMessage.calls.count() === 2); - result = await view.model.getDuplicateMessage.calls.all()[1].returnValue; - expect(result instanceof _converse.Message).toBe(true); - expect(view.model.messages.length).toBe(1); - await u.waitUntil(() => view.model.updateMessage.calls.count()); - done(); - })); - - it("is ignored if it has the same stanza-id of an already received one", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'room@muc.example.com'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - spyOn(view.model, 'getStanzaIdQueryAttrs').and.callThrough(); - let stanza = u.toStanza(` - - Typical body text - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.messages.length === 1); - await u.waitUntil(() => view.model.getStanzaIdQueryAttrs.calls.count() === 1); - let result = await view.model.getStanzaIdQueryAttrs.calls.all()[0].returnValue; - expect(result instanceof Array).toBe(true); - expect(result[0] instanceof Object).toBe(true); - expect(result[0]['stanza_id room@muc.example.com']).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad"); - - stanza = u.toStanza(` - - Typical body text - - `); - spyOn(view.model, 'updateMessage'); - spyOn(view.model, 'getDuplicateMessage').and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.getDuplicateMessage.calls.count()); - result = await view.model.getDuplicateMessage.calls.all()[0].returnValue; - expect(result instanceof _converse.Message).toBe(true); - expect(view.model.messages.length).toBe(1); - await u.waitUntil(() => view.model.updateMessage.calls.count()); - 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); - const muc_jid = 'xsf@muc.xmpp.org'; - const sender_jid = `${muc_jid}/romeo`; - const impersonated_jid = `${muc_jid}/i_am_groot` - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const stanza = $pres({ - to: 'romeo@montague.lit/_converse.js-29092160', - from: sender_jid - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }).tree(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - /* - * - * - * - * - * I am groot. - * - * - * - * - */ - const msg = $msg({ - 'from': sender_jid, - 'id': _converse.connection.getUniqueId(), - 'to': _converse.connection.jid, - 'type': 'groupchat', - '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': muc_jid, - 'type': 'groupchat' - }).c('body').t('I am groot').tree(); - const view = _converse.api.chatviews.get(muc_jid); - spyOn(converse.env.log, 'warn'); - await view.model.queueMessage(msg); - expect(converse.env.log.warn).toHaveBeenCalledWith( - 'onMessage: Ignoring XEP-0280 "groupchat" message carbon, '+ - 'according to the XEP groupchat messages SHOULD NOT be carbon copied' - ); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); - expect(view.model.messages.length).toBe(0); - done(); - })); - - it("keeps track of the sender's role and affiliation", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - let msg = $msg({ + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + const msg = $msg({ from: 'lounge@montague.lit/romeo', id: u.getUniqueId(), to: 'romeo@montague.lit', type: 'groupchat' }).c('body').t('I wrote this message!').tree(); - await view.model.queueMessage(msg); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length); - expect(view.model.messages.last().occupant.get('affiliation')).toBe('owner'); - expect(view.model.messages.last().occupant.get('role')).toBe('moderator'); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat moderator owner'); - let presence = $pres({ - to:'romeo@montague.lit/orchard', - from:'lounge@montague.lit/romeo', - id: u.getUniqueId() - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - affiliation: 'member', - jid: 'romeo@montague.lit/orchard', - role: 'participant' - }).up() - .c('status').attrs({code:'110'}).up() - .c('status').attrs({code:'210'}).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(presence)); + await view.model.queueMessage(msg); + expect(view.model.messages.last().get('sender')).toBe('me'); + done(); + })); - msg = $msg({ - from: 'lounge@montague.lit/romeo', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('Another message!').tree(); - await view.model.queueMessage(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.model.messages.last().occupant.get('affiliation')).toBe('member'); - expect(view.model.messages.last().occupant.get('role')).toBe('participant'); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); - expect(sizzle('.chat-msg', view.el).pop().classList.value.trim()).toBe('message chat-msg groupchat participant member'); + it("can be replaced with a correction", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - presence = $pres({ - to:'romeo@montague.lit/orchard', - from:'lounge@montague.lit/romeo', - id: u.getUniqueId() - }).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:'210'}).nodeTree; - _converse.connection._dataRecv(test_utils.createRequest(presence)); + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + const stanza = $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' + }).tree(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + const msg_id = u.getUniqueId(); + await view.model.queueMessage($msg({ + 'from': 'lounge@montague.lit/newguy', + 'to': _converse.connection.jid, + 'type': 'groupchat', + 'id': msg_id, + }).c('body').t('But soft, what light through yonder airlock breaks?').tree()); - view.model.sendMessage('hello world'); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 3); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length); + 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 occupant = await u.waitUntil(() => view.model.messages.filter(m => m.get('type') === 'groupchat')[2].occupant); - expect(occupant.get('affiliation')).toBe('owner'); - expect(occupant.get('role')).toBe('moderator'); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(3); - await u.waitUntil(() => sizzle('.chat-msg', view.el).pop().classList.value.trim() === 'message chat-msg groupchat moderator owner'); - - const add_events = view.model.occupants._events.add.length; - msg = $msg({ - from: 'lounge@montague.lit/some1', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('Message from someone not in the MUC right now').tree(); - await view.model.queueMessage(msg); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.model.messages.last().occupant).toBeUndefined(); - // Check that there's a new "add" event handler, for when the occupant appears. - expect(view.model.occupants._events.add.length).toBe(add_events+1); - - // Check that the occupant gets added/removed to the message as it - // gets removed or added. - presence = $pres({ - to:'romeo@montague.lit/orchard', - from:'lounge@montague.lit/some1', - id: u.getUniqueId() - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({jid: 'some1@montague.lit/orchard'}); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.model.messages.last().occupant); - expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now'); - expect(view.model.messages.last().occupant.get('nick')).toBe('some1'); - // Check that the "add" event handler was removed. - expect(view.model.occupants._events.add.length).toBe(add_events); - - presence = $pres({ - to:'romeo@montague.lit/orchard', - type: 'unavailable', - from:'lounge@montague.lit/some1', - id: u.getUniqueId() - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({jid: 'some1@montague.lit/orchard'}); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => !view.model.messages.last().occupant); - expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now'); - expect(view.model.messages.last().occupant).toBeUndefined(); - // Check that there's a new "add" event handler, for when the occupant appears. - expect(view.model.occupants._events.add.length).toBe(add_events+1); - - presence = $pres({ - to:'romeo@montague.lit/orchard', - from:'lounge@montague.lit/some1', - id: u.getUniqueId() - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({jid: 'some1@montague.lit/orchard'}); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await u.waitUntil(() => view.model.messages.last().occupant); - expect(view.model.messages.last().get('message')).toBe('Message from someone not in the MUC right now'); - expect(view.model.messages.last().occupant.get('nick')).toBe('some1'); - // Check that the "add" event handler was removed. - expect(view.model.occupants._events.add.length).toBe(add_events); - done(); - })); - - - it("keeps track whether you are the sender or not", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - const msg = $msg({ - from: 'lounge@montague.lit/romeo', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('I wrote this message!').tree(); - await view.model.queueMessage(msg); - expect(view.model.messages.last().get('sender')).toBe('me'); - done(); - })); - - it("can be replaced with a correction", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - const stanza = $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' - }).tree(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - const msg_id = u.getUniqueId(); - await view.model.queueMessage($msg({ - 'from': 'lounge@montague.lit/newguy', - 'to': _converse.connection.jid, - 'type': 'groupchat', - 'id': msg_id, - }).c('body').t('But soft, what light through yonder airlock breaks?').tree()); - - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length); - 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?'); - - await view.model.queueMessage($msg({ - 'from': 'lounge@montague.lit/newguy', - 'to': _converse.connection.jid, - 'type': 'groupchat', - '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 u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent === - 'But soft, what light through yonder chimney breaks?', 500); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1); - - await view.model.queueMessage($msg({ - 'from': 'lounge@montague.lit/newguy', - 'to': _converse.connection.jid, - 'type': 'groupchat', - '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 u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent === - 'But soft, what light through yonder window breaks?', 500); - 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[2].textContent).toBe('But soft, what light through yonder airlock breaks?'); - expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME'); - expect(older_msgs[1].childNodes[0].nodeName).toBe('TIME'); - expect(older_msgs[1].childNodes[2].textContent).toBe('But soft, what light through yonder chimney breaks?'); - done(); - })); - - it("can be sent as a correction by using the up arrow", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_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 u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 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 - }); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); - 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); - expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(true); - - 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 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); - - // Check that messages from other users are skipped - await view.model.queueMessage($msg({ - 'from': muc_jid+'/someone-else', + await view.model.queueMessage($msg({ + 'from': 'lounge@montague.lit/newguy', + 'to': _converse.connection.jid, + 'type': 'groupchat', 'id': u.getUniqueId(), - 'to': 'romeo@montague.lit', - 'type': 'groupchat' - }).c('body').t('Hello world').tree()); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); + }).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 u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent === + 'But soft, what light through yonder chimney breaks?', 500); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1); - // 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(2); - 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(2); - await u.waitUntil(() => !u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500); - done(); - })); + await view.model.queueMessage($msg({ + 'from': 'lounge@montague.lit/newguy', + 'to': _converse.connection.jid, + 'type': 'groupchat', + '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()); - it("will be shown as received upon MUC reflection", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent === + 'But soft, what light through yonder window breaks?', 500); + 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[2].textContent).toBe('But soft, what light through yonder airlock breaks?'); + expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME'); + expect(older_msgs[1].childNodes[0].nodeName).toBe('TIME'); + expect(older_msgs[1].childNodes[2].textContent).toBe('But soft, what light through yonder chimney breaks?'); + done(); + })); - await test_utils.waitForRoster(_converse, 'current'); - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_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)); - expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(0); + it("can be sent as a correction by using the up arrow", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - const msg_obj = view.model.messages.at(0); - const stanza = u.toStanza(` - - ${msg_obj.get('message')} - - - `); - await view.model.queueMessage(stanza); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); - expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); - expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1); - expect(view.model.messages.length).toBe(1); - - const message = view.model.messages.at(0); - expect(message.get('stanza_id lounge@montague.lit')).toBe('5f3dbc5e-e1d3-4077-a492-693f3769c7ad'); - expect(message.get('origin_id')).toBe(msg_obj.get('origin_id')); - done(); - })); - - it("gets updated with its stanza-id upon MUC reflection", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'room@muc.example.com'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - - view.model.sendMessage('hello world'); - await u.waitUntil(() => view.model.messages.length === 1); - const msg = view.model.messages.at(0); - expect(msg.get('stanza_id')).toBeUndefined(); - expect(msg.get('origin_id')).toBe(msg.get('origin_id')); - - const stanza = u.toStanza(` - - Hello world - - - `); - spyOn(view.model, 'updateMessage').and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.updateMessage.calls.count() === 1); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('stanza_id room@muc.example.com')).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad"); - expect(view.model.messages.at(0).get('origin_id')).toBe(msg.get('origin_id')); - done(); - })); - - it("can cause a delivery receipt to be returned", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_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)); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - - const msg_obj = view.model.messages.at(0); - const stanza = u.toStanza(` - - - - `); - spyOn(_converse.api, "trigger").and.callThrough(); - spyOn(stanza_utils, "isReceipt").and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => stanza_utils.isReceipt.calls.count() === 1); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); - expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); - done(); - })); - - it("can cause a chat marker to be returned", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current'); - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_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)); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelector('.chat-msg .chat-msg__body').textContent.trim()) - .toBe("But soft, what light through yonder airlock breaks?"); - - const msg_obj = view.model.messages.at(0); - let stanza = u.toStanza(` - - - `); - const stanza_utils = converse.env.stanza_utils; - spyOn(stanza_utils, "isChatMarker").and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 1); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); - - stanza = u.toStanza(` - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 2); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); - - stanza = u.toStanza(` - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 3); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); - - stanza = u.toStanza(` - - 'tis I! - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 4); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); - expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); - done(); - })); - - describe("when received", function () { - - it("highlights all users mentioned via XEP-0372 references", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'tom'); - const view = _converse.api.chatviews.get(muc_jid); - ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].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' - })) - ); - }); - const msg = $msg({ - from: 'lounge@montague.lit/gibson', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('hello z3r0 tom mr.robot, how are you?').up() - .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'6', 'end':'10', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up() - .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up() - .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree; - await view.model.queueMessage(msg); - const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text')); - expect(message.classList.length).toEqual(1); - expect(message.innerHTML).toBe( - 'hello z3r0 '+ - 'tom '+ - 'mr.robot, how are you?'); - done(); - })); - - it("highlights all users mentioned via XEP-0372 references in a quoted message", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'tom'); - const view = _converse.api.chatviews.get(muc_jid); - ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].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' - })) - ); - }); - const msg = $msg({ - from: 'lounge@montague.lit/gibson', - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('>hello z3r0 tom mr.robot, how are you?').up() - .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'7', 'end':'11', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up() - .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'12', 'end':'15', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up() - .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'16', 'end':'24', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree; - await view.model.queueMessage(msg); - const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text')); - expect(message.classList.length).toEqual(1); - expect(message.innerHTML).toBe( - '>hello z3r0 '+ - 'tom '+ - 'mr.robot, how are you?'); - done(); - })); + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_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(''); - describe("in which someone is mentioned", function () { + textarea.value = 'But soft, what light through yonder airlock breaks?'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); + expect(view.el.querySelector('.chat-msg__text').textContent) + .toBe('But soft, what light through yonder airlock breaks?'); - it("gets parsed for mentions which get turned into references", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + 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 + }); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); + 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); + expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(true); - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'tom'); - const view = _converse.api.chatviews.get(muc_jid); - ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh', 'Link Mauve'].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.replace(/\s/g, '-')}@montague.lit/resource`, - 'role': 'participant' - }))); - }); + 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)); - // Also check that nicks from received messages, (but for which - // we don't have occupant objects) can be mentioned. - const stanza = u.toStanza(` - - Boo! - `); - await view.model.queueMessage(stanza); + const msg = _converse.connection.send.calls.all()[0].args[0]; + expect(msg.toLocaleString()) + .toBe(``+ + `But soft, what light through yonder window breaks?`+ + ``+ + ``+ + ``+ + ``); - // Run a few unit tests for the parseTextForReferences method - let [text, references] = view.model.parseTextForReferences('hello z3r0') - expect(references.length).toBe(0); - expect(text).toBe('hello z3r0'); + 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); - [text, references] = view.model.parseTextForReferences('hello @z3r0') - expect(references.length).toBe(1); - expect(text).toBe('hello z3r0'); - expect(JSON.stringify(references)) - .toBe('[{"begin":6,"end":10,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"}]'); + 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?'); - [text, references] = view.model.parseTextForReferences('hello @some1 @z3r0 @gibson @mr.robot, how are you?') - expect(text).toBe('hello @some1 z3r0 gibson mr.robot, how are you?'); - expect(JSON.stringify(references)) - .toBe('[{"begin":13,"end":17,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"},'+ - '{"begin":18,"end":24,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"},'+ - '{"begin":25,"end":33,"value":"mr.robot","type":"mention","uri":"xmpp:mr.robot@montague.lit"}]'); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(u.hasClass('correcting', view.el.querySelector('.chat-msg'))).toBe(false); - [text, references] = view.model.parseTextForReferences('yo @gib') - expect(text).toBe('yo @gib'); - expect(references.length).toBe(0); + // Check that messages from other users are skipped + await view.model.queueMessage($msg({ + 'from': muc_jid+'/someone-else', + 'id': u.getUniqueId(), + 'to': 'romeo@montague.lit', + 'type': 'groupchat' + }).c('body').t('Hello world').tree()); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); - [text, references] = view.model.parseTextForReferences('yo @gibsonian') - expect(text).toBe('yo @gibsonian'); - expect(references.length).toBe(0); + // 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(2); + 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(2); + await u.waitUntil(() => !u.hasClass('correcting', view.el.querySelector('.chat-msg')), 500); + done(); + })); - [text, references] = view.model.parseTextForReferences('@gibson') - expect(text).toBe('gibson'); - expect(references.length).toBe(1); - expect(JSON.stringify(references)) - .toBe('[{"begin":0,"end":6,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"}]'); + it("will be shown as received upon MUC reflection", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - [text, references] = view.model.parseTextForReferences('hi @Link Mauve how are you?') - expect(text).toBe('hi Link Mauve how are you?'); - expect(references.length).toBe(1); - expect(JSON.stringify(references)) - .toBe('[{"begin":3,"end":13,"value":"Link Mauve","type":"mention","uri":"xmpp:Link-Mauve@montague.lit"}]'); + await mock.waitForRoster(_converse, 'current'); + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_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)); + expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(0); - [text, references] = view.model.parseTextForReferences('https://example.org/@gibson') - expect(text).toBe('https://example.org/@gibson'); - expect(references.length).toBe(0); - expect(JSON.stringify(references)) - .toBe('[]'); + const msg_obj = view.model.messages.at(0); + const stanza = u.toStanza(` + + ${msg_obj.get('message')} + + + `); + await view.model.queueMessage(stanza); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); + expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); + expect(view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length).toBe(1); + expect(view.model.messages.length).toBe(1); - [text, references] = view.model.parseTextForReferences('mail@gibson.com') - expect(text).toBe('mail@gibson.com'); - expect(references.length).toBe(0); - expect(JSON.stringify(references)) - .toBe('[]'); + const message = view.model.messages.at(0); + expect(message.get('stanza_id lounge@montague.lit')).toBe('5f3dbc5e-e1d3-4077-a492-693f3769c7ad'); + expect(message.get('origin_id')).toBe(msg_obj.get('origin_id')); + done(); + })); - [text, references] = view.model.parseTextForReferences( - 'https://linkmauve.fr@Link Mauve/ https://linkmauve.fr/@github/is_back gibson@gibson.com gibson@Link Mauve.fr') - expect(text).toBe( - 'https://linkmauve.fr@Link Mauve/ https://linkmauve.fr/@github/is_back gibson@gibson.com gibson@Link Mauve.fr'); - expect(references.length).toBe(0); - expect(JSON.stringify(references)) - .toBe('[]'); + it("gets updated with its stanza-id upon MUC reflection", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - [text, references] = view.model.parseTextForReferences('@gh0st where are you?') - expect(text).toBe('gh0st where are you?'); - expect(references.length).toBe(1); - expect(JSON.stringify(references)) - .toBe('[{"begin":0,"end":5,"value":"gh0st","type":"mention","uri":"xmpp:lounge@montague.lit/gh0st"}]'); - done(); - })); + const muc_jid = 'room@muc.example.com'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); - it("parses for mentions as indicated with an @ preceded by a space or at the start of the text", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + view.model.sendMessage('hello world'); + await u.waitUntil(() => view.model.messages.length === 1); + const msg = view.model.messages.at(0); + expect(msg.get('stanza_id')).toBeUndefined(); + expect(msg.get('origin_id')).toBe(msg.get('origin_id')); - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'tom'); - const view = _converse.api.chatviews.get(muc_jid); - ['NotAnAdress', 'darnuria'].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.replace(/\s/g, '-')}@montague.lit/resource`, - 'role': 'participant' - }))); - }); + const stanza = u.toStanza(` + + Hello world + + + `); + spyOn(view.model, 'updateMessage').and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.updateMessage.calls.count() === 1); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('stanza_id room@muc.example.com')).toBe("5f3dbc5e-e1d3-4077-a492-693f3769c7ad"); + expect(view.model.messages.at(0).get('origin_id')).toBe(msg.get('origin_id')); + done(); + })); - // Test that we don't match @nick in email adresses. - let [text, references] = view.model.parseTextForReferences('contact contact@NotAnAdress.eu'); - expect(references.length).toBe(0); - expect(text).toBe('contact contact@NotAnAdress.eu'); + it("can cause a delivery receipt to be returned", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - // Test that we don't match @nick in url - [text, references] = view.model.parseTextForReferences('nice website https://darnuria.eu/@darnuria'); - expect(references.length).toBe(0); - expect(text).toBe('nice website https://darnuria.eu/@darnuria'); - done(); - })); + await mock.waitForRoster(_converse, 'current'); + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_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)); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + const msg_obj = view.model.messages.at(0); + const stanza = u.toStanza(` + + + + `); + spyOn(_converse.api, "trigger").and.callThrough(); + spyOn(stanza_utils, "isReceipt").and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => stanza_utils.isReceipt.calls.count() === 1); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); + expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object)); + done(); + })); - it("properly encodes the URIs in sent out references", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("can cause a chat marker to be returned", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'tom'); - const view = _converse.api.roomviews.get(muc_jid); - _converse.connection._dataRecv(test_utils.createRequest( + await mock.waitForRoster(_converse, 'current'); + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_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)); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelector('.chat-msg .chat-msg__body').textContent.trim()) + .toBe("But soft, what light through yonder airlock breaks?"); + + const msg_obj = view.model.messages.at(0); + let stanza = u.toStanza(` + + + `); + const stanza_utils = converse.env.stanza_utils; + spyOn(stanza_utils, "isChatMarker").and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 1); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); + + stanza = u.toStanza(` + + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 2); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); + + stanza = u.toStanza(` + + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 3); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); + + stanza = u.toStanza(` + + 'tis I! + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => stanza_utils.isChatMarker.calls.count() === 4); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(2); + expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(0); + done(); + })); + + describe("when received", function () { + + it("highlights all users mentioned via XEP-0372 references", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); + const view = _converse.api.chatviews.get(muc_jid); + ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => { + _converse.connection._dataRecv(mock.createRequest( $pres({ 'to': 'tom@montague.lit/resource', - 'from': `lounge@montague.lit/Link Mauve` + 'from': `lounge@montague.lit/${nick}` }) .c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', + 'jid': `${nick}@montague.lit/resource`, + 'role': 'participant' + })) + ); + }); + const msg = $msg({ + from: 'lounge@montague.lit/gibson', + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('hello z3r0 tom mr.robot, how are you?').up() + .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'6', 'end':'10', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up() + .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'11', 'end':'14', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up() + .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'15', 'end':'23', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree; + await view.model.queueMessage(msg); + const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text')); + expect(message.classList.length).toEqual(1); + expect(message.innerHTML).toBe( + 'hello z3r0 '+ + 'tom '+ + 'mr.robot, how are you?'); + done(); + })); + + it("highlights all users mentioned via XEP-0372 references in a quoted message", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); + const view = _converse.api.chatviews.get(muc_jid); + ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].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' + })) + ); + }); + const msg = $msg({ + from: 'lounge@montague.lit/gibson', + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('>hello z3r0 tom mr.robot, how are you?').up() + .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'7', 'end':'11', 'type':'mention', 'uri':'xmpp:z3r0@montague.lit'}).up() + .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'12', 'end':'15', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).up() + .c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'16', 'end':'24', 'type':'mention', 'uri':'xmpp:mr.robot@montague.lit'}).nodeTree; + await view.model.queueMessage(msg); + const message = await u.waitUntil(() => view.el.querySelector('.chat-msg__text')); + expect(message.classList.length).toEqual(1); + expect(message.innerHTML).toBe( + '>hello z3r0 '+ + 'tom '+ + 'mr.robot, how are you?'); + done(); + })); + }); + + describe("in which someone is mentioned", function () { + + it("gets parsed for mentions which get turned into references", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); + const view = _converse.api.chatviews.get(muc_jid); + ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh', 'Link Mauve'].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.replace(/\s/g, '-')}@montague.lit/resource`, 'role': 'participant' }))); - await u.waitUntil(() => view.model.occupants.length === 2); + }); - const textarea = view.el.querySelector('textarea.chat-textarea'); - textarea.value = 'hello @Link Mauve' - const enter_event = { - 'target': textarea, - 'preventDefault': function preventDefault () {}, - 'stopPropagation': function stopPropagation () {}, - 'keyCode': 13 // Enter - } - spyOn(_converse.connection, 'send'); - view.onKeyDown(enter_event); - await new Promise(resolve => view.once('messageInserted', resolve)); - const msg = _converse.connection.send.calls.all()[0].args[0]; - expect(msg.toLocaleString()) - .toBe(``+ - `hello Link Mauve`+ - ``+ - ``+ - ``+ - ``); - done(); - })); + // Also check that nicks from received messages, (but for which + // we don't have occupant objects) can be mentioned. + const stanza = u.toStanza(` + + Boo! + `); + await view.model.queueMessage(stanza); - it("can get corrected and given new references", - mock.initConverse( - ['rosterGroupsFetched'], {}, + // Run a few unit tests for the parseTextForReferences method + let [text, references] = view.model.parseTextForReferences('hello z3r0') + expect(references.length).toBe(0); + expect(text).toBe('hello z3r0'); + + [text, references] = view.model.parseTextForReferences('hello @z3r0') + expect(references.length).toBe(1); + expect(text).toBe('hello z3r0'); + expect(JSON.stringify(references)) + .toBe('[{"begin":6,"end":10,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"}]'); + + [text, references] = view.model.parseTextForReferences('hello @some1 @z3r0 @gibson @mr.robot, how are you?') + expect(text).toBe('hello @some1 z3r0 gibson mr.robot, how are you?'); + expect(JSON.stringify(references)) + .toBe('[{"begin":13,"end":17,"value":"z3r0","type":"mention","uri":"xmpp:z3r0@montague.lit"},'+ + '{"begin":18,"end":24,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"},'+ + '{"begin":25,"end":33,"value":"mr.robot","type":"mention","uri":"xmpp:mr.robot@montague.lit"}]'); + + [text, references] = view.model.parseTextForReferences('yo @gib') + expect(text).toBe('yo @gib'); + expect(references.length).toBe(0); + + [text, references] = view.model.parseTextForReferences('yo @gibsonian') + expect(text).toBe('yo @gibsonian'); + expect(references.length).toBe(0); + + [text, references] = view.model.parseTextForReferences('@gibson') + expect(text).toBe('gibson'); + expect(references.length).toBe(1); + expect(JSON.stringify(references)) + .toBe('[{"begin":0,"end":6,"value":"gibson","type":"mention","uri":"xmpp:gibson@montague.lit"}]'); + + [text, references] = view.model.parseTextForReferences('hi @Link Mauve how are you?') + expect(text).toBe('hi Link Mauve how are you?'); + expect(references.length).toBe(1); + expect(JSON.stringify(references)) + .toBe('[{"begin":3,"end":13,"value":"Link Mauve","type":"mention","uri":"xmpp:Link-Mauve@montague.lit"}]'); + + [text, references] = view.model.parseTextForReferences('https://example.org/@gibson') + expect(text).toBe('https://example.org/@gibson'); + expect(references.length).toBe(0); + expect(JSON.stringify(references)) + .toBe('[]'); + + [text, references] = view.model.parseTextForReferences('mail@gibson.com') + expect(text).toBe('mail@gibson.com'); + expect(references.length).toBe(0); + expect(JSON.stringify(references)) + .toBe('[]'); + + [text, references] = view.model.parseTextForReferences( + 'https://linkmauve.fr@Link Mauve/ https://linkmauve.fr/@github/is_back gibson@gibson.com gibson@Link Mauve.fr') + expect(text).toBe( + 'https://linkmauve.fr@Link Mauve/ https://linkmauve.fr/@github/is_back gibson@gibson.com gibson@Link Mauve.fr'); + expect(references.length).toBe(0); + expect(JSON.stringify(references)) + .toBe('[]'); + + [text, references] = view.model.parseTextForReferences('@gh0st where are you?') + expect(text).toBe('gh0st where are you?'); + expect(references.length).toBe(1); + expect(JSON.stringify(references)) + .toBe('[{"begin":0,"end":5,"value":"gh0st","type":"mention","uri":"xmpp:lounge@montague.lit/gh0st"}]'); + done(); + })); + + it("parses for mentions as indicated with an @ preceded by a space or at the start of the text", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); + const view = _converse.api.chatviews.get(muc_jid); + ['NotAnAdress', 'darnuria'].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.replace(/\s/g, '-')}@montague.lit/resource`, + 'role': 'participant' + }))); + }); + + // Test that we don't match @nick in email adresses. + let [text, references] = view.model.parseTextForReferences('contact contact@NotAnAdress.eu'); + expect(references.length).toBe(0); + expect(text).toBe('contact contact@NotAnAdress.eu'); + + // Test that we don't match @nick in url + [text, references] = view.model.parseTextForReferences('nice website https://darnuria.eu/@darnuria'); + expect(references.length).toBe(0); + expect(text).toBe('nice website https://darnuria.eu/@darnuria'); + done(); + })); + + + it("properly encodes the URIs in sent out references", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); + const view = _converse.api.roomviews.get(muc_jid); + _converse.connection._dataRecv(mock.createRequest( + $pres({ + 'to': 'tom@montague.lit/resource', + 'from': `lounge@montague.lit/Link Mauve` + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'role': 'participant' + }))); + await u.waitUntil(() => view.model.occupants.length === 2); + + const textarea = view.el.querySelector('textarea.chat-textarea'); + textarea.value = 'hello @Link Mauve' + const enter_event = { + 'target': textarea, + 'preventDefault': function preventDefault () {}, + 'stopPropagation': function stopPropagation () {}, + 'keyCode': 13 // Enter + } + spyOn(_converse.connection, 'send'); + view.onKeyDown(enter_event); + await new Promise(resolve => view.once('messageInserted', resolve)); + const msg = _converse.connection.send.calls.all()[0].args[0]; + expect(msg.toLocaleString()) + .toBe(``+ + `hello Link Mauve`+ + ``+ + ``+ + ``+ + ``); + done(); + })); + + it("can get corrected and given new references", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom'); + const view = _converse.api.chatviews.get(muc_jid); + ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].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' + }))); + }); + await u.waitUntil(() => view.model.occupants.length === 5); + + const textarea = view.el.querySelector('textarea.chat-textarea'); + textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?' + const enter_event = { + 'target': textarea, + 'preventDefault': function preventDefault () {}, + 'stopPropagation': function stopPropagation () {}, + 'keyCode': 13 // Enter + } + spyOn(_converse.connection, 'send'); + view.onKeyDown(enter_event); + await new Promise(resolve => view.once('messageInserted', resolve)); + const msg = _converse.connection.send.calls.all()[0].args[0]; + expect(msg.toLocaleString()) + .toBe(``+ + `hello z3r0 gibson mr.robot, how are you?`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``); + + const action = view.el.querySelector('.chat-msg .chat-msg__action'); + action.style.opacity = 1; + action.click(); + + expect(textarea.value).toBe('hello @z3r0 @gibson @mr.robot, how are you?'); + 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); + await u.waitUntil(() => _converse.connection.send.calls.count() === 2); + + textarea.value = 'hello @z3r0 @gibson @sw0rdf1sh, how are you?'; + view.onKeyDown(enter_event); + await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent === + 'hello z3r0 gibson sw0rdf1sh, how are you?', 500); + + const correction = _converse.connection.send.calls.all()[2].args[0]; + expect(correction.toLocaleString()) + .toBe(``+ + `hello z3r0 gibson sw0rdf1sh, how are you?`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``); + done(); + })); + + it("includes XEP-0372 references to that person", + mock.initConverse( + ['rosterGroupsFetched'], {}, async function (done, _converse) { - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'tom'); - const view = _converse.api.chatviews.get(muc_jid); - ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].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 u.waitUntil(() => view.model.occupants.length === 5); + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].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' + }))); + }); + await u.waitUntil(() => view.model.occupants.length === 5); - const textarea = view.el.querySelector('textarea.chat-textarea'); - textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?' - const enter_event = { - 'target': textarea, - 'preventDefault': function preventDefault () {}, - 'stopPropagation': function stopPropagation () {}, - 'keyCode': 13 // Enter - } - spyOn(_converse.connection, 'send'); - view.onKeyDown(enter_event); - await new Promise(resolve => view.once('messageInserted', resolve)); - const msg = _converse.connection.send.calls.all()[0].args[0]; - expect(msg.toLocaleString()) - .toBe(``+ - `hello z3r0 gibson mr.robot, how are you?`+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``); + spyOn(_converse.connection, 'send'); + const textarea = view.el.querySelector('textarea.chat-textarea'); + textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?' + const enter_event = { + 'target': textarea, + 'preventDefault': function preventDefault () {}, + 'stopPropagation': function stopPropagation () {}, + 'keyCode': 13 // Enter + } + view.onKeyDown(enter_event); + await new Promise(resolve => view.once('messageInserted', resolve)); - const action = view.el.querySelector('.chat-msg .chat-msg__action'); - action.style.opacity = 1; - action.click(); - - expect(textarea.value).toBe('hello @z3r0 @gibson @mr.robot, how are you?'); - 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); - await u.waitUntil(() => _converse.connection.send.calls.count() === 2); - - textarea.value = 'hello @z3r0 @gibson @sw0rdf1sh, how are you?'; - view.onKeyDown(enter_event); - await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent === - 'hello z3r0 gibson sw0rdf1sh, how are you?', 500); - - const correction = _converse.connection.send.calls.all()[2].args[0]; - expect(correction.toLocaleString()) - .toBe(``+ - `hello z3r0 gibson sw0rdf1sh, how are you?`+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``); - done(); - })); - - it("includes XEP-0372 references to that person", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].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 u.waitUntil(() => view.model.occupants.length === 5); - - spyOn(_converse.connection, 'send'); - const textarea = view.el.querySelector('textarea.chat-textarea'); - textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?' - const enter_event = { - 'target': textarea, - 'preventDefault': function preventDefault () {}, - 'stopPropagation': function stopPropagation () {}, - 'keyCode': 13 // Enter - } - view.onKeyDown(enter_event); - await new Promise(resolve => view.once('messageInserted', resolve)); - - const msg = _converse.connection.send.calls.all()[0].args[0]; - expect(msg.toLocaleString()) - .toBe(``+ - `hello z3r0 gibson mr.robot, how are you?`+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``); - done(); - })); - }); + const msg = _converse.connection.send.calls.all()[0].args[0]; + expect(msg.toLocaleString()) + .toBe(``+ + `hello z3r0 gibson mr.robot, how are you?`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``); + done(); + })); }); }); diff --git a/spec/notification.js b/spec/notification.js index 3c4d9951d..b17c68b75 100644 --- a/spec/notification.js +++ b/spec/notification.js @@ -1,209 +1,207 @@ -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("Notifications", function () { - // Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config +const _ = converse.env._; +const $msg = converse.env.$msg; +const u = converse.env.utils; - describe("When show_desktop_notifications is set to true", function () { - describe("And the desktop is not focused", function () { - describe("an HTML5 Notification", function () { +describe("Notifications", function () { + // Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config - it("is shown when a new private message is received", - mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => { + describe("When show_desktop_notifications is set to true", function () { + describe("And the desktop is not focused", function () { + describe("an HTML5 Notification", function () { - await test_utils.waitForRoster(_converse, 'current'); - spyOn(_converse, 'showMessageNotification').and.callThrough(); - spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); - spyOn(_converse, 'isMessageToHiddenChat').and.returnValue(true); - - const message = 'This message will show a desktop notification'; - 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': 'http://jabber.org/protocol/chatstates'}).tree(); - await _converse.handleMessageStanza(msg); // This will emit 'message' - await u.waitUntil(() => _converse.api.chatviews.get(sender_jid)); - expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled(); - expect(_converse.showMessageNotification).toHaveBeenCalled(); - done(); - })); - - it("is shown when you are mentioned in a groupchat", - mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => { - - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - const view = _converse.api.chatviews.get('lounge@montague.lit'); - if (!view.el.querySelectorAll('.chat-area').length) { - view.renderChatArea(); - } - let no_notification = false; - if (typeof window.Notification === 'undefined') { - no_notification = true; - window.Notification = function () { - return { - 'close': function () {} - }; - }; - } - spyOn(_converse, 'showMessageNotification').and.callThrough(); - spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); - - const message = 'romeo: This message will show a desktop notification'; - const 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(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - await new Promise(resolve => view.once('messageInserted', resolve)); - - await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 1); - expect(_converse.showMessageNotification).toHaveBeenCalled(); - if (no_notification) { - delete window.Notification; - } - done(); - })); - - it("is shown for headline messages", - mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => { - - spyOn(_converse, 'showMessageNotification').and.callThrough(); - spyOn(_converse, 'isMessageToHiddenChat').and.returnValue(true); - spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); - 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().length); - const view = _converse.chatboxviews.get('notify.example.com'); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect( - _.includes(_converse.chatboxviews.keys(), - 'notify.example.com') - ).toBeTruthy(); - expect(_converse.showMessageNotification).toHaveBeenCalled(); - done(); - })); - - it("is not shown for full JID headline messages if allow_non_roster_messaging is false", mock.initConverse((done, _converse) => { - _converse.allow_non_roster_messaging = false; - spyOn(_converse, 'showMessageNotification').and.callThrough(); - spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); - const stanza = $msg({ - 'type': 'headline', - 'from': 'someone@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)); - expect( - _.includes(_converse.chatboxviews.keys(), - 'someone@notify.example.com') - ).toBeFalsy(); - expect(_converse.showMessageNotification).not.toHaveBeenCalled(); - done(); - })); - - it("is shown when a user changes their chat state (if show_chat_state_notifications is true)", - mock.initConverse(['rosterGroupsFetched'], {show_chat_state_notifications: true}, - async (done, _converse) => { - - await test_utils.waitForRoster(_converse, 'current', 3); - spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); - spyOn(_converse, 'showChatStateNotification'); - const jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - _converse.roster.get(jid).presence.set('show', 'busy'); // This will emit 'contactStatusChanged' - await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 1); - expect(_converse.showChatStateNotification).toHaveBeenCalled(); - done() - })); - }); - }); - - describe("When a new contact request is received", function () { - it("an HTML5 Notification is received", mock.initConverse((done, _converse) => { - spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); - spyOn(_converse, 'showContactRequestNotification'); - _converse.api.trigger('contactRequest', {'fullname': 'Peter Parker', 'jid': 'peter@parker.com'}); - expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled(); - expect(_converse.showContactRequestNotification).toHaveBeenCalled(); - done(); - })); - }); - }); - - describe("When play_sounds is set to true", function () { - describe("A notification sound", function () { - - it("is played when the current user is mentioned in a groupchat", + it("is shown when a new private message is received", mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => { - test_utils.createContacts(_converse, 'current'); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - _converse.play_sounds = true; - spyOn(_converse, 'playSoundNotification'); - const view = _converse.chatboxviews.get('lounge@montague.lit'); + await mock.waitForRoster(_converse, 'current'); + spyOn(_converse, 'showMessageNotification').and.callThrough(); + spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); + spyOn(_converse, 'isMessageToHiddenChat').and.returnValue(true); + + const message = 'This message will show a desktop notification'; + 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': 'http://jabber.org/protocol/chatstates'}).tree(); + await _converse.handleMessageStanza(msg); // This will emit 'message' + await u.waitUntil(() => _converse.api.chatviews.get(sender_jid)); + expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled(); + expect(_converse.showMessageNotification).toHaveBeenCalled(); + done(); + })); + + it("is shown when you are mentioned in a groupchat", + mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => { + + await mock.waitForRoster(_converse, 'current'); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + const view = _converse.api.chatviews.get('lounge@montague.lit'); if (!view.el.querySelectorAll('.chat-area').length) { view.renderChatArea(); } - let text = 'This message will play a sound because it mentions romeo'; - let message = $msg({ - from: 'lounge@montague.lit/otheruser', - id: '1', - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t(text); - await view.model.queueMessage(message.nodeTree); - await u.waitUntil(() => _converse.playSoundNotification.calls.count()); - expect(_converse.playSoundNotification).toHaveBeenCalled(); + let no_notification = false; + if (typeof window.Notification === 'undefined') { + no_notification = true; + window.Notification = function () { + return { + 'close': function () {} + }; + }; + } + spyOn(_converse, 'showMessageNotification').and.callThrough(); + spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); - text = "This message won't play a sound"; - message = $msg({ - from: 'lounge@montague.lit/otheruser', - id: '2', - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t(text); - await view.model.queueMessage(message.nodeTree); - expect(_converse.playSoundNotification, 1); - _converse.play_sounds = false; + const message = 'romeo: This message will show a desktop notification'; + const 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(); + _converse.connection._dataRecv(mock.createRequest(msg)); + await new Promise(resolve => view.once('messageInserted', resolve)); - text = "This message won't play a sound because it is sent by romeo"; - message = $msg({ - from: 'lounge@montague.lit/romeo', - id: '3', - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t(text); - await view.model.queueMessage(message.nodeTree); - expect(_converse.playSoundNotification, 1); - _converse.play_sounds = false; + await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 1); + expect(_converse.showMessageNotification).toHaveBeenCalled(); + if (no_notification) { + delete window.Notification; + } done(); })); + + it("is shown for headline messages", + mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => { + + spyOn(_converse, 'showMessageNotification').and.callThrough(); + spyOn(_converse, 'isMessageToHiddenChat').and.returnValue(true); + spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); + 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)); + await u.waitUntil(() => _converse.chatboxviews.keys().length); + const view = _converse.chatboxviews.get('notify.example.com'); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect( + _.includes(_converse.chatboxviews.keys(), + 'notify.example.com') + ).toBeTruthy(); + expect(_converse.showMessageNotification).toHaveBeenCalled(); + done(); + })); + + it("is not shown for full JID headline messages if allow_non_roster_messaging is false", mock.initConverse((done, _converse) => { + _converse.allow_non_roster_messaging = false; + spyOn(_converse, 'showMessageNotification').and.callThrough(); + spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); + const stanza = $msg({ + 'type': 'headline', + 'from': 'someone@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)); + expect( + _.includes(_converse.chatboxviews.keys(), + 'someone@notify.example.com') + ).toBeFalsy(); + expect(_converse.showMessageNotification).not.toHaveBeenCalled(); + done(); + })); + + it("is shown when a user changes their chat state (if show_chat_state_notifications is true)", + mock.initConverse(['rosterGroupsFetched'], {show_chat_state_notifications: true}, + async (done, _converse) => { + + await mock.waitForRoster(_converse, 'current', 3); + spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); + spyOn(_converse, 'showChatStateNotification'); + const jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + _converse.roster.get(jid).presence.set('show', 'busy'); // This will emit 'contactStatusChanged' + await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 1); + expect(_converse.showChatStateNotification).toHaveBeenCalled(); + done() + })); }); }); + + describe("When a new contact request is received", function () { + it("an HTML5 Notification is received", mock.initConverse((done, _converse) => { + spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); + spyOn(_converse, 'showContactRequestNotification'); + _converse.api.trigger('contactRequest', {'fullname': 'Peter Parker', 'jid': 'peter@parker.com'}); + expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled(); + expect(_converse.showContactRequestNotification).toHaveBeenCalled(); + done(); + })); + }); + }); + + describe("When play_sounds is set to true", function () { + describe("A notification sound", function () { + + it("is played when the current user is mentioned in a groupchat", + mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => { + + mock.createContacts(_converse, 'current'); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + _converse.play_sounds = true; + spyOn(_converse, 'playSoundNotification'); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + if (!view.el.querySelectorAll('.chat-area').length) { + view.renderChatArea(); + } + let text = 'This message will play a sound because it mentions romeo'; + let message = $msg({ + from: 'lounge@montague.lit/otheruser', + id: '1', + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t(text); + await view.model.queueMessage(message.nodeTree); + await u.waitUntil(() => _converse.playSoundNotification.calls.count()); + expect(_converse.playSoundNotification).toHaveBeenCalled(); + + text = "This message won't play a sound"; + message = $msg({ + from: 'lounge@montague.lit/otheruser', + id: '2', + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t(text); + await view.model.queueMessage(message.nodeTree); + expect(_converse.playSoundNotification, 1); + _converse.play_sounds = false; + + text = "This message won't play a sound because it is sent by romeo"; + message = $msg({ + from: 'lounge@montague.lit/romeo', + id: '3', + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t(text); + await view.model.queueMessage(message.nodeTree); + expect(_converse.playSoundNotification, 1); + _converse.play_sounds = false; + done(); + })); + }); }); }); diff --git a/spec/omemo.js b/spec/omemo.js index ff11946c7..42db3da2f 100644 --- a/spec/omemo.js +++ b/spec/omemo.js @@ -1,44 +1,1119 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const { $iq, $pres, $msg, _, Strophe } = converse.env; - const u = converse.env.utils; +/*global mock */ - async function deviceListFetched (_converse, jid) { - const selector = `iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.devicelist"]`; - const stanza = await u.waitUntil( - () => Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop() - ); - await u.waitUntil(() => _converse.devicelists.get(jid)); - return stanza; - } +const { $iq, $pres, $msg, _, Strophe } = converse.env; +const u = converse.env.utils; - function ownDeviceHasBeenPublished (_converse) { - return _.filter( - Array.from(_converse.connection.IQ_stanzas), - iq => iq.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]') - ).pop(); - } +async function deviceListFetched (_converse, jid) { + const selector = `iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.devicelist"]`; + const stanza = await u.waitUntil( + () => Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop() + ); + await u.waitUntil(() => _converse.devicelists.get(jid)); + return stanza; +} - function bundleHasBeenPublished (_converse) { - const selector = 'publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]'; - return Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop(); - } +function ownDeviceHasBeenPublished (_converse) { + return _.filter( + Array.from(_converse.connection.IQ_stanzas), + iq => iq.querySelector('iq[from="'+_converse.bare_jid+'"] publish[node="eu.siacs.conversations.axolotl.devicelist"]') + ).pop(); +} - function bundleFetched (_converse, jid, device_id) { - return _.filter( - Array.from(_converse.connection.IQ_stanzas), - iq => iq.querySelector(`iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.bundles:${device_id}"]`) - ).pop(); - } +function bundleHasBeenPublished (_converse) { + const selector = 'publish[node="eu.siacs.conversations.axolotl.bundles:123456789"]'; + return Array.from(_converse.connection.IQ_stanzas).filter(iq => iq.querySelector(selector)).pop(); +} - async function initializedOMEMO (_converse) { - await test_utils.waitUntilDiscoConfirmed( +function bundleFetched (_converse, jid, device_id) { + return _.filter( + Array.from(_converse.connection.IQ_stanzas), + iq => iq.querySelector(`iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.bundles:${device_id}"]`) + ).pop(); +} + +async function initializedOMEMO (_converse) { + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, + [{'category': 'pubsub', 'type': 'pep'}], + ['http://jabber.org/protocol/pubsub#publish-options'] + ); + let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); + let stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '482886413b977930064a5888b92134fe'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)) + + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)) + + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await _converse.api.waitUntil('OMEMOInitialized'); +} + + +describe("The OMEMO module", function() { + + it("adds methods for encrypting and decrypting messages via AES GCM", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const message = 'This message will be encrypted' + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const view = await mock.openChatBoxFor(_converse, contact_jid); + const payload = await view.model.encryptMessage(message); + const result = await view.model.decryptMessage(payload); + expect(result).toBe(message); + done(); + })); + + + it("enables encrypted messages to be sent and received", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + let sent_stanza; + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await u.waitUntil(() => initializedOMEMO(_converse)); + await mock.openChatBoxFor(_converse, contact_jid); + let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); + let stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.connection.jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '555'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.omemo_store); + const devicelist = _converse.devicelists.get({'jid': contact_jid}); + await u.waitUntil(() => devicelist.devices.length === 1); + + const view = _converse.chatboxviews.get(contact_jid); + view.model.set('omemo_active', true); + + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = 'This message will be encrypted'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '555')); + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', { + 'xmlns': 'http://jabber.org/protocol/pubsub' + }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"}) + .c('item') + .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up() + .c('signedPreKeySignature').t(btoa('2222')).up() + .c('identityKey').t(btoa('3333')).up() + .c('prekeys') + .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up() + .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up() + .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003')); + _converse.connection._dataRecv(mock.createRequest(stanza)); + iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe')); + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', { + 'xmlns': 'http://jabber.org/protocol/pubsub' + }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"}) + .c('item') + .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up() + .c('signedPreKeySignature').t(btoa('200000')).up() + .c('identityKey').t(btoa('300000')).up() + .c('prekeys') + .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up() + .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up() + .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993')); + + spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza }); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => sent_stanza); + expect(sent_stanza.toLocaleString()).toBe( + ``+ + `This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo`+ + ``+ + ``+ + `
`+ + `YzFwaDNSNzNYNw==`+ + `YzFwaDNSNzNYNw==`+ + `${sent_stanza.nodeTree.querySelector("iv").textContent}`+ + `
`+ + `${sent_stanza.nodeTree.querySelector("payload").textContent}`+ + `
`+ + ``+ + `
`); + + // Test reception of an encrypted message + let obj = await view.model.encryptMessage('This is an encrypted message from the contact') + // XXX: Normally the key will be encrypted via libsignal. + // However, we're mocking libsignal in the tests, so we include + // it as plaintext in the message. + stanza = $msg({ + 'from': contact_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': _converse.connection.getUniqueId() + }).c('body').t('This is a fallback message').up() + .c('encrypted', {'xmlns': Strophe.NS.OMEMO}) + .c('header', {'sid': '555'}) + .c('key', {'rid': _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up() + .c('iv').t(obj.iv) + .up().up() + .c('payload').t(obj.payload); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.model.messages.length).toBe(2); + expect(view.el.querySelectorAll('.chat-msg__body')[1].textContent.trim()) + .toBe('This is an encrypted message from the contact'); + + // #1193 Check for a received message without tag + obj = await view.model.encryptMessage('Another received encrypted message without fallback') + stanza = $msg({ + 'from': contact_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': _converse.connection.getUniqueId() + }).c('encrypted', {'xmlns': Strophe.NS.OMEMO}) + .c('header', {'sid': '555'}) + .c('key', {'rid': _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up() + .c('iv').t(obj.iv) + .up().up() + .c('payload').t(obj.payload); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await new Promise(resolve => view.once('messageInserted', resolve)); + await u.waitUntil(() => view.model.messages.length > 1); + expect(view.model.messages.length).toBe(3); + expect(view.el.querySelectorAll('.chat-msg__body')[2].textContent.trim()) + .toBe('Another received encrypted message without fallback'); + done(); + })); + + it("enables encrypted groupchat messages to be sent and received", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + // MEMO encryption works only in members only conferences + // that are non-anonymous. + const features = [ + 'http://jabber.org/protocol/muc', + 'jabber:iq:register', + 'muc_passwordprotected', + 'muc_hidden', + 'muc_temporary', + 'muc_membersonly', + 'muc_unmoderated', + 'muc_nonanonymous' + ]; + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + await u.waitUntil(() => initializedOMEMO(_converse)); + + const toolbar = view.el.querySelector('.chat-toolbar'); + let toggle = toolbar.querySelector('.toggle-omemo'); + toggle.click(); + expect(view.model.get('omemo_active')).toBe(true); + + // newguy enters the room + const contact_jid = 'newguy@montague.lit'; + let stanza = $pres({ + 'to': 'romeo@montague.lit/orchard', + 'from': 'lounge@montague.lit/newguy' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }).tree(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + // Wait for Converse to fetch newguy's device list + let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + // The server returns his device list + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up() + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.omemo_store); + expect(_converse.devicelists.length).toBe(2); + + await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); + const devicelist = _converse.devicelists.get(contact_jid); + expect(devicelist.devices.length).toBe(1); + expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); + + toggle = toolbar.querySelector('.toggle-omemo'); + expect(view.model.get('omemo_active')).toBe(true); + expect(u.hasClass('fa-unlock', toggle)).toBe(false); + expect(u.hasClass('fa-lock', toggle)).toBe(true); + + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = 'This message will be encrypted'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'), 1000); + console.log("Bundle fetched 4e30f35051b7b8b42abe083742187228"); + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', { + 'xmlns': 'http://jabber.org/protocol/pubsub' + }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:4e30f35051b7b8b42abe083742187228"}) + .c('item') + .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up() + .c('signedPreKeySignature').t(btoa('2222')).up() + .c('identityKey').t(btoa('3333')).up() + .c('prekeys') + .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up() + .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up() + .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003')); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'), 1000); + console.log("Bundle fetched 482886413b977930064a5888b92134fe"); + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', { + 'xmlns': 'http://jabber.org/protocol/pubsub' + }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"}) + .c('item') + .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up() + .c('signedPreKeySignature').t(btoa('200000')).up() + .c('identityKey').t(btoa('300000')).up() + .c('prekeys') + .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up() + .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up() + .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993')); + + spyOn(_converse.connection, 'send'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.connection.send.calls.count(), 1000); + const sent_stanza = _converse.connection.send.calls.all()[0].args[0]; + + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + `This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo`+ + ``+ + `
`+ + `YzFwaDNSNzNYNw==`+ + `YzFwaDNSNzNYNw==`+ + `${sent_stanza.nodeTree.querySelector("iv").textContent}`+ + `
`+ + `${sent_stanza.nodeTree.querySelector("payload").textContent}`+ + `
`+ + ``+ + `
`); + done(); + })); + + it("will create a new device based on a received carbon message", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]); + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await u.waitUntil(() => initializedOMEMO(_converse)); + await mock.openChatBoxFor(_converse, contact_jid); + let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); + const stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.connection.jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '555'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.omemo_store); + const devicelist = _converse.devicelists.get({'jid': contact_jid}); + await u.waitUntil(() => devicelist.devices.length === 1); + + const view = _converse.chatboxviews.get(contact_jid); + view.model.set('omemo_active', true); + + // Test reception of an encrypted carbon message + const obj = await view.model.encryptMessage('This is an encrypted carbon message from another device of mine') + const carbon = u.toStanza(` + + + + + + + + + + + +
+ ${u.arrayBufferToBase64(obj.key_and_tag)} + ${obj.iv} +
+ ${obj.payload} +
+ + +
+
+
+
+ `); + _converse.connection._dataRecv(mock.createRequest(carbon)); + await new Promise(resolve => view.once('messageInserted', resolve)); + expect(view.model.messages.length).toBe(1); + expect(view.el.querySelector('.chat-msg__body').textContent.trim()) + .toBe('This is an encrypted carbon message from another device of mine'); + + expect(devicelist.devices.length).toBe(2); + expect(devicelist.devices.at(0).get('id')).toBe('555'); + expect(devicelist.devices.at(1).get('id')).toBe('988349631'); + expect(devicelist.devices.get('988349631').get('active')).toBe(true); + + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = 'This is an encrypted message from this device'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '988349631')); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + done(); + })); + + it("gracefully handles auth errors when trying to send encrypted groupchat messages", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + // MEMO encryption works only in members only conferences + // that are non-anonymous. + const features = [ + 'http://jabber.org/protocol/muc', + 'jabber:iq:register', + 'muc_passwordprotected', + 'muc_hidden', + 'muc_temporary', + 'muc_membersonly', + 'muc_unmoderated', + 'muc_nonanonymous' + ]; + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + await u.waitUntil(() => initializedOMEMO(_converse)); + + const contact_jid = 'newguy@montague.lit'; + let stanza = $pres({ + 'to': 'romeo@montague.lit/orchard', + 'from': 'lounge@montague.lit/newguy' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }).tree(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + const toolbar = view.el.querySelector('.chat-toolbar'); + const toggle = toolbar.querySelector('.toggle-omemo'); + toggle.click(); + expect(view.model.get('omemo_active')).toBe(true); + expect(view.model.get('omemo_supported')).toBe(true); + + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = 'This message will be encrypted'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 // Enter + }); + let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up() + + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.omemo_store); + expect(_converse.devicelists.length).toBe(2); + + const devicelist = _converse.devicelists.get(contact_jid); + await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); + expect(devicelist.devices.length).toBe(1); + expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); + + iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe')); + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', { + 'xmlns': 'http://jabber.org/protocol/pubsub' + }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"}) + .c('item') + .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up() + .c('signedPreKeySignature').t(btoa('200000')).up() + .c('identityKey').t(btoa('300000')).up() + .c('prekeys') + .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up() + .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up() + .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993')); + iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228')); + + /* + * + * + * + * + * + * + * + * + */ + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', {'xmlns': 'http://jabber.org/protocol/pubsub'}) + .c('items', {'node': "eu.siacs.conversations.axolotl.bundles:4e30f35051b7b8b42abe083742187228"}).up().up() + .c('error', {'code': '401', 'type': 'auth'}) + .c('presence-subscription-required', {'xmlns':"http://jabber.org/protocol/pubsub#errors" }).up() + .c('not-authorized', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + await u.waitUntil(() => document.querySelectorAll('.alert-danger').length, 2000); + const header = document.querySelector('.alert-danger .modal-title'); + expect(header.textContent).toBe("Error"); + expect(u.ancestor(header, '.modal-content').querySelector('.modal-body p').textContent.trim()) + .toBe("Sorry, we're unable to send an encrypted message because newguy@montague.lit requires you "+ + "to be subscribed to their presence in order to see their OMEMO information"); + + expect(view.model.get('omemo_supported')).toBe(false); + expect(view.el.querySelector('.chat-textarea').value).toBe('This message will be encrypted'); + done(); + })); + + it("can receive a PreKeySignalMessage", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + _converse.NUM_PREKEYS = 5; // Restrict to 5, otherwise the resulting stanza is too large to easily test + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + + await u.waitUntil(() => initializedOMEMO(_converse)); + const obj = await _converse.ChatBox.prototype.encryptMessage('This is an encrypted message from the contact'); + // XXX: Normally the key will be encrypted via libsignal. + // However, we're mocking libsignal in the tests, so we include + // it as plaintext in the message. + let stanza = $msg({ + 'from': contact_jid, + 'to': _converse.connection.jid, + 'type': 'chat', + 'id': 'qwerty' + }).c('body').t('This is a fallback message').up() + .c('encrypted', {'xmlns': Strophe.NS.OMEMO}) + .c('header', {'sid': '555'}) + .c('key', { + 'prekey': 'true', + 'rid': _converse.omemo_store.get('device_id') + }).t(u.arrayBufferToBase64(obj.key_and_tag)).up() + .c('iv').t(obj.iv) + .up().up() + .c('payload').t(obj.payload); + + const generateMissingPreKeys = _converse.omemo_store.generateMissingPreKeys; + spyOn(_converse.omemo_store, 'generateMissingPreKeys').and.callFake(() => { + // Since it's difficult to override + // decryptPreKeyWhisperMessage, where a prekey will be + // removed from the store, we do it here, before the + // missing prekeys are generated. + _converse.omemo_store.removePreKey(1); + return generateMissingPreKeys.apply(_converse.omemo_store, arguments); + }); + _converse.connection._dataRecv(mock.createRequest(stanza)); + let iq_stanza = await u.waitUntil(() => _converse.chatboxviews.get(contact_jid)); + iq_stanza = await deviceListFetched(_converse, contact_jid); + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.connection.jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '555'}); + + // XXX: the bundle gets published twice, we want to make sure + // that we wait for the 2nd, so we clear all the already sent + // stanzas. + _converse.connection.IQ_stanzas = []; + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.omemo_store); + + iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``+ + `${btoa("1234")}`+ + `${btoa("11112222333344445555")}`+ + `${btoa("1234")}`+ + ``+ + `${btoa("1234")}`+ + `${btoa("1234")}`+ + `${btoa("1234")}`+ + `${btoa("1234")}`+ + `${btoa("1234")}`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `http://jabber.org/protocol/pubsub#publish-options`+ + ``+ + ``+ + `open`+ + ``+ + ``+ + ``+ + ``+ + ``) + const own_device = _converse.devicelists.get(_converse.bare_jid).devices.get(_converse.omemo_store.get('device_id')); + expect(own_device.get('bundle').prekeys.length).toBe(5); + expect(_converse.omemo_store.generateMissingPreKeys).toHaveBeenCalled(); + done(); + })); + + + it("updates device lists based on PEP messages", + mock.initConverse( + ['rosterGroupsFetched'], {'allow_non_roster_messaging': true}, + async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed( _converse, _converse.bare_jid, [{'category': 'pubsub', 'type': 'pep'}], ['http://jabber.org/protocol/pubsub#publish-options'] ); + + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + + // Wait until own devices are fetched let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + let stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '555'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.omemo_store); + expect(_converse.chatboxes.length).toBe(1); + expect(_converse.devicelists.length).toBe(1); + const devicelist = _converse.devicelists.get(_converse.bare_jid); + expect(devicelist.devices.length).toBe(2); + expect(devicelist.devices.at(0).get('id')).toBe('555'); + expect(devicelist.devices.at(1).get('id')).toBe('123456789'); + iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)); + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)); + + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await _converse.api.waitUntil('OMEMOInitialized'); + + stanza = $msg({ + 'from': contact_jid, + 'to': _converse.bare_jid, + 'type': 'headline', + 'id': 'update_01', + }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) + .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'}) + .c('item') + .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('device', {'id': '1234'}) + .c('device', {'id': '4223'}) + _converse.connection._dataRecv(mock.createRequest(stanza)); + + expect(_converse.devicelists.length).toBe(2); + let devices = _converse.devicelists.get(contact_jid).devices; + expect(devices.length).toBe(2); + expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('1234,4223'); + + stanza = $msg({ + 'from': contact_jid, + 'to': _converse.bare_jid, + 'type': 'headline', + 'id': 'update_02', + }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) + .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'}) + .c('item') + .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('device', {'id': '4223'}) + .c('device', {'id': '4224'}) + _converse.connection._dataRecv(mock.createRequest(stanza)); + + expect(_converse.devicelists.length).toBe(2); + expect(devices.length).toBe(3); + expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('1234,4223,4224'); + expect(devices.get('1234').get('active')).toBe(false); + expect(devices.get('4223').get('active')).toBe(true); + expect(devices.get('4224').get('active')).toBe(true); + + // Check that own devicelist gets updated + stanza = $msg({ + 'from': _converse.bare_jid, + 'to': _converse.bare_jid, + 'type': 'headline', + 'id': 'update_03', + }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) + .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'}) + .c('item') + .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('device', {'id': '123456789'}) + .c('device', {'id': '555'}) + .c('device', {'id': '777'}) + _converse.connection._dataRecv(mock.createRequest(stanza)); + + expect(_converse.devicelists.length).toBe(2); + devices = _converse.devicelists.get(_converse.bare_jid).devices; + expect(devices.length).toBe(3); + expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('123456789,555,777'); + expect(devices.get('123456789').get('active')).toBe(true); + expect(devices.get('555').get('active')).toBe(true); + expect(devices.get('777').get('active')).toBe(true); + + _converse.connection.IQ_stanzas = []; + + // Check that own device gets re-added + stanza = $msg({ + 'from': _converse.bare_jid, + 'to': _converse.bare_jid, + 'type': 'headline', + 'id': 'update_04', + }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) + .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'}) + .c('item') + .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('device', {'id': '444'}) + _converse.connection._dataRecv(mock.createRequest(stanza)); + + iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)); + // Check that our own device is added again, but that removed + // devices are not added. + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `http://jabber.org/protocol/pubsub#publish-options`+ + ``+ + ``+ + `open`+ + ``+ + ``+ + ``+ + ``+ + ``); + expect(_converse.devicelists.length).toBe(2); + devices = _converse.devicelists.get(_converse.bare_jid).devices; + // The device id for this device (123456789) was also generated and added to the list, + // which is why we have 2 devices now. + expect(devices.length).toBe(4); + expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('123456789,444,555,777'); + expect(devices.get('123456789').get('active')).toBe(true); + expect(devices.get('444').get('active')).toBe(true); + expect(devices.get('555').get('active')).toBe(false); + expect(devices.get('777').get('active')).toBe(false); + done(); + })); + + + it("updates device bundles based on PEP messages", + 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'] + ); + + await mock.waitForRoster(_converse, 'current'); + const contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + let stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '555'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await await u.waitUntil(() => _converse.omemo_store); + expect(_converse.devicelists.length).toBe(1); + let devicelist = _converse.devicelists.get(_converse.bare_jid); + expect(devicelist.devices.length).toBe(2); + expect(devicelist.devices.at(0).get('id')).toBe('555'); + expect(devicelist.devices.at(1).get('id')).toBe('123456789'); + iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)); + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)); + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await _converse.api.waitUntil('OMEMOInitialized'); + stanza = $msg({ + 'from': contact_jid, + 'to': _converse.bare_jid, + 'type': 'headline', + 'id': 'update_01', + }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) + .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:555'}) + .c('item') + .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t('1111').up() + .c('signedPreKeySignature').t('2222').up() + .c('identityKey').t('3333').up() + .c('prekeys') + .c('preKeyPublic', {'preKeyId': '1001'}).up() + .c('preKeyPublic', {'preKeyId': '1002'}).up() + .c('preKeyPublic', {'preKeyId': '1003'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + expect(_converse.devicelists.length).toBe(2); + devicelist = _converse.devicelists.get(contact_jid); + expect(devicelist.devices.length).toBe(1); + let device = devicelist.devices.at(0); + expect(device.get('bundle').identity_key).toBe('3333'); + expect(device.get('bundle').signed_prekey.public_key).toBe('1111'); + expect(device.get('bundle').signed_prekey.id).toBe(4223); + expect(device.get('bundle').signed_prekey.signature).toBe('2222'); + expect(device.get('bundle').prekeys.length).toBe(3); + expect(device.get('bundle').prekeys[0].id).toBe(1001); + expect(device.get('bundle').prekeys[1].id).toBe(1002); + expect(device.get('bundle').prekeys[2].id).toBe(1003); + + stanza = $msg({ + 'from': contact_jid, + 'to': _converse.bare_jid, + 'type': 'headline', + 'id': 'update_02', + }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) + .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:555'}) + .c('item') + .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t('5555').up() + .c('signedPreKeySignature').t('6666').up() + .c('identityKey').t('7777').up() + .c('prekeys') + .c('preKeyPublic', {'preKeyId': '2001'}).up() + .c('preKeyPublic', {'preKeyId': '2002'}).up() + .c('preKeyPublic', {'preKeyId': '2003'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + expect(_converse.devicelists.length).toBe(2); + devicelist = _converse.devicelists.get(contact_jid); + expect(devicelist.devices.length).toBe(1); + device = devicelist.devices.at(0); + expect(device.get('bundle').identity_key).toBe('7777'); + expect(device.get('bundle').signed_prekey.public_key).toBe('5555'); + expect(device.get('bundle').signed_prekey.id).toBe(4223); + expect(device.get('bundle').signed_prekey.signature).toBe('6666'); + expect(device.get('bundle').prekeys.length).toBe(3); + expect(device.get('bundle').prekeys[0].id).toBe(2001); + expect(device.get('bundle').prekeys[1].id).toBe(2002); + expect(device.get('bundle').prekeys[2].id).toBe(2003); + + stanza = $msg({ + 'from': _converse.bare_jid, + 'to': _converse.bare_jid, + 'type': 'headline', + 'id': 'update_03', + }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) + .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:123456789'}) + .c('item') + .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('signedPreKeyPublic', {'signedPreKeyId': '9999'}).t('8888').up() + .c('signedPreKeySignature').t('3333').up() + .c('identityKey').t('1111').up() + .c('prekeys') + .c('preKeyPublic', {'preKeyId': '3001'}).up() + .c('preKeyPublic', {'preKeyId': '3002'}).up() + .c('preKeyPublic', {'preKeyId': '3003'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + expect(_converse.devicelists.length).toBe(2); + devicelist = _converse.devicelists.get(_converse.bare_jid); + expect(devicelist.devices.length).toBe(2); + expect(devicelist.devices.at(0).get('id')).toBe('555'); + expect(devicelist.devices.at(1).get('id')).toBe('123456789'); + device = devicelist.devices.at(1); + expect(device.get('bundle').identity_key).toBe('1111'); + expect(device.get('bundle').signed_prekey.public_key).toBe('8888'); + expect(device.get('bundle').signed_prekey.id).toBe(9999); + expect(device.get('bundle').signed_prekey.signature).toBe('3333'); + expect(device.get('bundle').prekeys.length).toBe(3); + expect(device.get('bundle').prekeys[0].id).toBe(3001); + expect(device.get('bundle').prekeys[1].id).toBe(3002); + expect(device.get('bundle').prekeys[2].id).toBe(3003); + done(); + })); + + it("publishes a bundle with which an encrypted session can be created", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, + [{'category': 'pubsub', 'type': 'pep'}], + ['http://jabber.org/protocol/pubsub#publish-options'] + ); + + _converse.NUM_PREKEYS = 2; // Restrict to 2, otherwise the resulting stanza is too large to easily test + + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); + let stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '482886413b977930064a5888b92134fe'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_converse.devicelists.length).toBe(1); + await mock.openChatBoxFor(_converse, contact_jid); + iq_stanza = await ownDeviceHasBeenPublished(_converse); + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``+ + `${btoa("1234")}`+ + `${btoa("11112222333344445555")}`+ + `${btoa("1234")}`+ + ``+ + `${btoa("1234")}`+ + `${btoa("1234")}`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `http://jabber.org/protocol/pubsub#publish-options`+ + ``+ + ``+ + `open`+ + ``+ + ``+ + ``+ + ``+ + ``) + + stanza = $iq({ + 'from': _converse.bare_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await _converse.api.waitUntil('OMEMOInitialized'); + done(); + })); + + + it("adds a toolbar button for starting an encrypted chat session", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, + [{'category': 'pubsub', 'type': 'pep'}], + ['http://jabber.org/protocol/pubsub#publish-options'] + ); + + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + + let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + let stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), @@ -49,1490 +1124,413 @@ window.addEventListener('converse-loaded', () => { .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) .c('device', {'id': '482886413b977930064a5888b92134fe'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)) + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.omemo_store); + expect(_converse.devicelists.length).toBe(1); + let devicelist = _converse.devicelists.get(_converse.bare_jid); + expect(devicelist.devices.length).toBe(2); + expect(devicelist.devices.at(0).get('id')).toBe('482886413b977930064a5888b92134fe'); + expect(devicelist.devices.at(1).get('id')).toBe('123456789'); + // Check that own device was published + iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `http://jabber.org/protocol/pubsub#publish-options`+ + ``+ + ``+ + `open`+ + ``+ + ``+ + ``+ + ``+ + ``); stanza = $iq({ 'from': _converse.bare_jid, 'id': iq_stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)) + _converse.connection._dataRecv(mock.createRequest(stanza)); + + const iq_el = await u.waitUntil(() => bundleHasBeenPublished(_converse)); + expect(iq_el.getAttributeNames().sort().join()).toBe(["from", "type", "xmlns", "id"].sort().join()); + expect(iq_el.querySelector('prekeys').childNodes.length).toBe(100); + + const signed_prekeys = iq_el.querySelectorAll('signedPreKeyPublic'); + expect(signed_prekeys.length).toBe(1); + const signed_prekey = signed_prekeys[0]; + expect(signed_prekey.getAttribute('signedPreKeyId')).toBe('0') + expect(iq_el.querySelectorAll('signedPreKeySignature').length).toBe(1); + expect(iq_el.querySelectorAll('identityKey').length).toBe(1); stanza = $iq({ 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), + 'id': iq_el.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await _converse.api.waitUntil('OMEMOInitialized'); - } - - - describe("The OMEMO module", function() { - - it("adds methods for encrypting and decrypting messages via AES GCM", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const message = 'This message will be encrypted' - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const view = await test_utils.openChatBoxFor(_converse, contact_jid); - const payload = await view.model.encryptMessage(message); - const result = await view.model.decryptMessage(payload); - expect(result).toBe(message); - done(); - })); - - - it("enables encrypted messages to be sent and received", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - let sent_stanza; - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await u.waitUntil(() => initializedOMEMO(_converse)); - await test_utils.openChatBoxFor(_converse, contact_jid); - let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); - let stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.connection.jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '555'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.omemo_store); - const devicelist = _converse.devicelists.get({'jid': contact_jid}); - await u.waitUntil(() => devicelist.devices.length === 1); - - const view = _converse.chatboxviews.get(contact_jid); - view.model.set('omemo_active', true); - - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = 'This message will be encrypted'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '555')); - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', { - 'xmlns': 'http://jabber.org/protocol/pubsub' - }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"}) - .c('item') - .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up() - .c('signedPreKeySignature').t(btoa('2222')).up() - .c('identityKey').t(btoa('3333')).up() - .c('prekeys') - .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up() - .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up() - .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003')); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe')); - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', { - 'xmlns': 'http://jabber.org/protocol/pubsub' - }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"}) - .c('item') - .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up() - .c('signedPreKeySignature').t(btoa('200000')).up() - .c('identityKey').t(btoa('300000')).up() - .c('prekeys') - .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up() - .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up() - .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993')); - - spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza }); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => sent_stanza); - expect(sent_stanza.toLocaleString()).toBe( - ``+ - `This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo`+ - ``+ - ``+ - `
`+ - `YzFwaDNSNzNYNw==`+ - `YzFwaDNSNzNYNw==`+ - `${sent_stanza.nodeTree.querySelector("iv").textContent}`+ - `
`+ - `${sent_stanza.nodeTree.querySelector("payload").textContent}`+ - `
`+ - ``+ - `
`); - - // Test reception of an encrypted message - let obj = await view.model.encryptMessage('This is an encrypted message from the contact') - // XXX: Normally the key will be encrypted via libsignal. - // However, we're mocking libsignal in the tests, so we include - // it as plaintext in the message. - stanza = $msg({ - 'from': contact_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': _converse.connection.getUniqueId() - }).c('body').t('This is a fallback message').up() - .c('encrypted', {'xmlns': Strophe.NS.OMEMO}) - .c('header', {'sid': '555'}) - .c('key', {'rid': _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up() - .c('iv').t(obj.iv) - .up().up() - .c('payload').t(obj.payload); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.model.messages.length).toBe(2); - expect(view.el.querySelectorAll('.chat-msg__body')[1].textContent.trim()) - .toBe('This is an encrypted message from the contact'); - - // #1193 Check for a received message without tag - obj = await view.model.encryptMessage('Another received encrypted message without fallback') - stanza = $msg({ - 'from': contact_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': _converse.connection.getUniqueId() - }).c('encrypted', {'xmlns': Strophe.NS.OMEMO}) - .c('header', {'sid': '555'}) - .c('key', {'rid': _converse.omemo_store.get('device_id')}).t(u.arrayBufferToBase64(obj.key_and_tag)).up() - .c('iv').t(obj.iv) - .up().up() - .c('payload').t(obj.payload); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await new Promise(resolve => view.once('messageInserted', resolve)); - await u.waitUntil(() => view.model.messages.length > 1); - expect(view.model.messages.length).toBe(3); - expect(view.el.querySelectorAll('.chat-msg__body')[2].textContent.trim()) - .toBe('Another received encrypted message without fallback'); - done(); - })); - - it("enables encrypted groupchat messages to be sent and received", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - // MEMO encryption works only in members only conferences - // that are non-anonymous. - const features = [ - 'http://jabber.org/protocol/muc', - 'jabber:iq:register', - 'muc_passwordprotected', - 'muc_hidden', - 'muc_temporary', - 'muc_membersonly', - 'muc_unmoderated', - 'muc_nonanonymous' - ]; - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - await u.waitUntil(() => initializedOMEMO(_converse)); - - const toolbar = view.el.querySelector('.chat-toolbar'); - let toggle = toolbar.querySelector('.toggle-omemo'); - toggle.click(); - expect(view.model.get('omemo_active')).toBe(true); - - // newguy enters the room - const contact_jid = 'newguy@montague.lit'; - let stanza = $pres({ - 'to': 'romeo@montague.lit/orchard', - 'from': 'lounge@montague.lit/newguy' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }).tree(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - // Wait for Converse to fetch newguy's device list - let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - // The server returns his device list - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up() - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.omemo_store); - expect(_converse.devicelists.length).toBe(2); - - await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); - const devicelist = _converse.devicelists.get(contact_jid); - expect(devicelist.devices.length).toBe(1); - expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); - - toggle = toolbar.querySelector('.toggle-omemo'); - expect(view.model.get('omemo_active')).toBe(true); - expect(u.hasClass('fa-unlock', toggle)).toBe(false); - expect(u.hasClass('fa-lock', toggle)).toBe(true); - - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = 'This message will be encrypted'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228'), 1000); - console.log("Bundle fetched 4e30f35051b7b8b42abe083742187228"); - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', { - 'xmlns': 'http://jabber.org/protocol/pubsub' - }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:4e30f35051b7b8b42abe083742187228"}) - .c('item') - .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up() - .c('signedPreKeySignature').t(btoa('2222')).up() - .c('identityKey').t(btoa('3333')).up() - .c('prekeys') - .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up() - .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up() - .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003')); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe'), 1000); - console.log("Bundle fetched 482886413b977930064a5888b92134fe"); - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', { - 'xmlns': 'http://jabber.org/protocol/pubsub' - }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"}) - .c('item') - .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up() - .c('signedPreKeySignature').t(btoa('200000')).up() - .c('identityKey').t(btoa('300000')).up() - .c('prekeys') - .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up() - .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up() - .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993')); - - spyOn(_converse.connection, 'send'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.connection.send.calls.count(), 1000); - const sent_stanza = _converse.connection.send.calls.all()[0].args[0]; - - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - `This is an OMEMO encrypted message which your client doesn’t seem to support. Find more information on https://conversations.im/omemo`+ - ``+ - `
`+ - `YzFwaDNSNzNYNw==`+ - `YzFwaDNSNzNYNw==`+ - `${sent_stanza.nodeTree.querySelector("iv").textContent}`+ - `
`+ - `${sent_stanza.nodeTree.querySelector("payload").textContent}`+ - `
`+ - ``+ - `
`); - done(); - })); - - it("will create a new device based on a received carbon message", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], [Strophe.NS.SID]); - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await u.waitUntil(() => initializedOMEMO(_converse)); - await test_utils.openChatBoxFor(_converse, contact_jid); - let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); - const stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.connection.jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '555'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.omemo_store); - const devicelist = _converse.devicelists.get({'jid': contact_jid}); - await u.waitUntil(() => devicelist.devices.length === 1); - - const view = _converse.chatboxviews.get(contact_jid); - view.model.set('omemo_active', true); - - // Test reception of an encrypted carbon message - const obj = await view.model.encryptMessage('This is an encrypted carbon message from another device of mine') - const carbon = u.toStanza(` - - - - - - - - - - - -
- ${u.arrayBufferToBase64(obj.key_and_tag)} - ${obj.iv} -
- ${obj.payload} -
- - -
-
-
-
- `); - _converse.connection._dataRecv(test_utils.createRequest(carbon)); - await new Promise(resolve => view.once('messageInserted', resolve)); - expect(view.model.messages.length).toBe(1); - expect(view.el.querySelector('.chat-msg__body').textContent.trim()) - .toBe('This is an encrypted carbon message from another device of mine'); - - expect(devicelist.devices.length).toBe(2); - expect(devicelist.devices.at(0).get('id')).toBe('555'); - expect(devicelist.devices.at(1).get('id')).toBe('988349631'); - expect(devicelist.devices.get('988349631').get('active')).toBe(true); - - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = 'This is an encrypted message from this device'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '988349631')); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - done(); - })); - - it("gracefully handles auth errors when trying to send encrypted groupchat messages", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - // MEMO encryption works only in members only conferences - // that are non-anonymous. - const features = [ - 'http://jabber.org/protocol/muc', - 'jabber:iq:register', - 'muc_passwordprotected', - 'muc_hidden', - 'muc_temporary', - 'muc_membersonly', - 'muc_unmoderated', - 'muc_nonanonymous' - ]; - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - await u.waitUntil(() => initializedOMEMO(_converse)); - - const contact_jid = 'newguy@montague.lit'; - let stanza = $pres({ - 'to': 'romeo@montague.lit/orchard', - 'from': 'lounge@montague.lit/newguy' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }).tree(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - const toolbar = view.el.querySelector('.chat-toolbar'); - const toggle = toolbar.querySelector('.toggle-omemo'); - toggle.click(); - expect(view.model.get('omemo_active')).toBe(true); - expect(view.model.get('omemo_supported')).toBe(true); - - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = 'This message will be encrypted'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 // Enter - }); - let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up() - - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.omemo_store); - expect(_converse.devicelists.length).toBe(2); - - const devicelist = _converse.devicelists.get(contact_jid); - await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); - expect(devicelist.devices.length).toBe(1); - expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); - - iq_stanza = await u.waitUntil(() => bundleFetched(_converse, _converse.bare_jid, '482886413b977930064a5888b92134fe')); - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', { - 'xmlns': 'http://jabber.org/protocol/pubsub' - }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:482886413b977930064a5888b92134fe"}) - .c('item') - .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('100000')).up() - .c('signedPreKeySignature').t(btoa('200000')).up() - .c('identityKey').t(btoa('300000')).up() - .c('prekeys') - .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1991')).up() - .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1992')).up() - .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1993')); - iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '4e30f35051b7b8b42abe083742187228')); - - /* - * - * - * - * - * - * - * - * - */ - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', {'xmlns': 'http://jabber.org/protocol/pubsub'}) - .c('items', {'node': "eu.siacs.conversations.axolotl.bundles:4e30f35051b7b8b42abe083742187228"}).up().up() - .c('error', {'code': '401', 'type': 'auth'}) - .c('presence-subscription-required', {'xmlns':"http://jabber.org/protocol/pubsub#errors" }).up() - .c('not-authorized', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - await u.waitUntil(() => document.querySelectorAll('.alert-danger').length, 2000); - const header = document.querySelector('.alert-danger .modal-title'); - expect(header.textContent).toBe("Error"); - expect(u.ancestor(header, '.modal-content').querySelector('.modal-body p').textContent.trim()) - .toBe("Sorry, we're unable to send an encrypted message because newguy@montague.lit requires you "+ - "to be subscribed to their presence in order to see their OMEMO information"); - - expect(view.model.get('omemo_supported')).toBe(false); - expect(view.el.querySelector('.chat-textarea').value).toBe('This message will be encrypted'); - done(); - })); - - it("can receive a PreKeySignalMessage", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - _converse.NUM_PREKEYS = 5; // Restrict to 5, otherwise the resulting stanza is too large to easily test - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - - await u.waitUntil(() => initializedOMEMO(_converse)); - const obj = await _converse.ChatBox.prototype.encryptMessage('This is an encrypted message from the contact'); - // XXX: Normally the key will be encrypted via libsignal. - // However, we're mocking libsignal in the tests, so we include - // it as plaintext in the message. - let stanza = $msg({ - 'from': contact_jid, - 'to': _converse.connection.jid, - 'type': 'chat', - 'id': 'qwerty' - }).c('body').t('This is a fallback message').up() - .c('encrypted', {'xmlns': Strophe.NS.OMEMO}) - .c('header', {'sid': '555'}) - .c('key', { - 'prekey': 'true', - 'rid': _converse.omemo_store.get('device_id') - }).t(u.arrayBufferToBase64(obj.key_and_tag)).up() - .c('iv').t(obj.iv) - .up().up() - .c('payload').t(obj.payload); - - const generateMissingPreKeys = _converse.omemo_store.generateMissingPreKeys; - spyOn(_converse.omemo_store, 'generateMissingPreKeys').and.callFake(() => { - // Since it's difficult to override - // decryptPreKeyWhisperMessage, where a prekey will be - // removed from the store, we do it here, before the - // missing prekeys are generated. - _converse.omemo_store.removePreKey(1); - return generateMissingPreKeys.apply(_converse.omemo_store, arguments); - }); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - let iq_stanza = await u.waitUntil(() => _converse.chatboxviews.get(contact_jid)); - iq_stanza = await deviceListFetched(_converse, contact_jid); - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.connection.jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '555'}); - - // XXX: the bundle gets published twice, we want to make sure - // that we wait for the 2nd, so we clear all the already sent - // stanzas. - _converse.connection.IQ_stanzas = []; - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.omemo_store); - - iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``+ - `${btoa("1234")}`+ - `${btoa("11112222333344445555")}`+ - `${btoa("1234")}`+ - ``+ - `${btoa("1234")}`+ - `${btoa("1234")}`+ - `${btoa("1234")}`+ - `${btoa("1234")}`+ - `${btoa("1234")}`+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - `http://jabber.org/protocol/pubsub#publish-options`+ - ``+ - ``+ - `open`+ - ``+ - ``+ - ``+ - ``+ - ``) - const own_device = _converse.devicelists.get(_converse.bare_jid).devices.get(_converse.omemo_store.get('device_id')); - expect(own_device.get('bundle').prekeys.length).toBe(5); - expect(_converse.omemo_store.generateMissingPreKeys).toHaveBeenCalled(); - done(); - })); - - - it("updates device lists based on PEP messages", - mock.initConverse( - ['rosterGroupsFetched'], {'allow_non_roster_messaging': true}, - async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - ['http://jabber.org/protocol/pubsub#publish-options'] - ); - - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - - // Wait until own devices are fetched - let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - let stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '555'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.omemo_store); - expect(_converse.chatboxes.length).toBe(1); - expect(_converse.devicelists.length).toBe(1); - const devicelist = _converse.devicelists.get(_converse.bare_jid); - expect(devicelist.devices.length).toBe(2); - expect(devicelist.devices.at(0).get('id')).toBe('555'); - expect(devicelist.devices.at(1).get('id')).toBe('123456789'); - iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)); - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)); - - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await _converse.api.waitUntil('OMEMOInitialized'); - - stanza = $msg({ - 'from': contact_jid, - 'to': _converse.bare_jid, - 'type': 'headline', - 'id': 'update_01', - }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) - .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'}) - .c('item') - .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('device', {'id': '1234'}) - .c('device', {'id': '4223'}) - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - expect(_converse.devicelists.length).toBe(2); - let devices = _converse.devicelists.get(contact_jid).devices; - expect(devices.length).toBe(2); - expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('1234,4223'); - - stanza = $msg({ - 'from': contact_jid, - 'to': _converse.bare_jid, - 'type': 'headline', - 'id': 'update_02', - }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) - .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'}) - .c('item') - .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('device', {'id': '4223'}) - .c('device', {'id': '4224'}) - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - expect(_converse.devicelists.length).toBe(2); - expect(devices.length).toBe(3); - expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('1234,4223,4224'); - expect(devices.get('1234').get('active')).toBe(false); - expect(devices.get('4223').get('active')).toBe(true); - expect(devices.get('4224').get('active')).toBe(true); - - // Check that own devicelist gets updated - stanza = $msg({ - 'from': _converse.bare_jid, - 'to': _converse.bare_jid, - 'type': 'headline', - 'id': 'update_03', - }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) - .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'}) - .c('item') - .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('device', {'id': '123456789'}) - .c('device', {'id': '555'}) - .c('device', {'id': '777'}) - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - expect(_converse.devicelists.length).toBe(2); - devices = _converse.devicelists.get(_converse.bare_jid).devices; - expect(devices.length).toBe(3); - expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('123456789,555,777'); - expect(devices.get('123456789').get('active')).toBe(true); - expect(devices.get('555').get('active')).toBe(true); - expect(devices.get('777').get('active')).toBe(true); - - _converse.connection.IQ_stanzas = []; - - // Check that own device gets re-added - stanza = $msg({ - 'from': _converse.bare_jid, - 'to': _converse.bare_jid, - 'type': 'headline', - 'id': 'update_04', - }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) - .c('items', {'node': 'eu.siacs.conversations.axolotl.devicelist'}) - .c('item') - .c('list', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('device', {'id': '444'}) - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)); - // Check that our own device is added again, but that removed - // devices are not added. - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - `http://jabber.org/protocol/pubsub#publish-options`+ - ``+ - ``+ - `open`+ - ``+ - ``+ - ``+ - ``+ - ``); - expect(_converse.devicelists.length).toBe(2); - devices = _converse.devicelists.get(_converse.bare_jid).devices; - // The device id for this device (123456789) was also generated and added to the list, - // which is why we have 2 devices now. - expect(devices.length).toBe(4); - expect(_.map(devices.models, 'attributes.id').sort().join()).toBe('123456789,444,555,777'); - expect(devices.get('123456789').get('active')).toBe(true); - expect(devices.get('444').get('active')).toBe(true); - expect(devices.get('555').get('active')).toBe(false); - expect(devices.get('777').get('active')).toBe(false); - done(); - })); - - - it("updates device bundles based on PEP messages", - 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 test_utils.waitForRoster(_converse, 'current'); - const contact_jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - let stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '555'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await await u.waitUntil(() => _converse.omemo_store); - expect(_converse.devicelists.length).toBe(1); - let devicelist = _converse.devicelists.get(_converse.bare_jid); - expect(devicelist.devices.length).toBe(2); - expect(devicelist.devices.at(0).get('id')).toBe('555'); - expect(devicelist.devices.at(1).get('id')).toBe('123456789'); - iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)); - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)); - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await _converse.api.waitUntil('OMEMOInitialized'); - stanza = $msg({ - 'from': contact_jid, - 'to': _converse.bare_jid, - 'type': 'headline', - 'id': 'update_01', - }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) - .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:555'}) - .c('item') - .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t('1111').up() - .c('signedPreKeySignature').t('2222').up() - .c('identityKey').t('3333').up() - .c('prekeys') - .c('preKeyPublic', {'preKeyId': '1001'}).up() - .c('preKeyPublic', {'preKeyId': '1002'}).up() - .c('preKeyPublic', {'preKeyId': '1003'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - expect(_converse.devicelists.length).toBe(2); - devicelist = _converse.devicelists.get(contact_jid); - expect(devicelist.devices.length).toBe(1); - let device = devicelist.devices.at(0); - expect(device.get('bundle').identity_key).toBe('3333'); - expect(device.get('bundle').signed_prekey.public_key).toBe('1111'); - expect(device.get('bundle').signed_prekey.id).toBe(4223); - expect(device.get('bundle').signed_prekey.signature).toBe('2222'); - expect(device.get('bundle').prekeys.length).toBe(3); - expect(device.get('bundle').prekeys[0].id).toBe(1001); - expect(device.get('bundle').prekeys[1].id).toBe(1002); - expect(device.get('bundle').prekeys[2].id).toBe(1003); - - stanza = $msg({ - 'from': contact_jid, - 'to': _converse.bare_jid, - 'type': 'headline', - 'id': 'update_02', - }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) - .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:555'}) - .c('item') - .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t('5555').up() - .c('signedPreKeySignature').t('6666').up() - .c('identityKey').t('7777').up() - .c('prekeys') - .c('preKeyPublic', {'preKeyId': '2001'}).up() - .c('preKeyPublic', {'preKeyId': '2002'}).up() - .c('preKeyPublic', {'preKeyId': '2003'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - expect(_converse.devicelists.length).toBe(2); - devicelist = _converse.devicelists.get(contact_jid); - expect(devicelist.devices.length).toBe(1); - device = devicelist.devices.at(0); - expect(device.get('bundle').identity_key).toBe('7777'); - expect(device.get('bundle').signed_prekey.public_key).toBe('5555'); - expect(device.get('bundle').signed_prekey.id).toBe(4223); - expect(device.get('bundle').signed_prekey.signature).toBe('6666'); - expect(device.get('bundle').prekeys.length).toBe(3); - expect(device.get('bundle').prekeys[0].id).toBe(2001); - expect(device.get('bundle').prekeys[1].id).toBe(2002); - expect(device.get('bundle').prekeys[2].id).toBe(2003); - - stanza = $msg({ - 'from': _converse.bare_jid, - 'to': _converse.bare_jid, - 'type': 'headline', - 'id': 'update_03', - }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'}) - .c('items', {'node': 'eu.siacs.conversations.axolotl.bundles:123456789'}) - .c('item') - .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('signedPreKeyPublic', {'signedPreKeyId': '9999'}).t('8888').up() - .c('signedPreKeySignature').t('3333').up() - .c('identityKey').t('1111').up() - .c('prekeys') - .c('preKeyPublic', {'preKeyId': '3001'}).up() - .c('preKeyPublic', {'preKeyId': '3002'}).up() - .c('preKeyPublic', {'preKeyId': '3003'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - expect(_converse.devicelists.length).toBe(2); - devicelist = _converse.devicelists.get(_converse.bare_jid); - expect(devicelist.devices.length).toBe(2); - expect(devicelist.devices.at(0).get('id')).toBe('555'); - expect(devicelist.devices.at(1).get('id')).toBe('123456789'); - device = devicelist.devices.at(1); - expect(device.get('bundle').identity_key).toBe('1111'); - expect(device.get('bundle').signed_prekey.public_key).toBe('8888'); - expect(device.get('bundle').signed_prekey.id).toBe(9999); - expect(device.get('bundle').signed_prekey.signature).toBe('3333'); - expect(device.get('bundle').prekeys.length).toBe(3); - expect(device.get('bundle').prekeys[0].id).toBe(3001); - expect(device.get('bundle').prekeys[1].id).toBe(3002); - expect(device.get('bundle').prekeys[2].id).toBe(3003); - done(); - })); - - it("publishes a bundle with which an encrypted session can be created", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - ['http://jabber.org/protocol/pubsub#publish-options'] - ); - - _converse.NUM_PREKEYS = 2; // Restrict to 2, otherwise the resulting stanza is too large to easily test - - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); - let stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '482886413b977930064a5888b92134fe'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_converse.devicelists.length).toBe(1); - await test_utils.openChatBoxFor(_converse, contact_jid); - iq_stanza = await ownDeviceHasBeenPublished(_converse); - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - iq_stanza = await u.waitUntil(() => bundleHasBeenPublished(_converse)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``+ - `${btoa("1234")}`+ - `${btoa("11112222333344445555")}`+ - `${btoa("1234")}`+ - ``+ - `${btoa("1234")}`+ - `${btoa("1234")}`+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - `http://jabber.org/protocol/pubsub#publish-options`+ - ``+ - ``+ - `open`+ - ``+ - ``+ - ``+ - ``+ - ``) - - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await _converse.api.waitUntil('OMEMOInitialized'); - done(); - })); - - - it("adds a toolbar button for starting an encrypted chat session", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - ['http://jabber.org/protocol/pubsub#publish-options'] - ); - - await test_utils.waitForRoster(_converse, 'current', 1); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - - let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, _converse.bare_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - let stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '482886413b977930064a5888b92134fe'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.omemo_store); - expect(_converse.devicelists.length).toBe(1); - let devicelist = _converse.devicelists.get(_converse.bare_jid); - expect(devicelist.devices.length).toBe(2); - expect(devicelist.devices.at(0).get('id')).toBe('482886413b977930064a5888b92134fe'); - expect(devicelist.devices.at(1).get('id')).toBe('123456789'); - // Check that own device was published - iq_stanza = await u.waitUntil(() => ownDeviceHasBeenPublished(_converse)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - `http://jabber.org/protocol/pubsub#publish-options`+ - ``+ - ``+ - `open`+ - ``+ - ``+ - ``+ - ``+ - ``); - - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - const iq_el = await u.waitUntil(() => bundleHasBeenPublished(_converse)); - expect(iq_el.getAttributeNames().sort().join()).toBe(["from", "type", "xmlns", "id"].sort().join()); - expect(iq_el.querySelector('prekeys').childNodes.length).toBe(100); - - const signed_prekeys = iq_el.querySelectorAll('signedPreKeyPublic'); - expect(signed_prekeys.length).toBe(1); - const signed_prekey = signed_prekeys[0]; - expect(signed_prekey.getAttribute('signedPreKeyId')).toBe('0') - expect(iq_el.querySelectorAll('signedPreKeySignature').length).toBe(1); - expect(iq_el.querySelectorAll('identityKey').length).toBe(1); - - stanza = $iq({ - 'from': _converse.bare_jid, - 'id': iq_el.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await _converse.api.waitUntil('OMEMOInitialized', 1000); - await test_utils.openChatBoxFor(_converse, contact_jid); - iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '368866411b877c30064a5f62b917cffe'}).up() - .c('device', {'id': '3300659945416e274474e469a1f0154c'}).up() - .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up() - .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - devicelist = _converse.devicelists.get(contact_jid); - await u.waitUntil(() => devicelist.devices.length); - expect(_converse.devicelists.length).toBe(2); - devicelist = _converse.devicelists.get(contact_jid); - expect(devicelist.devices.length).toBe(4); - expect(devicelist.devices.at(0).get('id')).toBe('368866411b877c30064a5f62b917cffe'); - expect(devicelist.devices.at(1).get('id')).toBe('3300659945416e274474e469a1f0154c'); - expect(devicelist.devices.at(2).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); - expect(devicelist.devices.at(3).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901'); - await u.waitUntil(() => _converse.chatboxviews.get(contact_jid).el.querySelector('.chat-toolbar')); - const view = _converse.chatboxviews.get(contact_jid); - const toolbar = view.el.querySelector('.chat-toolbar'); - expect(view.model.get('omemo_active')).toBe(undefined); - let toggle = toolbar.querySelector('.toggle-omemo'); - expect(toggle === null).toBe(false); - expect(u.hasClass('fa-unlock', toggle)).toBe(true); - expect(u.hasClass('fa-lock', toggle)).toBe(false); - - spyOn(view, 'toggleOMEMO').and.callThrough(); - view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - toolbar.querySelector('.toggle-omemo').click(); - expect(view.toggleOMEMO).toHaveBeenCalled(); - expect(view.model.get('omemo_active')).toBe(true); - - await u.waitUntil(() => u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo'))); - toggle = toolbar.querySelector('.toggle-omemo'); - expect(u.hasClass('fa-unlock', toggle)).toBe(false); - expect(u.hasClass('fa-lock', toggle)).toBe(true); - - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = 'This message will be sent encrypted'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - - view.model.save({'omemo_supported': false}); - toggle = toolbar.querySelector('.toggle-omemo'); - expect(u.hasClass('fa-lock', toggle)).toBe(false); - expect(u.hasClass('fa-unlock', toggle)).toBe(true); - expect(u.hasClass('disabled', toggle)).toBe(true); - - view.model.save({'omemo_supported': true}); - toggle = toolbar.querySelector('.toggle-omemo'); - expect(u.hasClass('fa-lock', toggle)).toBe(false); - expect(u.hasClass('fa-unlock', toggle)).toBe(true); - expect(u.hasClass('disabled', toggle)).toBe(false); - done(); - })); - - it("adds a toolbar button for starting an encrypted groupchat session", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'view_mode': 'fullscreen'}, - async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - ['http://jabber.org/protocol/pubsub#publish-options'] - ); - - // MEMO encryption works only in members-only conferences that are non-anonymous. - const features = [ - 'http://jabber.org/protocol/muc', - 'jabber:iq:register', - 'muc_passwordprotected', - 'muc_hidden', - 'muc_temporary', - 'muc_membersonly', - 'muc_unmoderated', - 'muc_nonanonymous' - ]; - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - await u.waitUntil(() => initializedOMEMO(_converse)); - - const toolbar = view.el.querySelector('.chat-toolbar'); - let toggle = toolbar.querySelector('.toggle-omemo'); - expect(view.model.get('omemo_active')).toBe(undefined); - expect(toggle === null).toBe(false); - expect(u.hasClass('fa-unlock', toggle)).toBe(true); - expect(u.hasClass('fa-lock', toggle)).toBe(false); - expect(u.hasClass('disabled', toggle)).toBe(false); - expect(view.model.get('omemo_supported')).toBe(true); - - toggle.click(); - toggle = toolbar.querySelector('.toggle-omemo'); - expect(view.model.get('omemo_active')).toBe(true); - expect(u.hasClass('fa-unlock', toggle)).toBe(false); - expect(u.hasClass('fa-lock', toggle)).toBe(true); - expect(u.hasClass('disabled', toggle)).toBe(false); - expect(view.model.get('omemo_supported')).toBe(true); - - let contact_jid = 'newguy@montague.lit'; - let stanza = $pres({ - to: 'romeo@montague.lit/orchard', - from: 'lounge@montague.lit/newguy' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }).tree(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up() - .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => _converse.omemo_store); - expect(_converse.devicelists.length).toBe(2); - - await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); - const devicelist = _converse.devicelists.get(contact_jid); - expect(devicelist.devices.length).toBe(2); - expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); - expect(devicelist.devices.at(1).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901'); - - expect(view.model.get('omemo_active')).toBe(true); - toggle = toolbar.querySelector('.toggle-omemo'); - expect(toggle === null).toBe(false); - expect(u.hasClass('fa-unlock', toggle)).toBe(false); - expect(u.hasClass('fa-lock', toggle)).toBe(true); - expect(u.hasClass('disabled', toggle)).toBe(false); - expect(view.model.get('omemo_supported')).toBe(true); - - // Test that the button gets disabled when the room becomes - // anonymous or semi-anonymous - view.model.features.save({'nonanonymous': false, 'semianonymous': true}); - await u.waitUntil(() => !view.model.get('omemo_supported')); - toggle = toolbar.querySelector('.toggle-omemo'); - expect(toggle === null).toBe(true); - expect(view.model.get('omemo_supported')).toBe(false); - - view.model.features.save({'nonanonymous': true, 'semianonymous': false}); - await u.waitUntil(() => view.model.get('omemo_supported')); - toggle = toolbar.querySelector('.toggle-omemo'); - expect(toggle === null).toBe(false); - expect(u.hasClass('fa-unlock', toggle)).toBe(true); - expect(u.hasClass('fa-lock', toggle)).toBe(false); - expect(u.hasClass('disabled', toggle)).toBe(false); - - // Test that the button gets disabled when the room becomes open - view.model.features.save({'membersonly': false, 'open': true}); - await u.waitUntil(() => !view.model.get('omemo_supported')); - toggle = toolbar.querySelector('.toggle-omemo'); - expect(toggle === null).toBe(true); - - view.model.features.save({'membersonly': true, 'open': false}); - await u.waitUntil(() => view.model.get('omemo_supported')); - toggle = toolbar.querySelector('.toggle-omemo'); - expect(toggle === null).toBe(false); - expect(u.hasClass('fa-unlock', toggle)).toBe(true); - expect(u.hasClass('fa-lock', toggle)).toBe(false); - expect(u.hasClass('disabled', toggle)).toBe(false); - expect(view.model.get('omemo_supported')).toBe(true); - expect(view.model.get('omemo_active')).toBe(false); - - toggle.click(); - expect(view.model.get('omemo_active')).toBe(true); - - // Someone enters the room who doesn't have OMEMO support, while we - // have OMEMO activated... - contact_jid = 'oldguy@montague.lit'; - stanza = $pres({ - to: 'romeo@montague.lit/orchard', - from: 'lounge@montague.lit/oldguy' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': `${contact_jid}/_converse.js-290929788`, - 'role': 'participant' - }).tree(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'error' - }).c('error', {'type': 'cancel'}) - .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - await u.waitUntil(() => !view.model.get('omemo_supported')); - - expect(view.el.querySelector('.chat-error').textContent.trim()).toBe( - "oldguy doesn't appear to have a client that supports OMEMO. "+ - "Encrypted chat will no longer be possible in this grouchat." - ); - - toggle = toolbar.querySelector('.toggle-omemo'); - expect(toggle === null).toBe(false); - expect(u.hasClass('fa-unlock', toggle)).toBe(true); - expect(u.hasClass('fa-lock', toggle)).toBe(false); - expect(u.hasClass('disabled', toggle)).toBe(true); - - expect( _converse.chatboxviews.el.querySelector('.modal-body p')).toBe(null); - toggle.click(); - const msg = _converse.chatboxviews.el.querySelector('.modal-body p'); - expect(msg.textContent).toBe( - 'Cannot use end-to-end encryption in this groupchat, '+ - 'either the groupchat has some anonymity or not all participants support OMEMO.'); - done(); - })); - - - it("shows OMEMO device fingerprints in the user details modal", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, - [{'category': 'pubsub', 'type': 'pep'}], - ['http://jabber.org/protocol/pubsub#publish-options'] - ); - - 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) - // We simply emit, to avoid doing all the setup work - _converse.api.trigger('OMEMOInitialized'); - - const view = _converse.chatboxviews.get(contact_jid); - const show_modal_button = view.el.querySelector('.show-user-details-modal'); - show_modal_button.click(); - const modal = view.user_details_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``); - let stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) - .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) - .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute - .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) - .c('device', {'id': '555'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => u.isVisible(modal.el), 1000); - iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '555')); - expect(Strophe.serialize(iq_stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - stanza = $iq({ - 'from': contact_jid, - 'id': iq_stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('pubsub', { - 'xmlns': 'http://jabber.org/protocol/pubsub' - }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"}) - .c('item') - .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) - .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up() - .c('signedPreKeySignature').t(btoa('2222')).up() - .c('identityKey').t('BQmHEOHjsYm3w5M8VqxAtqJmLCi7CaxxsdZz6G0YpuMI').up() - .c('prekeys') - .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up() - .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up() - .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003')); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - await u.waitUntil(() => modal.el.querySelectorAll('.fingerprints .fingerprint').length); - expect(modal.el.querySelectorAll('.fingerprints .fingerprint').length).toBe(1); - const el = modal.el.querySelector('.fingerprints .fingerprint'); - expect(el.textContent.trim()).toBe( - u.formatFingerprint(u.arrayBufferToHex(u.base64ToArrayBuffer('BQmHEOHjsYm3w5M8VqxAtqJmLCi7CaxxsdZz6G0YpuMI'))) - ); - expect(modal.el.querySelectorAll('input[type="radio"]').length).toBe(2); - - const devicelist = _converse.devicelists.get(contact_jid); - expect(devicelist.devices.get('555').get('trusted')).toBe(0); - - let trusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="1"]'); - expect(trusted_radio.checked).toBe(true); - - let untrusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="-1"]'); - expect(untrusted_radio.checked).toBe(false); - - // Test that the device can be set to untrusted - untrusted_radio.click(); - trusted_radio = document.querySelector('input[type="radio"][name="555"][value="1"]'); - expect(trusted_radio.hasAttribute('checked')).toBe(false); - expect(devicelist.devices.get('555').get('trusted')).toBe(-1); - - untrusted_radio = document.querySelector('input[type="radio"][name="555"][value="-1"]'); - expect(untrusted_radio.hasAttribute('checked')).toBe(true); - - trusted_radio.click(); - expect(devicelist.devices.get('555').get('trusted')).toBe(1); - done(); - })); - }); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await _converse.api.waitUntil('OMEMOInitialized', 1000); + await mock.openChatBoxFor(_converse, contact_jid); + iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '368866411b877c30064a5f62b917cffe'}).up() + .c('device', {'id': '3300659945416e274474e469a1f0154c'}).up() + .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up() + .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + devicelist = _converse.devicelists.get(contact_jid); + await u.waitUntil(() => devicelist.devices.length); + expect(_converse.devicelists.length).toBe(2); + devicelist = _converse.devicelists.get(contact_jid); + expect(devicelist.devices.length).toBe(4); + expect(devicelist.devices.at(0).get('id')).toBe('368866411b877c30064a5f62b917cffe'); + expect(devicelist.devices.at(1).get('id')).toBe('3300659945416e274474e469a1f0154c'); + expect(devicelist.devices.at(2).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); + expect(devicelist.devices.at(3).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901'); + await u.waitUntil(() => _converse.chatboxviews.get(contact_jid).el.querySelector('.chat-toolbar')); + const view = _converse.chatboxviews.get(contact_jid); + const toolbar = view.el.querySelector('.chat-toolbar'); + expect(view.model.get('omemo_active')).toBe(undefined); + let toggle = toolbar.querySelector('.toggle-omemo'); + expect(toggle === null).toBe(false); + expect(u.hasClass('fa-unlock', toggle)).toBe(true); + expect(u.hasClass('fa-lock', toggle)).toBe(false); + + spyOn(view, 'toggleOMEMO').and.callThrough(); + view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + toolbar.querySelector('.toggle-omemo').click(); + expect(view.toggleOMEMO).toHaveBeenCalled(); + expect(view.model.get('omemo_active')).toBe(true); + + await u.waitUntil(() => u.hasClass('fa-lock', toolbar.querySelector('.toggle-omemo'))); + toggle = toolbar.querySelector('.toggle-omemo'); + expect(u.hasClass('fa-unlock', toggle)).toBe(false); + expect(u.hasClass('fa-lock', toggle)).toBe(true); + + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = 'This message will be sent encrypted'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + + view.model.save({'omemo_supported': false}); + toggle = toolbar.querySelector('.toggle-omemo'); + expect(u.hasClass('fa-lock', toggle)).toBe(false); + expect(u.hasClass('fa-unlock', toggle)).toBe(true); + expect(u.hasClass('disabled', toggle)).toBe(true); + + view.model.save({'omemo_supported': true}); + toggle = toolbar.querySelector('.toggle-omemo'); + expect(u.hasClass('fa-lock', toggle)).toBe(false); + expect(u.hasClass('fa-unlock', toggle)).toBe(true); + expect(u.hasClass('disabled', toggle)).toBe(false); + done(); + })); + + it("adds a toolbar button for starting an encrypted groupchat session", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'view_mode': 'fullscreen'}, + async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, + [{'category': 'pubsub', 'type': 'pep'}], + ['http://jabber.org/protocol/pubsub#publish-options'] + ); + + // MEMO encryption works only in members-only conferences that are non-anonymous. + const features = [ + 'http://jabber.org/protocol/muc', + 'jabber:iq:register', + 'muc_passwordprotected', + 'muc_hidden', + 'muc_temporary', + 'muc_membersonly', + 'muc_unmoderated', + 'muc_nonanonymous' + ]; + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + await u.waitUntil(() => initializedOMEMO(_converse)); + + const toolbar = view.el.querySelector('.chat-toolbar'); + let toggle = toolbar.querySelector('.toggle-omemo'); + expect(view.model.get('omemo_active')).toBe(undefined); + expect(toggle === null).toBe(false); + expect(u.hasClass('fa-unlock', toggle)).toBe(true); + expect(u.hasClass('fa-lock', toggle)).toBe(false); + expect(u.hasClass('disabled', toggle)).toBe(false); + expect(view.model.get('omemo_supported')).toBe(true); + + toggle.click(); + toggle = toolbar.querySelector('.toggle-omemo'); + expect(view.model.get('omemo_active')).toBe(true); + expect(u.hasClass('fa-unlock', toggle)).toBe(false); + expect(u.hasClass('fa-lock', toggle)).toBe(true); + expect(u.hasClass('disabled', toggle)).toBe(false); + expect(view.model.get('omemo_supported')).toBe(true); + + let contact_jid = 'newguy@montague.lit'; + let stanza = $pres({ + to: 'romeo@montague.lit/orchard', + from: 'lounge@montague.lit/newguy' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }).tree(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '4e30f35051b7b8b42abe083742187228'}).up() + .c('device', {'id': 'ae890ac52d0df67ed7cfdf51b644e901'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => _converse.omemo_store); + expect(_converse.devicelists.length).toBe(2); + + await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); + const devicelist = _converse.devicelists.get(contact_jid); + expect(devicelist.devices.length).toBe(2); + expect(devicelist.devices.at(0).get('id')).toBe('4e30f35051b7b8b42abe083742187228'); + expect(devicelist.devices.at(1).get('id')).toBe('ae890ac52d0df67ed7cfdf51b644e901'); + + expect(view.model.get('omemo_active')).toBe(true); + toggle = toolbar.querySelector('.toggle-omemo'); + expect(toggle === null).toBe(false); + expect(u.hasClass('fa-unlock', toggle)).toBe(false); + expect(u.hasClass('fa-lock', toggle)).toBe(true); + expect(u.hasClass('disabled', toggle)).toBe(false); + expect(view.model.get('omemo_supported')).toBe(true); + + // Test that the button gets disabled when the room becomes + // anonymous or semi-anonymous + view.model.features.save({'nonanonymous': false, 'semianonymous': true}); + await u.waitUntil(() => !view.model.get('omemo_supported')); + toggle = toolbar.querySelector('.toggle-omemo'); + expect(toggle === null).toBe(true); + expect(view.model.get('omemo_supported')).toBe(false); + + view.model.features.save({'nonanonymous': true, 'semianonymous': false}); + await u.waitUntil(() => view.model.get('omemo_supported')); + toggle = toolbar.querySelector('.toggle-omemo'); + expect(toggle === null).toBe(false); + expect(u.hasClass('fa-unlock', toggle)).toBe(true); + expect(u.hasClass('fa-lock', toggle)).toBe(false); + expect(u.hasClass('disabled', toggle)).toBe(false); + + // Test that the button gets disabled when the room becomes open + view.model.features.save({'membersonly': false, 'open': true}); + await u.waitUntil(() => !view.model.get('omemo_supported')); + toggle = toolbar.querySelector('.toggle-omemo'); + expect(toggle === null).toBe(true); + + view.model.features.save({'membersonly': true, 'open': false}); + await u.waitUntil(() => view.model.get('omemo_supported')); + toggle = toolbar.querySelector('.toggle-omemo'); + expect(toggle === null).toBe(false); + expect(u.hasClass('fa-unlock', toggle)).toBe(true); + expect(u.hasClass('fa-lock', toggle)).toBe(false); + expect(u.hasClass('disabled', toggle)).toBe(false); + expect(view.model.get('omemo_supported')).toBe(true); + expect(view.model.get('omemo_active')).toBe(false); + + toggle.click(); + expect(view.model.get('omemo_active')).toBe(true); + + // Someone enters the room who doesn't have OMEMO support, while we + // have OMEMO activated... + contact_jid = 'oldguy@montague.lit'; + stanza = $pres({ + to: 'romeo@montague.lit/orchard', + from: 'lounge@montague.lit/oldguy' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': `${contact_jid}/_converse.js-290929788`, + 'role': 'participant' + }).tree(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'error' + }).c('error', {'type': 'cancel'}) + .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + await u.waitUntil(() => !view.model.get('omemo_supported')); + + expect(view.el.querySelector('.chat-error').textContent.trim()).toBe( + "oldguy doesn't appear to have a client that supports OMEMO. "+ + "Encrypted chat will no longer be possible in this grouchat." + ); + + toggle = toolbar.querySelector('.toggle-omemo'); + expect(toggle === null).toBe(false); + expect(u.hasClass('fa-unlock', toggle)).toBe(true); + expect(u.hasClass('fa-lock', toggle)).toBe(false); + expect(u.hasClass('disabled', toggle)).toBe(true); + + expect( _converse.chatboxviews.el.querySelector('.modal-body p')).toBe(null); + toggle.click(); + const msg = _converse.chatboxviews.el.querySelector('.modal-body p'); + expect(msg.textContent).toBe( + 'Cannot use end-to-end encryption in this groupchat, '+ + 'either the groupchat has some anonymity or not all participants support OMEMO.'); + done(); + })); + + + it("shows OMEMO device fingerprints in the user details modal", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, + [{'category': 'pubsub', 'type': 'pep'}], + ['http://jabber.org/protocol/pubsub#publish-options'] + ); + + await mock.waitForRoster(_converse, 'current', 1); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid) + // We simply emit, to avoid doing all the setup work + _converse.api.trigger('OMEMOInitialized'); + + const view = _converse.chatboxviews.get(contact_jid); + const show_modal_button = view.el.querySelector('.show-user-details-modal'); + show_modal_button.click(); + const modal = view.user_details_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + let iq_stanza = await u.waitUntil(() => deviceListFetched(_converse, contact_jid)); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``); + let stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', {'xmlns': "http://jabber.org/protocol/pubsub"}) + .c('items', {'node': "eu.siacs.conversations.axolotl.devicelist"}) + .c('item', {'xmlns': "http://jabber.org/protocol/pubsub"}) // TODO: must have an id attribute + .c('list', {'xmlns': "eu.siacs.conversations.axolotl"}) + .c('device', {'id': '555'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => u.isVisible(modal.el), 1000); + iq_stanza = await u.waitUntil(() => bundleFetched(_converse, contact_jid, '555')); + expect(Strophe.serialize(iq_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + stanza = $iq({ + 'from': contact_jid, + 'id': iq_stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('pubsub', { + 'xmlns': 'http://jabber.org/protocol/pubsub' + }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"}) + .c('item') + .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'}) + .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up() + .c('signedPreKeySignature').t(btoa('2222')).up() + .c('identityKey').t('BQmHEOHjsYm3w5M8VqxAtqJmLCi7CaxxsdZz6G0YpuMI').up() + .c('prekeys') + .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up() + .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up() + .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003')); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + await u.waitUntil(() => modal.el.querySelectorAll('.fingerprints .fingerprint').length); + expect(modal.el.querySelectorAll('.fingerprints .fingerprint').length).toBe(1); + const el = modal.el.querySelector('.fingerprints .fingerprint'); + expect(el.textContent.trim()).toBe( + u.formatFingerprint(u.arrayBufferToHex(u.base64ToArrayBuffer('BQmHEOHjsYm3w5M8VqxAtqJmLCi7CaxxsdZz6G0YpuMI'))) + ); + expect(modal.el.querySelectorAll('input[type="radio"]').length).toBe(2); + + const devicelist = _converse.devicelists.get(contact_jid); + expect(devicelist.devices.get('555').get('trusted')).toBe(0); + + let trusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="1"]'); + expect(trusted_radio.checked).toBe(true); + + let untrusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="-1"]'); + expect(untrusted_radio.checked).toBe(false); + + // Test that the device can be set to untrusted + untrusted_radio.click(); + trusted_radio = document.querySelector('input[type="radio"][name="555"][value="1"]'); + expect(trusted_radio.hasAttribute('checked')).toBe(false); + expect(devicelist.devices.get('555').get('trusted')).toBe(-1); + + untrusted_radio = document.querySelector('input[type="radio"][name="555"][value="-1"]'); + expect(untrusted_radio.hasAttribute('checked')).toBe(true); + + trusted_radio.click(); + expect(devicelist.devices.get('555').get('trusted')).toBe(1); + done(); + })); }); diff --git a/spec/ping.js b/spec/ping.js index 6601caeb6..5bd5ae10f 100644 --- a/spec/ping.js +++ b/spec/ping.js @@ -1,36 +1,34 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const Strophe = converse.env.Strophe; - const u = converse.env.utils; +/*global mock */ + +const Strophe = converse.env.Strophe; +const u = converse.env.utils; - describe("XMPP Ping", function () { +describe("XMPP Ping", function () { - describe("An IQ stanza", function () { + describe("An IQ stanza", function () { - it("is returned when converse.js gets pinged", mock.initConverse((done, _converse) => { - const ping = u.toStanza(` - - - `); - _converse.connection._dataRecv(test_utils.createRequest(ping)); - const sent_stanza = _converse.connection.IQ_stanzas.pop(); - expect(Strophe.serialize(sent_stanza)).toBe( - ``); - done(); - })); + it("is returned when converse.js gets pinged", mock.initConverse((done, _converse) => { + const ping = u.toStanza(` + + + `); + _converse.connection._dataRecv(mock.createRequest(ping)); + const sent_stanza = _converse.connection.IQ_stanzas.pop(); + expect(Strophe.serialize(sent_stanza)).toBe( + ``); + done(); + })); - it("is sent out when converse.js pings a server", mock.initConverse((done, _converse) => { - _converse.api.ping(); - const sent_stanza = _converse.connection.IQ_stanzas.pop(); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ``+ - ``); - done(); - })); - }); + it("is sent out when converse.js pings a server", mock.initConverse((done, _converse) => { + _converse.api.ping(); + const sent_stanza = _converse.connection.IQ_stanzas.pop(); + expect(Strophe.serialize(sent_stanza)).toBe( + ``+ + ``+ + ``); + done(); + })); }); }); diff --git a/spec/presence.js b/spec/presence.js index fffff89fd..99e7723ea 100644 --- a/spec/presence.js +++ b/spec/presence.js @@ -1,290 +1,287 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const Strophe = converse.env.Strophe; - const u = converse.env.utils; - // See: https://xmpp.org/rfcs/rfc3921.html +/*global mock */ +// See: https://xmpp.org/rfcs/rfc3921.html - describe("A sent presence stanza", function () { +describe("A sent presence stanza", function () { - it("includes a entity capabilities node", - mock.initConverse( - ['rosterGroupsFetched'], {}, - (done, _converse) => { + it("includes a entity capabilities node", + mock.initConverse( + ['rosterGroupsFetched'], {}, + (done, _converse) => { - _converse.api.disco.own.identities.clear(); - _converse.api.disco.own.features.clear(); + _converse.api.disco.own.identities.clear(); + _converse.api.disco.own.features.clear(); - _converse.api.disco.own.identities.add("client", "pc", "Exodus 0.9.1"); - _converse.api.disco.own.features.add("http://jabber.org/protocol/caps"); - _converse.api.disco.own.features.add("http://jabber.org/protocol/disco#info"); - _converse.api.disco.own.features.add("http://jabber.org/protocol/disco#items"); - _converse.api.disco.own.features.add("http://jabber.org/protocol/muc"); + _converse.api.disco.own.identities.add("client", "pc", "Exodus 0.9.1"); + _converse.api.disco.own.features.add("http://jabber.org/protocol/caps"); + _converse.api.disco.own.features.add("http://jabber.org/protocol/disco#info"); + _converse.api.disco.own.features.add("http://jabber.org/protocol/disco#items"); + _converse.api.disco.own.features.add("http://jabber.org/protocol/muc"); - const presence = _converse.xmppstatus.constructPresence(); - expect(presence.toLocaleString()).toBe( - ``+ - `0`+ - ``+ - ``) - done(); - })); + const presence = _converse.xmppstatus.constructPresence(); + expect(presence.toLocaleString()).toBe( + ``+ + `0`+ + ``+ + ``) + done(); + })); - it("has a given priority", mock.initConverse((done, _converse) => { - let pres = _converse.xmppstatus.constructPresence('online', null, 'Hello world'); - expect(pres.toLocaleString()).toBe( - ``+ - `Hello world`+ + it("has a given priority", mock.initConverse((done, _converse) => { + let pres = _converse.xmppstatus.constructPresence('online', null, 'Hello world'); + expect(pres.toLocaleString()).toBe( + ``+ + `Hello world`+ + `0`+ + ``+ + `` + ); + _converse.priority = 2; + pres = _converse.xmppstatus.constructPresence('away', null, 'Going jogging'); + expect(pres.toLocaleString()).toBe( + ``+ + `away`+ + `Going jogging`+ + `2`+ + ``+ + `` + ); + + delete _converse.priority; + pres = _converse.xmppstatus.constructPresence('dnd', null, 'Doing taxes'); + expect(pres.toLocaleString()).toBe( + ``+ + `dnd`+ + `Doing taxes`+ + `0`+ + ``+ + `` + ); + done(); + })); + + it("includes the saved status message", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async (done, _converse) => { + + const { u, Strophe } = converse.env; + mock.openControlBox(_converse); + spyOn(_converse.connection, 'send').and.callThrough(); + + 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 msg = 'My custom status'; + modal.el.querySelector('input[name="status_message"]').value = msg; + modal.el.querySelector('[type="submit"]').click(); + + const sent_stanzas = _converse.connection.sent_stanzas; + let sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop()); + expect(Strophe.serialize(sent_presence)) + .toBe(``+ + `My custom status`+ `0`+ ``+ - `` - ); - _converse.priority = 2; - pres = _converse.xmppstatus.constructPresence('away', null, 'Going jogging'); - expect(pres.toLocaleString()).toBe( - ``+ - `away`+ - `Going jogging`+ - `2`+ - ``+ - `` - ); + ``) - delete _converse.priority; - pres = _converse.xmppstatus.constructPresence('dnd', null, 'Doing taxes'); - expect(pres.toLocaleString()).toBe( + await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "true"); + await u.waitUntil(() => !u.isVisible(modal.el)); + cbview.el.querySelector('.change-status').click() + await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "false", 1000); + modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd" + modal.el.querySelector('[type="submit"]').click(); + await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).length === 2); + sent_presence = sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop(); + expect(Strophe.serialize(sent_presence)) + .toBe( ``+ `dnd`+ - `Doing taxes`+ + `My custom status`+ `0`+ ``+ - `` - ); - done(); - })); - - it("includes the saved status message", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async (done, _converse) => { - - test_utils.openControlBox(_converse); - spyOn(_converse.connection, 'send').and.callThrough(); - - 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 msg = 'My custom status'; - modal.el.querySelector('input[name="status_message"]').value = msg; - modal.el.querySelector('[type="submit"]').click(); - - const sent_stanzas = _converse.connection.sent_stanzas; - let sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop()); - expect(Strophe.serialize(sent_presence)) - .toBe(``+ - `My custom status`+ - `0`+ - ``+ - ``) - - await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "true"); - await u.waitUntil(() => !u.isVisible(modal.el)); - cbview.el.querySelector('.change-status').click() - await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "false", 1000); - modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd" - modal.el.querySelector('[type="submit"]').click(); - await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).length === 2); - sent_presence = sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop(); - expect(Strophe.serialize(sent_presence)) - .toBe( - ``+ - `dnd`+ - `My custom status`+ - `0`+ - ``+ - ``) - done(); - })); - }); - - describe("A received presence stanza", function () { - - it("has its priority taken into account", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async (done, _converse) => { - - test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current'); - const contact_jid = mock.cur_names[8].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const contact = await _converse.api.contacts.get(contact_jid); - let stanza = u.toStanza(` - - 1 - - - ce51d94f7f22b87a21274abb93710b9eb7cc1c65 - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(contact.presence.get('show')).toBe('online'); - expect(contact.presence.resources.length).toBe(1); - expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); - expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); - - stanza = u.toStanza( - ''+ - ' '+ - ' 0'+ - ' xa'+ - ' '+ - ' '+ - ''); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(contact.presence.get('show')).toBe('online'); - - expect(contact.presence.resources.length).toBe(2); - expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); - expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); - expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); - expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); - - stanza = u.toStanza( - ''+ - ' 2'+ - ' dnd'+ - ''); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(contact.presence.get('show')).toBe('dnd'); - expect(contact.presence.resources.length).toBe(3); - expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); - expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); - expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); - expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); - expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2); - expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd'); - - stanza = u.toStanza( - ''+ - ' 3'+ - ' away'+ - ''); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('away'); - expect(contact.presence.resources.length).toBe(4); - expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); - expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); - expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); - expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); - expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2); - expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd'); - expect(contact.presence.resources.get('priority-3-resource').get('priority')).toBe(3); - expect(contact.presence.resources.get('priority-3-resource').get('show')).toBe('away'); - - stanza = u.toStanza( - ''+ - ' 1'+ - ' dnd'+ - ' '+ - ''); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('away'); - expect(contact.presence.resources.length).toBe(5); - expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1); - expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd'); - expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); - expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); - expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); - expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); - expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2); - expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd'); - expect(contact.presence.resources.get('priority-3-resource').get('priority')).toBe(3); - expect(contact.presence.resources.get('priority-3-resource').get('show')).toBe('away'); - - stanza = u.toStanza( - ''+ - ''); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('dnd'); - expect(contact.presence.resources.length).toBe(4); - expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); - expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); - expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); - expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); - expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2); - expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd'); - expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1); - expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd'); - - stanza = u.toStanza( - ''+ - ''); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('online'); - expect(contact.presence.resources.length).toBe(3); - expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); - expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); - expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); - expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); - expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1); - expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd'); - - stanza = u.toStanza( - ''+ - ''); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('dnd'); - expect(contact.presence.resources.length).toBe(2); - expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); - expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); - expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1); - expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd'); - - stanza = u.toStanza( - ''+ - ''); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('xa'); - expect(contact.presence.resources.length).toBe(1); - expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); - expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); - - stanza = u.toStanza( - ''+ - ''); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('offline'); - expect(contact.presence.resources.length).toBe(0); - done(); - })); - }); + ``) + done(); + })); +}); + +describe("A received presence stanza", function () { + + it("has its priority taken into account", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async (done, _converse) => { + + const u = converse.env.utils; + mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + const contact_jid = mock.cur_names[8].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const contact = await _converse.api.contacts.get(contact_jid); + let stanza = u.toStanza(` + + 1 + + + ce51d94f7f22b87a21274abb93710b9eb7cc1c65 + + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(contact.presence.get('show')).toBe('online'); + expect(contact.presence.resources.length).toBe(1); + expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); + expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); + + stanza = u.toStanza( + ''+ + ' '+ + ' 0'+ + ' xa'+ + ' '+ + ' '+ + ''); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(contact.presence.get('show')).toBe('online'); + + expect(contact.presence.resources.length).toBe(2); + expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); + expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); + expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); + expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); + + stanza = u.toStanza( + ''+ + ' 2'+ + ' dnd'+ + ''); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(contact.presence.get('show')).toBe('dnd'); + expect(contact.presence.resources.length).toBe(3); + expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); + expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); + expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); + expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); + expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2); + expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd'); + + stanza = u.toStanza( + ''+ + ' 3'+ + ' away'+ + ''); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('away'); + expect(contact.presence.resources.length).toBe(4); + expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); + expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); + expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); + expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); + expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2); + expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd'); + expect(contact.presence.resources.get('priority-3-resource').get('priority')).toBe(3); + expect(contact.presence.resources.get('priority-3-resource').get('show')).toBe('away'); + + stanza = u.toStanza( + ''+ + ' 1'+ + ' dnd'+ + ' '+ + ''); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('away'); + expect(contact.presence.resources.length).toBe(5); + expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1); + expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd'); + expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); + expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); + expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); + expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); + expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2); + expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd'); + expect(contact.presence.resources.get('priority-3-resource').get('priority')).toBe(3); + expect(contact.presence.resources.get('priority-3-resource').get('show')).toBe('away'); + + stanza = u.toStanza( + ''+ + ''); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('dnd'); + expect(contact.presence.resources.length).toBe(4); + expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); + expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); + expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); + expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); + expect(contact.presence.resources.get('priority-2-resource').get('priority')).toBe(2); + expect(contact.presence.resources.get('priority-2-resource').get('show')).toBe('dnd'); + expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1); + expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd'); + + stanza = u.toStanza( + ''+ + ''); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('online'); + expect(contact.presence.resources.length).toBe(3); + expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); + expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); + expect(contact.presence.resources.get('priority-1-resource').get('priority')).toBe(1); + expect(contact.presence.resources.get('priority-1-resource').get('show')).toBe('online'); + expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1); + expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd'); + + stanza = u.toStanza( + ''+ + ''); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('dnd'); + expect(contact.presence.resources.length).toBe(2); + expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); + expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); + expect(contact.presence.resources.get('older-priority-1-resource').get('priority')).toBe(1); + expect(contact.presence.resources.get('older-priority-1-resource').get('show')).toBe('dnd'); + + stanza = u.toStanza( + ''+ + ''); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('xa'); + expect(contact.presence.resources.length).toBe(1); + expect(contact.presence.resources.get('priority-0-resource').get('priority')).toBe(0); + expect(contact.presence.resources.get('priority-0-resource').get('show')).toBe('xa'); + + stanza = u.toStanza( + ''+ + ''); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_converse.roster.get(contact_jid).presence.get('show')).toBe('offline'); + expect(contact.presence.resources.length).toBe(0); + done(); + })); }); diff --git a/spec/profiling.js b/spec/profiling.js deleted file mode 100644 index 0f0a7d534..000000000 --- a/spec/profiling.js +++ /dev/null @@ -1,131 +0,0 @@ -(function (root, factory) { - define(["jasmine", "mock", "test-utils"], factory); -} (this, function (jasmine, mock, test_utils) { - var _ = converse.env._; - var $iq = converse.env.$iq; - var $pres = converse.env.$pres; - var u = converse.env.utils; - - describe("Profiling", function() { - - it("shows users currently present in the groupchat", - mock.initConverse( - ['rosterGroupsFetched'], {'muc_show_join_leave': false}, - async function (done, _converse) { - - test_utils.openControlBox(_converse); - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - _.rangeRight(3000, 0).forEach(i => { - const name = `User ${i.toString().padStart(5, '0')}`; - const presence = $pres({ - 'to': 'romeo@montague.lit/orchard', - 'from': 'lounge@montague.lit/'+name - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - affiliation: 'none', - jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - // expect(occupants.querySelectorAll('li').length).toBe(1+i); - // const model = view.model.occupants.where({'nick': name})[0]; - // const index = view.model.occupants.indexOf(model); - // expect(occupants.querySelectorAll('li .occupant-nick')[index].textContent.trim()).toBe(name); - }); - done(); - })); - - xit("adds hundreds of contacts to the roster", - mock.initConverse( - ['rosterGroupsFetched'], {}, - function (done, _converse) { - - _converse.roster_groups = false; - test_utils.openControlBox(_converse); - - expect(_converse.roster.pluck('jid').length).toBe(0); - var stanza = $iq({ - to: _converse.connection.jid, - type: 'result', - id: 'roster_1' - }).c('query', { - xmlns: 'jabber:iq:roster' - }); - _.each(['Friends', 'Colleagues', 'Family', 'Acquaintances'], function (group) { - var i; - for (i=0; i<50; i++) { - stanza = stanza.c('item', { - jid: Math.random().toString().replace('0.', '')+'@example.net', - subscription:'both' - }).c('group').t(group).up().up(); - } - }); - _converse.roster.onReceivedFromServer(stanza.tree()); - - return u.waitUntil(function () { - var $group = _converse.rosterview.$el.find('.roster-group') - return $group.length && u.isVisible($group[0]); - }).then(function () { - var count = 0; - _converse.roster.each(function (contact) { - if (count < 10) { - contact.set('chat_status', 'online'); - count += 1; - } - }); - return u.waitUntil(function () { - return _converse.rosterview.$el.find('li.online').length - }) - }).then(done); - })); - - xit("adds hundreds of contacts to the roster, with roster groups", - mock.initConverse( - ['rosterGroupsFetched'], {}, - function (done, _converse) { - - // _converse.show_only_online_users = true; - _converse.roster_groups = true; - test_utils.openControlBox(_converse); - - expect(_converse.roster.pluck('jid').length).toBe(0); - var stanza = $iq({ - to: _converse.connection.jid, - type: 'result', - id: 'roster_1' - }).c('query', { - xmlns: 'jabber:iq:roster' - }); - _.each(['Friends', 'Colleagues', 'Family', 'Acquaintances'], function (group) { - var i; - for (i=0; i<100; i++) { - stanza = stanza.c('item', { - jid: Math.random().toString().replace('0.', '')+'@example.net', - subscription:'both' - }).c('group').t(group).up().up(); - } - }); - _converse.roster.onReceivedFromServer(stanza.tree()); - - return u.waitUntil(function () { - var $group = _converse.rosterview.$el.find('.roster-group') - return $group.length && u.isVisible($group[0]); - }).then(function () { - _.each(['Friends', 'Colleagues', 'Family', 'Acquaintances'], function (group) { - var count = 0; - _converse.roster.each(function (contact) { - if (_.includes(contact.get('groups'), group)) { - if (count < 10) { - contact.set('chat_status', 'online'); - count += 1; - } - } - }); - }); - return u.waitUntil(function () { - return _converse.rosterview.$el.find('li.online').length - }) - }).then(done); - })); - }); -})); diff --git a/spec/protocol.js b/spec/protocol.js index bfe652439..cdf5ca7bb 100644 --- a/spec/protocol.js +++ b/spec/protocol.js @@ -1,539 +1,534 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const $iq = converse.env.$iq; - const $pres = converse.env.$pres; - const Strophe = converse.env.Strophe; - const _ = converse.env._; - const sizzle = converse.env.sizzle; - const u = converse.env.utils; - // See: - // https://xmpp.org/rfcs/rfc3921.html +/*global mock */ - describe("The Protocol", function () { +// See: https://xmpp.org/rfcs/rfc3921.html - describe("Integration of Roster Items and Presence Subscriptions", function () { - // Stub the trimChat method. It causes havoc when running with - // phantomJS. +describe("The Protocol", function () { - /* Some level of integration between roster items and presence - * subscriptions is normally expected by an instant messaging user - * regarding the user's subscriptions to and from other contacts. This - * section describes the level of integration that MUST be supported - * within an XMPP instant messaging applications. - * - * There are four primary subscription states: - * - * None -- the user does not have a subscription to the contact's - * presence information, and the contact does not have a subscription - * to the user's presence information - * To -- the user has a subscription to the contact's presence - * information, but the contact does not have a subscription to the - * user's presence information - * From -- the contact has a subscription to the user's presence - * information, but the user does not have a subscription to the - * contact's presence information - * Both -- both the user and the contact have subscriptions to each - * other's presence information (i.e., the union of 'from' and 'to') - * - * Each of these states is reflected in the roster of both the user and - * the contact, thus resulting in durable subscription states. - * - * The 'from' and 'to' addresses are OPTIONAL in roster pushes; if - * included, their values SHOULD be the full JID of the resource for - * that session. A client MUST acknowledge each roster push with an IQ - * stanza of type "result". - */ - it("Subscribe to contact, contact accepts and subscribes back", - mock.initConverse( - ['rosterGroupsFetched'], - { roster_groups: false }, - async function (done, _converse) { + describe("Integration of Roster Items and Presence Subscriptions", function () { + // Stub the trimChat method. It causes havoc when running with + // phantomJS. - let contact, sent_stanza, IQ_id, stanza; - await test_utils.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); - await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'), 300); - /* The process by which a user subscribes to a contact, including - * the interaction between roster items and subscription states. - */ - test_utils.openControlBox(_converse); - const cbview = _converse.chatboxviews.get('controlbox'); - - spyOn(_converse.roster, "addAndSubscribe").and.callThrough(); - spyOn(_converse.roster, "addContactToRoster").and.callThrough(); - spyOn(_converse.roster, "sendContactAddIQ").and.callThrough(); - spyOn(_converse.api.vcard, "get").and.callThrough(); - - 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); - }); - - cbview.el.querySelector('.add-contact').click() - const modal = _converse.rosterview.add_contact_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - spyOn(modal, "addContactFromForm").and.callThrough(); - modal.delegateEvents(); - - // Fill in the form and submit - const form = modal.el.querySelector('form.add-xmpp-contact'); - form.querySelector('input').value = 'contact@example.org'; - form.querySelector('[type="submit"]').click(); - - /* In preparation for being able to render the contact in the - * user's client interface and for the server to keep track of the - * subscription, the user's client SHOULD perform a "roster set" - * for the new roster item. - */ - expect(modal.addContactFromForm).toHaveBeenCalled(); - expect(_converse.roster.addAndSubscribe).toHaveBeenCalled(); - expect(_converse.roster.addContactToRoster).toHaveBeenCalled(); - - /* _converse request consists of sending an IQ - * stanza of type='set' containing a element qualified by - * the 'jabber:iq:roster' namespace, which in turn contains an - * element that defines the new roster item; the - * element MUST possess a 'jid' attribute, MAY possess a 'name' - * attribute, MUST NOT possess a 'subscription' attribute, and MAY - * contain one or more child elements: - * - * - * - * - * MyBuddies - * - * - * - */ - await test_utils.waitForRoster(_converse, 'all', 0); - expect(_converse.roster.sendContactAddIQ).toHaveBeenCalled(); - expect(sent_stanza.toLocaleString()).toBe( - ``+ - ``+ - ``+ - ``+ - `` - ); - /* As a result, the user's server (1) MUST initiate a roster push - * for the new roster item to all available resources associated - * with _converse user that have requested the roster, setting the - * 'subscription' attribute to a value of "none"; and (2) MUST - * reply to the sending resource with an IQ result indicating the - * success of the roster set: - * - * - * - * - * MyBuddies - * - * - * - */ - const create = _converse.roster.create; - const sent_stanzas = []; - spyOn(_converse.connection, 'send').and.callFake(function (stanza) { - sent_stanza = stanza; - sent_stanzas.push(stanza.toLocaleString()); - }); - spyOn(_converse.roster, 'create').and.callFake(function () { - contact = create.apply(_converse.roster, arguments); - spyOn(contact, 'subscribe').and.callThrough(); - return contact; - }); - stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'}) - .c('item', { - 'jid': 'contact@example.org', - 'subscription': 'none', - 'name': 'contact@example.org'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - /* - */ - stanza = $iq({'type': 'result', 'id':IQ_id}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - await u.waitUntil(() => _converse.roster.create.calls.count()); - - // A contact should now have been created - expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy(); - expect(contact.get('jid')).toBe('contact@example.org'); - await u.waitUntil(() => contact.initialized); - - /* To subscribe to the contact's presence information, - * the user's client MUST send a presence stanza of - * type='subscribe' to the contact: - * - * - */ - const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => s.match('presence')).pop()); - expect(contact.subscribe).toHaveBeenCalled(); - expect(sent_presence).toBe( - ``+ - `Romeo Montague`+ - `` - ); - /* As a result, the user's server MUST initiate a second roster - * push to all of the user's available resources that have - * requested the roster, setting the contact to the pending - * sub-state of the 'none' subscription state; _converse pending - * sub-state is denoted by the inclusion of the ask='subscribe' - * attribute in the roster item: - * - * - * - * - * MyBuddies - * - * - * - */ - spyOn(_converse.roster, "updateContact").and.callThrough(); - stanza = $iq({'type': 'set', 'from': _converse.bare_jid}) - .c('query', {'xmlns': 'jabber:iq:roster'}) - .c('item', { - 'jid': 'contact@example.org', - 'subscription': 'none', - 'ask': 'subscribe', - 'name': 'contact@example.org'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_converse.roster.updateContact).toHaveBeenCalled(); - // Check that the user is now properly shown as a pending - // contact in the roster. - await u.waitUntil(() => { - const header = sizzle('a:contains("Pending contacts")', _converse.rosterview.el).pop(); - const contacts = _.filter(header.parentElement.querySelectorAll('li'), u.isVisible); - return contacts.length; - }, 600); - - let header = sizzle('a:contains("Pending contacts")', _converse.rosterview.el).pop(); - let contacts = header.parentElement.querySelectorAll('li'); - expect(contacts.length).toBe(1); - expect(u.isVisible(contacts[0])).toBe(true); - - spyOn(contact, "ackSubscribe").and.callThrough(); - /* Here we assume the "happy path" that the contact - * approves the subscription request - * - * - */ - stanza = $pres({ - 'to': _converse.bare_jid, - 'from': 'contact@example.org', - 'type': 'subscribed' - }); - sent_stanza = ""; // Reset - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - /* Upon receiving the presence stanza of type "subscribed", - * the user SHOULD acknowledge receipt of that - * subscription state notification by sending a presence - * stanza of type "subscribe". - */ - expect(contact.ackSubscribe).toHaveBeenCalled(); - expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec) - `` - ); - - /* The user's server MUST initiate a roster push to all of the user's - * available resources that have requested the roster, - * containing an updated roster item for the contact with - * the 'subscription' attribute set to a value of "to"; - * - * - * - * - * MyBuddies - * - * - * - */ - IQ_id = _converse.connection.getUniqueId('roster'); - stanza = $iq({'type': 'set', 'id': IQ_id}) - .c('query', {'xmlns': 'jabber:iq:roster'}) - .c('item', { - 'jid': 'contact@example.org', - 'subscription': 'to', - 'name': 'Nicky'}); - - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - // Check that the IQ set was acknowledged. - expect(Strophe.serialize(sent_stanza)).toBe( // Strophe adds the xmlns attr (although not in spec) - `` - ); - expect(_converse.roster.updateContact).toHaveBeenCalled(); - - // The contact should now be visible as an existing - // contact (but still offline). - await u.waitUntil(() => { - const header = sizzle('a:contains("My contacts")', _converse.rosterview.el); - return sizzle('li', header[0].parentNode).filter(l => u.isVisible(l)).length; - }, 600); - header = sizzle('a:contains("My contacts")', _converse.rosterview.el); - expect(header.length).toBe(1); - expect(u.isVisible(header[0])).toBeTruthy(); - contacts = header[0].parentNode.querySelectorAll('li'); - expect(contacts.length).toBe(1); - // Check that it has the right classes and text - expect(u.hasClass('to', contacts[0])).toBeTruthy(); - expect(u.hasClass('both', contacts[0])).toBeFalsy(); - expect(u.hasClass('current-xmpp-contact', contacts[0])).toBeTruthy(); - expect(contacts[0].textContent.trim()).toBe('Nicky'); - - expect(contact.presence.get('show')).toBe('offline'); - - /* - */ - stanza = $pres({'to': _converse.bare_jid, 'from': 'contact@example.org/resource'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - // Now the contact should also be online. - expect(contact.presence.get('show')).toBe('online'); - - /* Section 8.3. Creating a Mutual Subscription - * - * If the contact wants to create a mutual subscription, - * the contact MUST send a subscription request to the - * user. - * - * - */ - spyOn(contact, 'authorize').and.callThrough(); - spyOn(_converse.roster, 'handleIncomingSubscription').and.callThrough(); - stanza = $pres({ - 'to': _converse.bare_jid, - 'from': 'contact@example.org/resource', - 'type': 'subscribe'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_converse.roster.handleIncomingSubscription).toHaveBeenCalled(); - - /* The user's client MUST send a presence stanza of type - * "subscribed" to the contact in order to approve the - * subscription request. - * - * - */ - expect(contact.authorize).toHaveBeenCalled(); - expect(sent_stanza.toLocaleString()).toBe( - `` - ); - - /* As a result, the user's server MUST initiate a - * roster push containing a roster item for the - * contact with the 'subscription' attribute set to - * a value of "both". - * - * - * - * - * MyBuddies - * - * - * - */ - stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'}) - .c('item', { - 'jid': 'contact@example.org', - 'subscription': 'both', - 'name': 'contact@example.org'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(_converse.roster.updateContact).toHaveBeenCalled(); - - // The class on the contact will now have switched. - await u.waitUntil(() => !u.hasClass('to', contacts[0])); - expect(u.hasClass('both', contacts[0])).toBe(true); - done(); - - })); - - it("Alternate Flow: Contact Declines Subscription Request", - mock.initConverse( - ['rosterGroupsFetched'], {}, - function (done, _converse) { - - /* The process by which a user subscribes to a contact, including - * the interaction between roster items and subscription states. - */ - var contact, stanza, sent_stanza, sent_IQ; - test_utils.openControlBox(_converse); - // Add a new roster contact via roster push - stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'}) - .c('item', { - 'jid': 'contact@example.org', - 'subscription': 'none', - 'ask': 'subscribe', - 'name': 'contact@example.org'}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - // A pending contact should now exist. - contact = _converse.roster.get('contact@example.org'); - expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy(); - spyOn(contact, "ackUnsubscribe").and.callThrough(); - - spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza }); - spyOn(_converse.connection, 'sendIQ').and.callFake(iq => { sent_IQ = iq }); - /* We now assume the contact declines the subscription - * requests. - * - * Upon receiving the presence stanza of type "unsubscribed" - * addressed to the user, the user's server (1) MUST deliver - * that presence stanza to the user and (2) MUST initiate a - * roster push to all of the user's available resources that - * have requested the roster, containing an updated roster - * item for the contact with the 'subscription' attribute - * set to a value of "none" and with no 'ask' attribute: - * - * - * - * - * - * - * MyBuddies - * - * - * - */ - // FIXME: also add the - stanza = $pres({ - 'to': _converse.bare_jid, - 'from': 'contact@example.org', - 'type': 'unsubscribed' - }); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - /* Upon receiving the presence stanza of type "unsubscribed", - * the user SHOULD acknowledge receipt of that subscription - * state notification through either "affirming" it by - * sending a presence stanza of type "unsubscribe - */ - expect(contact.ackUnsubscribe).toHaveBeenCalled(); - expect(sent_stanza.toLocaleString()).toBe( - `` - ); - - /* _converse.js will then also automatically remove the - * contact from the user's roster. - */ - expect(sent_IQ.toLocaleString()).toBe( - ``+ - ``+ - ``+ - ``+ - `` - ); - done(); - })); - - it("Unsubscribe to a contact when subscription is mutual", - mock.initConverse( - ['rosterGroupsFetched'], - { roster_groups: false }, - async function (done, _converse) { - - const jid = 'abram@montague.lit'; - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current'); - spyOn(window, 'confirm').and.returnValue(true); - // We now have a contact we want to remove - expect(_converse.roster.get(jid) instanceof _converse.RosterContact).toBeTruthy(); - - const header = sizzle('a:contains("My contacts")', _converse.rosterview.el).pop(); - await u.waitUntil(() => header.parentElement.querySelectorAll('li').length); - - // remove the first user - header.parentElement.querySelector('li .remove-xmpp-contact').click(); - expect(window.confirm).toHaveBeenCalled(); - - /* Section 8.6 Removing a Roster Item and Cancelling All - * Subscriptions - * - * First the user is removed from the roster - * Because there may be many steps involved in completely - * removing a roster item and cancelling subscriptions in - * both directions, the roster management protocol includes - * a "shortcut" method for doing so. The process may be - * initiated no matter what the current subscription state - * is by sending a roster set containing an item for the - * contact with the 'subscription' attribute set to a value - * of "remove": - * - * - * - * - * - * - */ - const sent_iq = _converse.connection.IQ_stanzas.pop(); - expect(Strophe.serialize(sent_iq)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - // Receive confirmation from the contact's server - // - const stanza = $iq({'type': 'result', 'id': sent_iq.getAttribute('id')}); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - // Our contact has now been removed - await u.waitUntil(() => typeof _converse.roster.get(jid) === "undefined"); - done(); - })); - - it("Receiving a subscription request", mock.initConverse( - ['rosterGroupsFetched'], {}, + /* Some level of integration between roster items and presence + * subscriptions is normally expected by an instant messaging user + * regarding the user's subscriptions to and from other contacts. This + * section describes the level of integration that MUST be supported + * within an XMPP instant messaging applications. + * + * There are four primary subscription states: + * + * None -- the user does not have a subscription to the contact's + * presence information, and the contact does not have a subscription + * to the user's presence information + * To -- the user has a subscription to the contact's presence + * information, but the contact does not have a subscription to the + * user's presence information + * From -- the contact has a subscription to the user's presence + * information, but the user does not have a subscription to the + * contact's presence information + * Both -- both the user and the contact have subscriptions to each + * other's presence information (i.e., the union of 'from' and 'to') + * + * Each of these states is reflected in the roster of both the user and + * the contact, thus resulting in durable subscription states. + * + * The 'from' and 'to' addresses are OPTIONAL in roster pushes; if + * included, their values SHOULD be the full JID of the resource for + * that session. A client MUST acknowledge each roster push with an IQ + * stanza of type "result". + */ + it("Subscribe to contact, contact accepts and subscribes back", + mock.initConverse( + ['rosterGroupsFetched'], + { roster_groups: false }, async function (done, _converse) { - spyOn(_converse.api, "trigger").and.callThrough(); - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current'); - /* - */ - const stanza = $pres({ - 'to': _converse.bare_jid, - 'from': 'contact@example.org', - 'type': 'subscribe' - }).c('nick', { - 'xmlns': Strophe.NS.NICK, - }).t('Clint Contact'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => { - const header = sizzle('a:contains("Contact requests")', _converse.rosterview.el).pop(); - const contacts = _.filter(header.parentElement.querySelectorAll('li'), u.isVisible); - return contacts.length; - }, 500); - expect(_converse.api.trigger).toHaveBeenCalledWith('contactRequest', jasmine.any(Object)); + const { u, $iq, $pres, sizzle, Strophe } = converse.env; + let contact, sent_stanza, IQ_id, stanza; + await mock.waitUntilDiscoConfirmed(_converse, 'montague.lit', [], ['vcard-temp']); + await u.waitUntil(() => _converse.xmppstatus.vcard.get('fullname'), 300); + /* The process by which a user subscribes to a contact, including + * the interaction between roster items and subscription states. + */ + mock.openControlBox(_converse); + const cbview = _converse.chatboxviews.get('controlbox'); + + spyOn(_converse.roster, "addAndSubscribe").and.callThrough(); + spyOn(_converse.roster, "addContactToRoster").and.callThrough(); + spyOn(_converse.roster, "sendContactAddIQ").and.callThrough(); + spyOn(_converse.api.vcard, "get").and.callThrough(); + + 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); + }); + + cbview.el.querySelector('.add-contact').click() + const modal = _converse.rosterview.add_contact_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + spyOn(modal, "addContactFromForm").and.callThrough(); + modal.delegateEvents(); + + // Fill in the form and submit + const form = modal.el.querySelector('form.add-xmpp-contact'); + form.querySelector('input').value = 'contact@example.org'; + form.querySelector('[type="submit"]').click(); + + /* In preparation for being able to render the contact in the + * user's client interface and for the server to keep track of the + * subscription, the user's client SHOULD perform a "roster set" + * for the new roster item. + */ + expect(modal.addContactFromForm).toHaveBeenCalled(); + expect(_converse.roster.addAndSubscribe).toHaveBeenCalled(); + expect(_converse.roster.addContactToRoster).toHaveBeenCalled(); + + /* _converse request consists of sending an IQ + * stanza of type='set' containing a element qualified by + * the 'jabber:iq:roster' namespace, which in turn contains an + * element that defines the new roster item; the + * element MUST possess a 'jid' attribute, MAY possess a 'name' + * attribute, MUST NOT possess a 'subscription' attribute, and MAY + * contain one or more child elements: + * + * + * + * + * MyBuddies + * + * + * + */ + await mock.waitForRoster(_converse, 'all', 0); + expect(_converse.roster.sendContactAddIQ).toHaveBeenCalled(); + expect(sent_stanza.toLocaleString()).toBe( + ``+ + ``+ + ``+ + ``+ + `` + ); + /* As a result, the user's server (1) MUST initiate a roster push + * for the new roster item to all available resources associated + * with _converse user that have requested the roster, setting the + * 'subscription' attribute to a value of "none"; and (2) MUST + * reply to the sending resource with an IQ result indicating the + * success of the roster set: + * + * + * + * + * MyBuddies + * + * + * + */ + const create = _converse.roster.create; + const sent_stanzas = []; + spyOn(_converse.connection, 'send').and.callFake(function (stanza) { + sent_stanza = stanza; + sent_stanzas.push(stanza.toLocaleString()); + }); + spyOn(_converse.roster, 'create').and.callFake(function () { + contact = create.apply(_converse.roster, arguments); + spyOn(contact, 'subscribe').and.callThrough(); + return contact; + }); + stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'}) + .c('item', { + 'jid': 'contact@example.org', + 'subscription': 'none', + 'name': 'contact@example.org'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + /* + */ + stanza = $iq({'type': 'result', 'id':IQ_id}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + await u.waitUntil(() => _converse.roster.create.calls.count()); + + // A contact should now have been created + expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy(); + expect(contact.get('jid')).toBe('contact@example.org'); + await u.waitUntil(() => contact.initialized); + + /* To subscribe to the contact's presence information, + * the user's client MUST send a presence stanza of + * type='subscribe' to the contact: + * + * + */ + const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => s.match('presence')).pop()); + expect(contact.subscribe).toHaveBeenCalled(); + expect(sent_presence).toBe( + ``+ + `Romeo Montague`+ + `` + ); + /* As a result, the user's server MUST initiate a second roster + * push to all of the user's available resources that have + * requested the roster, setting the contact to the pending + * sub-state of the 'none' subscription state; _converse pending + * sub-state is denoted by the inclusion of the ask='subscribe' + * attribute in the roster item: + * + * + * + * + * MyBuddies + * + * + * + */ + spyOn(_converse.roster, "updateContact").and.callThrough(); + stanza = $iq({'type': 'set', 'from': _converse.bare_jid}) + .c('query', {'xmlns': 'jabber:iq:roster'}) + .c('item', { + 'jid': 'contact@example.org', + 'subscription': 'none', + 'ask': 'subscribe', + 'name': 'contact@example.org'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_converse.roster.updateContact).toHaveBeenCalled(); + // Check that the user is now properly shown as a pending + // contact in the roster. + await u.waitUntil(() => { + const header = sizzle('a:contains("Pending contacts")', _converse.rosterview.el).pop(); + const contacts = Array.from(header.parentElement.querySelectorAll('li')).filter(u.isVisible); + return contacts.length; + }, 600); + + let header = sizzle('a:contains("Pending contacts")', _converse.rosterview.el).pop(); + let contacts = header.parentElement.querySelectorAll('li'); + expect(contacts.length).toBe(1); + expect(u.isVisible(contacts[0])).toBe(true); + + spyOn(contact, "ackSubscribe").and.callThrough(); + /* Here we assume the "happy path" that the contact + * approves the subscription request + * + * + */ + stanza = $pres({ + 'to': _converse.bare_jid, + 'from': 'contact@example.org', + 'type': 'subscribed' + }); + sent_stanza = ""; // Reset + _converse.connection._dataRecv(mock.createRequest(stanza)); + /* Upon receiving the presence stanza of type "subscribed", + * the user SHOULD acknowledge receipt of that + * subscription state notification by sending a presence + * stanza of type "subscribe". + */ + expect(contact.ackSubscribe).toHaveBeenCalled(); + expect(sent_stanza.toLocaleString()).toBe( // Strophe adds the xmlns attr (although not in spec) + `` + ); + + /* The user's server MUST initiate a roster push to all of the user's + * available resources that have requested the roster, + * containing an updated roster item for the contact with + * the 'subscription' attribute set to a value of "to"; + * + * + * + * + * MyBuddies + * + * + * + */ + IQ_id = _converse.connection.getUniqueId('roster'); + stanza = $iq({'type': 'set', 'id': IQ_id}) + .c('query', {'xmlns': 'jabber:iq:roster'}) + .c('item', { + 'jid': 'contact@example.org', + 'subscription': 'to', + 'name': 'Nicky'}); + + _converse.connection._dataRecv(mock.createRequest(stanza)); + // Check that the IQ set was acknowledged. + expect(Strophe.serialize(sent_stanza)).toBe( // Strophe adds the xmlns attr (although not in spec) + `` + ); + expect(_converse.roster.updateContact).toHaveBeenCalled(); + + // The contact should now be visible as an existing + // contact (but still offline). + await u.waitUntil(() => { + const header = sizzle('a:contains("My contacts")', _converse.rosterview.el); + return sizzle('li', header[0].parentNode).filter(l => u.isVisible(l)).length; + }, 600); + header = sizzle('a:contains("My contacts")', _converse.rosterview.el); + expect(header.length).toBe(1); + expect(u.isVisible(header[0])).toBeTruthy(); + contacts = header[0].parentNode.querySelectorAll('li'); + expect(contacts.length).toBe(1); + // Check that it has the right classes and text + expect(u.hasClass('to', contacts[0])).toBeTruthy(); + expect(u.hasClass('both', contacts[0])).toBeFalsy(); + expect(u.hasClass('current-xmpp-contact', contacts[0])).toBeTruthy(); + expect(contacts[0].textContent.trim()).toBe('Nicky'); + + expect(contact.presence.get('show')).toBe('offline'); + + /* + */ + stanza = $pres({'to': _converse.bare_jid, 'from': 'contact@example.org/resource'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + // Now the contact should also be online. + expect(contact.presence.get('show')).toBe('online'); + + /* Section 8.3. Creating a Mutual Subscription + * + * If the contact wants to create a mutual subscription, + * the contact MUST send a subscription request to the + * user. + * + * + */ + spyOn(contact, 'authorize').and.callThrough(); + spyOn(_converse.roster, 'handleIncomingSubscription').and.callThrough(); + stanza = $pres({ + 'to': _converse.bare_jid, + 'from': 'contact@example.org/resource', + 'type': 'subscribe'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_converse.roster.handleIncomingSubscription).toHaveBeenCalled(); + + /* The user's client MUST send a presence stanza of type + * "subscribed" to the contact in order to approve the + * subscription request. + * + * + */ + expect(contact.authorize).toHaveBeenCalled(); + expect(sent_stanza.toLocaleString()).toBe( + `` + ); + + /* As a result, the user's server MUST initiate a + * roster push containing a roster item for the + * contact with the 'subscription' attribute set to + * a value of "both". + * + * + * + * + * MyBuddies + * + * + * + */ + stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'}) + .c('item', { + 'jid': 'contact@example.org', + 'subscription': 'both', + 'name': 'contact@example.org'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(_converse.roster.updateContact).toHaveBeenCalled(); + + // The class on the contact will now have switched. + await u.waitUntil(() => !u.hasClass('to', contacts[0])); + expect(u.hasClass('both', contacts[0])).toBe(true); + done(); + + })); + + it("Alternate Flow: Contact Declines Subscription Request", + mock.initConverse( + ['rosterGroupsFetched'], {}, + function (done, _converse) { + + const { $iq, $pres } = converse.env; + /* The process by which a user subscribes to a contact, including + * the interaction between roster items and subscription states. + */ + var contact, stanza, sent_stanza, sent_IQ; + mock.openControlBox(_converse); + // Add a new roster contact via roster push + stanza = $iq({'type': 'set'}).c('query', {'xmlns': 'jabber:iq:roster'}) + .c('item', { + 'jid': 'contact@example.org', + 'subscription': 'none', + 'ask': 'subscribe', + 'name': 'contact@example.org'}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + // A pending contact should now exist. + contact = _converse.roster.get('contact@example.org'); + expect(_converse.roster.get('contact@example.org') instanceof _converse.RosterContact).toBeTruthy(); + spyOn(contact, "ackUnsubscribe").and.callThrough(); + + spyOn(_converse.connection, 'send').and.callFake(stanza => { sent_stanza = stanza }); + spyOn(_converse.connection, 'sendIQ').and.callFake(iq => { sent_IQ = iq }); + /* We now assume the contact declines the subscription + * requests. + * + * Upon receiving the presence stanza of type "unsubscribed" + * addressed to the user, the user's server (1) MUST deliver + * that presence stanza to the user and (2) MUST initiate a + * roster push to all of the user's available resources that + * have requested the roster, containing an updated roster + * item for the contact with the 'subscription' attribute + * set to a value of "none" and with no 'ask' attribute: + * + * + * + * + * + * + * MyBuddies + * + * + * + */ + // FIXME: also add the + stanza = $pres({ + 'to': _converse.bare_jid, + 'from': 'contact@example.org', + 'type': 'unsubscribed' + }); + _converse.connection._dataRecv(mock.createRequest(stanza)); + + /* Upon receiving the presence stanza of type "unsubscribed", + * the user SHOULD acknowledge receipt of that subscription + * state notification through either "affirming" it by + * sending a presence stanza of type "unsubscribe + */ + expect(contact.ackUnsubscribe).toHaveBeenCalled(); + expect(sent_stanza.toLocaleString()).toBe( + `` + ); + + /* _converse.js will then also automatically remove the + * contact from the user's roster. + */ + expect(sent_IQ.toLocaleString()).toBe( + ``+ + ``+ + ``+ + ``+ + `` + ); + done(); + })); + + it("Unsubscribe to a contact when subscription is mutual", + mock.initConverse( + ['rosterGroupsFetched'], + { roster_groups: false }, + async function (done, _converse) { + + const { u, $iq, sizzle, Strophe } = converse.env; + const jid = 'abram@montague.lit'; + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + spyOn(window, 'confirm').and.returnValue(true); + // We now have a contact we want to remove + expect(_converse.roster.get(jid) instanceof _converse.RosterContact).toBeTruthy(); + + const header = sizzle('a:contains("My contacts")', _converse.rosterview.el).pop(); + await u.waitUntil(() => header.parentElement.querySelectorAll('li').length); + + // remove the first user + header.parentElement.querySelector('li .remove-xmpp-contact').click(); + expect(window.confirm).toHaveBeenCalled(); + + /* Section 8.6 Removing a Roster Item and Cancelling All + * Subscriptions + * + * First the user is removed from the roster + * Because there may be many steps involved in completely + * removing a roster item and cancelling subscriptions in + * both directions, the roster management protocol includes + * a "shortcut" method for doing so. The process may be + * initiated no matter what the current subscription state + * is by sending a roster set containing an item for the + * contact with the 'subscription' attribute set to a value + * of "remove": + * + * + * + * + * + * + */ + const sent_iq = _converse.connection.IQ_stanzas.pop(); + expect(Strophe.serialize(sent_iq)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + // Receive confirmation from the contact's server + // + const stanza = $iq({'type': 'result', 'id': sent_iq.getAttribute('id')}); + _converse.connection._dataRecv(mock.createRequest(stanza)); + // Our contact has now been removed + await u.waitUntil(() => typeof _converse.roster.get(jid) === "undefined"); + done(); + })); + + it("Receiving a subscription request", mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const { u, $pres, sizzle, Strophe } = converse.env; + spyOn(_converse.api, "trigger").and.callThrough(); + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + /* + */ + const stanza = $pres({ + 'to': _converse.bare_jid, + 'from': 'contact@example.org', + 'type': 'subscribe' + }).c('nick', { + 'xmlns': Strophe.NS.NICK, + }).t('Clint Contact'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => { const header = sizzle('a:contains("Contact requests")', _converse.rosterview.el).pop(); - expect(u.isVisible(header)).toBe(true); - const contacts = header.parentElement.querySelectorAll('li'); - expect(contacts.length).toBe(1); - done(); - })); - }); + const contacts = Array.from(header.parentElement.querySelectorAll('li')).filter(u.isVisible); + return contacts.length; + }, 500); + expect(_converse.api.trigger).toHaveBeenCalledWith('contactRequest', jasmine.any(Object)); + const header = sizzle('a:contains("Contact requests")', _converse.rosterview.el).pop(); + expect(u.isVisible(header)).toBe(true); + const contacts = header.parentElement.querySelectorAll('li'); + expect(contacts.length).toBe(1); + done(); + })); }); }); diff --git a/spec/push.js b/spec/push.js index ccfc37d46..81e94a18b 100644 --- a/spec/push.js +++ b/spec/push.js @@ -1,191 +1,189 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const $iq = converse.env.$iq; - const Strophe = converse.env.Strophe; - const _ = converse.env._; - const sizzle = converse.env.sizzle; - const u = converse.env.utils; +/*global mock */ - describe("XEP-0357 Push Notifications", function () { +const $iq = converse.env.$iq; +const Strophe = converse.env.Strophe; +const _ = converse.env._; +const sizzle = converse.env.sizzle; +const u = converse.env.utils; - it("can be enabled", - mock.initConverse( - ['rosterGroupsFetched'], { - 'push_app_servers': [{ - 'jid': 'push-5@client.example', - 'node': 'yxs32uqsflafdk3iuqo' - }] - }, async function (done, _converse) { +describe("XEP-0357 Push Notifications", function () { - const IQ_stanzas = _converse.connection.IQ_stanzas; - expect(_converse.session.get('push_enabled')).toBeFalsy(); + it("can be enabled", + mock.initConverse( + ['rosterGroupsFetched'], { + 'push_app_servers': [{ + 'jid': 'push-5@client.example', + 'node': 'yxs32uqsflafdk3iuqo' + }] + }, async function (done, _converse) { - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.push_app_servers[0].jid, - [{'category': 'pubsub', 'type':'push'}], - ['urn:xmpp:push:0'], [], 'info'); - await test_utils.waitUntilDiscoConfirmed( - _converse, - _converse.bare_jid, - [{'category': 'account', 'type':'registered'}], - ['urn:xmpp:push:0'], [], 'info'); - const stanza = await u.waitUntil(() => - _.filter(IQ_stanzas, iq => iq.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]')).pop() - ); - expect(Strophe.serialize(stanza)).toEqual( - ``+ - ''+ - '' - ) - _converse.connection._dataRecv(test_utils.createRequest($iq({ - 'to': _converse.connection.jid, - 'type': 'result', - 'id': stanza.getAttribute('id') - }))); - await u.waitUntil(() => _converse.session.get('push_enabled')); - done(); - })); + const IQ_stanzas = _converse.connection.IQ_stanzas; + expect(_converse.session.get('push_enabled')).toBeFalsy(); - it("can be enabled for a MUC domain", - mock.initConverse( - ['rosterGroupsFetched'], { - 'enable_muc_push': true, - 'push_app_servers': [{ - 'jid': 'push-5@client.example', - 'node': 'yxs32uqsflafdk3iuqo' - }] - }, async function (done, _converse) { - - const IQ_stanzas = _converse.connection.IQ_stanzas; - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.push_app_servers[0].jid, - [{'category': 'pubsub', 'type':'push'}], - ['urn:xmpp:push:0'], [], 'info'); - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, [], - ['urn:xmpp:push:0']); - - let iq = await u.waitUntil(() => _.filter( - IQ_stanzas, - iq => sizzle(`iq[type="set"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length - ).pop()); - - expect(Strophe.serialize(iq)).toBe( - ``+ - ``+ - `` - ); - const result = u.toStanza(``); - _converse.connection._dataRecv(test_utils.createRequest(result)); - - await u.waitUntil(() => _converse.session.get('push_enabled')); - expect(_converse.session.get('push_enabled').length).toBe(1); - expect(_.includes(_converse.session.get('push_enabled'), 'romeo@montague.lit')).toBe(true); - - test_utils.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'oldhag'); - await test_utils.waitUntilDiscoConfirmed( - _converse, 'chat.shakespeare.lit', - [{'category': 'account', 'type':'registered'}], - ['urn:xmpp:push:0'], [], 'info'); - iq = await u.waitUntil(() => _.filter( - IQ_stanzas, - iq => sizzle(`iq[type="set"][to="chat.shakespeare.lit"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length - ).pop()); - - expect(Strophe.serialize(iq)).toEqual( - ``+ - ''+ - '' - ); - _converse.connection._dataRecv(test_utils.createRequest($iq({ - 'to': _converse.connection.jid, - 'type': 'result', - 'id': iq.getAttribute('id') - }))); - await u.waitUntil(() => _.includes(_converse.session.get('push_enabled'), 'chat.shakespeare.lit')); - done(); - })); - - it("can be disabled", - mock.initConverse( - ['rosterGroupsFetched'], { - 'push_app_servers': [{ - 'jid': 'push-5@client.example', - 'node': 'yxs32uqsflafdk3iuqo', - 'disable': true - }] - }, async function (done, _converse) { - - const IQ_stanzas = _converse.connection.IQ_stanzas; - expect(_converse.session.get('push_enabled')).toBeFalsy(); - - await test_utils.waitUntilDiscoConfirmed( + await mock.waitUntilDiscoConfirmed( + _converse, _converse.push_app_servers[0].jid, + [{'category': 'pubsub', 'type':'push'}], + ['urn:xmpp:push:0'], [], 'info'); + await mock.waitUntilDiscoConfirmed( _converse, _converse.bare_jid, [{'category': 'account', 'type':'registered'}], ['urn:xmpp:push:0'], [], 'info'); - const stanza = await u.waitUntil( - () => _.filter(IQ_stanzas, iq => iq.querySelector('iq[type="set"] disable[xmlns="urn:xmpp:push:0"]')).pop() - ); - expect(Strophe.serialize(stanza)).toEqual( - ``+ - ''+ - '' - ); - _converse.connection._dataRecv(test_utils.createRequest($iq({ - 'to': _converse.connection.jid, - 'type': 'result', - 'id': stanza.getAttribute('id') - }))); - await u.waitUntil(() => _converse.session.get('push_enabled')) - done(); - })); + const stanza = await u.waitUntil(() => + _.filter(IQ_stanzas, iq => iq.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]')).pop() + ); + expect(Strophe.serialize(stanza)).toEqual( + ``+ + ''+ + '' + ) + _converse.connection._dataRecv(mock.createRequest($iq({ + 'to': _converse.connection.jid, + 'type': 'result', + 'id': stanza.getAttribute('id') + }))); + await u.waitUntil(() => _converse.session.get('push_enabled')); + done(); + })); + + it("can be enabled for a MUC domain", + mock.initConverse( + ['rosterGroupsFetched'], { + 'enable_muc_push': true, + 'push_app_servers': [{ + 'jid': 'push-5@client.example', + 'node': 'yxs32uqsflafdk3iuqo' + }] + }, async function (done, _converse) { + + const IQ_stanzas = _converse.connection.IQ_stanzas; + await mock.waitUntilDiscoConfirmed( + _converse, _converse.push_app_servers[0].jid, + [{'category': 'pubsub', 'type':'push'}], + ['urn:xmpp:push:0'], [], 'info'); + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, [], + ['urn:xmpp:push:0']); + + let iq = await u.waitUntil(() => _.filter( + IQ_stanzas, + iq => sizzle(`iq[type="set"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length + ).pop()); + + expect(Strophe.serialize(iq)).toBe( + ``+ + ``+ + `` + ); + const result = u.toStanza(``); + _converse.connection._dataRecv(mock.createRequest(result)); + + await u.waitUntil(() => _converse.session.get('push_enabled')); + expect(_converse.session.get('push_enabled').length).toBe(1); + expect(_.includes(_converse.session.get('push_enabled'), 'romeo@montague.lit')).toBe(true); + + mock.openAndEnterChatRoom(_converse, 'coven@chat.shakespeare.lit', 'oldhag'); + await mock.waitUntilDiscoConfirmed( + _converse, 'chat.shakespeare.lit', + [{'category': 'account', 'type':'registered'}], + ['urn:xmpp:push:0'], [], 'info'); + iq = await u.waitUntil(() => _.filter( + IQ_stanzas, + iq => sizzle(`iq[type="set"][to="chat.shakespeare.lit"] enable[xmlns="${Strophe.NS.PUSH}"]`, iq).length + ).pop()); + + expect(Strophe.serialize(iq)).toEqual( + ``+ + ''+ + '' + ); + _converse.connection._dataRecv(mock.createRequest($iq({ + 'to': _converse.connection.jid, + 'type': 'result', + 'id': iq.getAttribute('id') + }))); + await u.waitUntil(() => _.includes(_converse.session.get('push_enabled'), 'chat.shakespeare.lit')); + done(); + })); + + it("can be disabled", + mock.initConverse( + ['rosterGroupsFetched'], { + 'push_app_servers': [{ + 'jid': 'push-5@client.example', + 'node': 'yxs32uqsflafdk3iuqo', + 'disable': true + }] + }, async function (done, _converse) { + + const IQ_stanzas = _converse.connection.IQ_stanzas; + expect(_converse.session.get('push_enabled')).toBeFalsy(); + + await mock.waitUntilDiscoConfirmed( + _converse, + _converse.bare_jid, + [{'category': 'account', 'type':'registered'}], + ['urn:xmpp:push:0'], [], 'info'); + const stanza = await u.waitUntil( + () => _.filter(IQ_stanzas, iq => iq.querySelector('iq[type="set"] disable[xmlns="urn:xmpp:push:0"]')).pop() + ); + expect(Strophe.serialize(stanza)).toEqual( + ``+ + ''+ + '' + ); + _converse.connection._dataRecv(mock.createRequest($iq({ + 'to': _converse.connection.jid, + 'type': 'result', + 'id': stanza.getAttribute('id') + }))); + await u.waitUntil(() => _converse.session.get('push_enabled')) + done(); + })); - it("can require a secret token to be included", - mock.initConverse( - ['rosterGroupsFetched'], { - 'push_app_servers': [{ - 'jid': 'push-5@client.example', - 'node': 'yxs32uqsflafdk3iuqo', - 'secret': 'eruio234vzxc2kla-91' - }] - }, async function (done, _converse) { + it("can require a secret token to be included", + mock.initConverse( + ['rosterGroupsFetched'], { + 'push_app_servers': [{ + 'jid': 'push-5@client.example', + 'node': 'yxs32uqsflafdk3iuqo', + 'secret': 'eruio234vzxc2kla-91' + }] + }, async function (done, _converse) { - const IQ_stanzas = _converse.connection.IQ_stanzas; - expect(_converse.session.get('push_enabled')).toBeFalsy(); + const IQ_stanzas = _converse.connection.IQ_stanzas; + expect(_converse.session.get('push_enabled')).toBeFalsy(); - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.push_app_servers[0].jid, - [{'category': 'pubsub', 'type':'push'}], + await mock.waitUntilDiscoConfirmed( + _converse, _converse.push_app_servers[0].jid, + [{'category': 'pubsub', 'type':'push'}], + ['urn:xmpp:push:0'], [], 'info'); + await mock.waitUntilDiscoConfirmed( + _converse, + _converse.bare_jid, + [{'category': 'account', 'type':'registered'}], ['urn:xmpp:push:0'], [], 'info'); - await test_utils.waitUntilDiscoConfirmed( - _converse, - _converse.bare_jid, - [{'category': 'account', 'type':'registered'}], - ['urn:xmpp:push:0'], [], 'info'); - const stanza = await u.waitUntil( - () => _.filter(IQ_stanzas, iq => iq.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]')).pop() - ); - expect(Strophe.serialize(stanza)).toEqual( - ``+ - ''+ - ''+ - 'http://jabber.org/protocol/pubsub#publish-options'+ - 'eruio234vzxc2kla-91'+ - ''+ - ''+ - '' - ) - _converse.connection._dataRecv(test_utils.createRequest($iq({ - 'to': _converse.connection.jid, - 'type': 'result', - 'id': stanza.getAttribute('id') - }))); - await u.waitUntil(() => _converse.session.get('push_enabled')) - done(); - })); - }); + const stanza = await u.waitUntil( + () => _.filter(IQ_stanzas, iq => iq.querySelector('iq[type="set"] enable[xmlns="urn:xmpp:push:0"]')).pop() + ); + expect(Strophe.serialize(stanza)).toEqual( + ``+ + ''+ + ''+ + 'http://jabber.org/protocol/pubsub#publish-options'+ + 'eruio234vzxc2kla-91'+ + ''+ + ''+ + '' + ) + _converse.connection._dataRecv(mock.createRequest($iq({ + 'to': _converse.connection.jid, + 'type': 'result', + 'id': stanza.getAttribute('id') + }))); + await u.waitUntil(() => _converse.session.get('push_enabled')) + done(); + })); }); diff --git a/spec/register.js b/spec/register.js index 3ee5600d0..1dd7e1752 100644 --- a/spec/register.js +++ b/spec/register.js @@ -1,365 +1,363 @@ -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 { _, sizzle} = converse.env; - const u = converse.env.utils; +/*global mock */ - describe("The Registration Panel", function () { +const Strophe = converse.env.Strophe; +const $iq = converse.env.$iq; +const { _, sizzle} = converse.env; +const u = converse.env.utils; - it("is not available unless allow_registration=true", - mock.initConverse( - ['chatBoxesInitialized'], - { auto_login: false, - allow_registration: false }, - async function (done, _converse) { +describe("The Registration Panel", function () { - await u.waitUntil(() => _converse.chatboxviews.get('controlbox')); - const cbview = _converse.api.controlbox.get(); - expect(cbview.el.querySelectorAll('a.register-account').length).toBe(0); - done(); - })); + it("is not available unless allow_registration=true", + mock.initConverse( + ['chatBoxesInitialized'], + { auto_login: false, + allow_registration: false }, + async function (done, _converse) { - it("can be opened by clicking on the registration tab", - mock.initConverse( - ['chatBoxesInitialized'], - { auto_login: false, - allow_registration: true }, - async function (done, _converse) { + await u.waitUntil(() => _converse.chatboxviews.get('controlbox')); + const cbview = _converse.api.controlbox.get(); + expect(cbview.el.querySelectorAll('a.register-account').length).toBe(0); + done(); + })); - const toggle = document.querySelector(".toggle-controlbox"); - if (!u.isVisible(document.querySelector("#controlbox"))) { - if (!u.isVisible(toggle)) { - u.removeClass('hidden', toggle); - } - toggle.click(); + it("can be opened by clicking on the registration tab", + mock.initConverse( + ['chatBoxesInitialized'], + { auto_login: false, + allow_registration: true }, + async function (done, _converse) { + + const toggle = document.querySelector(".toggle-controlbox"); + if (!u.isVisible(document.querySelector("#controlbox"))) { + if (!u.isVisible(toggle)) { + u.removeClass('hidden', toggle); } - await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'), 300); - const cbview = _converse.chatboxviews.get('controlbox'); - const panels = cbview.el.querySelector('.controlbox-panes'); - const login = panels.firstElementChild; - const registration = panels.childNodes[1]; - const register_link = cbview.el.querySelector('a.register-account'); - expect(register_link.textContent).toBe("Create an account"); - register_link.click(); - - await u.waitUntil(() => u.isVisible(registration)); - expect(u.isVisible(login)).toBe(false); - done(); - })); - - it("allows the user to choose an XMPP provider's domain", - mock.initConverse( - ['chatBoxesInitialized'], - { auto_login: false, - discover_connection_methods: false, - allow_registration: true }, - async function (done, _converse) { - - spyOn(Strophe.Connection.prototype, 'connect'); - - await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); - const toggle = document.querySelector(".toggle-controlbox"); toggle.click(); + } + await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel'), 300); + const cbview = _converse.chatboxviews.get('controlbox'); + const panels = cbview.el.querySelector('.controlbox-panes'); + const login = panels.firstElementChild; + const registration = panels.childNodes[1]; + const register_link = cbview.el.querySelector('a.register-account'); + expect(register_link.textContent).toBe("Create an account"); + register_link.click(); - const cbview = _converse.api.controlbox.get(); - await u.waitUntil(() => u.isVisible(cbview.el)); - const registerview = cbview.registerpanel; - spyOn(registerview, 'onProviderChosen').and.callThrough(); - registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + await u.waitUntil(() => u.isVisible(registration)); + expect(u.isVisible(login)).toBe(false); + done(); + })); - // Open the register panel - cbview.el.querySelector('.toggle-register-login').click(); + it("allows the user to choose an XMPP provider's domain", + mock.initConverse( + ['chatBoxesInitialized'], + { auto_login: false, + discover_connection_methods: false, + allow_registration: true }, + async function (done, _converse) { - // Check the form layout - const form = cbview.el.querySelector('#converse-register'); - expect(form.querySelectorAll('input').length).toEqual(2); - expect(form.querySelectorAll('input')[0].getAttribute('name')).toEqual('domain'); - expect(sizzle('input:last', form).pop().getAttribute('type')).toEqual('submit'); - // Check that the input[type=domain] input is required - const submit_button = form.querySelector('input[type=submit]'); - submit_button.click(); - expect(registerview.onProviderChosen).not.toHaveBeenCalled(); + spyOn(Strophe.Connection.prototype, 'connect'); - // Check that the form is accepted if input[type=domain] has a value - form.querySelector('input[name=domain]').value = 'conversejs.org'; - submit_button.click(); - expect(registerview.onProviderChosen).toHaveBeenCalled(); - await u.waitUntil(() => _converse.connection.connect.calls.count()); - done(); - })); + await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); + const toggle = document.querySelector(".toggle-controlbox"); + toggle.click(); - it("will render a registration form as received from the XMPP provider", - mock.initConverse( - ['chatBoxesInitialized'], - { auto_login: false, - discover_connection_methods: false, - allow_registration: true }, - async function (done, _converse) { + const cbview = _converse.api.controlbox.get(); + await u.waitUntil(() => u.isVisible(cbview.el)); + const registerview = cbview.registerpanel; + spyOn(registerview, 'onProviderChosen').and.callThrough(); + registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - spyOn(Strophe.Connection.prototype, 'connect'); - await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); - const cbview = _converse.api.controlbox.get(); - cbview.el.querySelector('.toggle-register-login').click(); + // Open the register panel + cbview.el.querySelector('.toggle-register-login').click(); - const registerview = _converse.chatboxviews.get('controlbox').registerpanel; - spyOn(registerview, 'onProviderChosen').and.callThrough(); - spyOn(registerview, 'getRegistrationFields').and.callThrough(); - spyOn(registerview, 'onRegistrationFields').and.callThrough(); - spyOn(registerview, 'renderRegistrationForm').and.callThrough(); - registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + // Check the form layout + const form = cbview.el.querySelector('#converse-register'); + expect(form.querySelectorAll('input').length).toEqual(2); + expect(form.querySelectorAll('input')[0].getAttribute('name')).toEqual('domain'); + expect(sizzle('input:last', form).pop().getAttribute('type')).toEqual('submit'); + // Check that the input[type=domain] input is required + const submit_button = form.querySelector('input[type=submit]'); + submit_button.click(); + expect(registerview.onProviderChosen).not.toHaveBeenCalled(); - expect(registerview._registering).toBeFalsy(); - expect(_converse.api.connection.connected()).toBeFalsy(); - registerview.el.querySelector('input[name=domain]').value = 'conversejs.org'; - registerview.el.querySelector('input[type=submit]').click(); - expect(registerview.onProviderChosen).toHaveBeenCalled(); - expect(registerview._registering).toBeTruthy(); - await u.waitUntil(() => _converse.connection.connect.calls.count()); + // Check that the form is accepted if input[type=domain] has a value + form.querySelector('input[name=domain]').value = 'conversejs.org'; + submit_button.click(); + expect(registerview.onProviderChosen).toHaveBeenCalled(); + await u.waitUntil(() => _converse.connection.connect.calls.count()); + done(); + })); - let stanza = new Strophe.Builder("stream:features", { - 'xmlns:stream': "http://etherx.jabber.org/streams", - 'xmlns': "jabber:client" - }) - .c('register', {xmlns: "http://jabber.org/features/iq-register"}).up() - .c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"}); - _converse.connection._connect_cb(test_utils.createRequest(stanza)); + it("will render a registration form as received from the XMPP provider", + mock.initConverse( + ['chatBoxesInitialized'], + { auto_login: false, + discover_connection_methods: false, + allow_registration: true }, + async function (done, _converse) { - expect(registerview.getRegistrationFields).toHaveBeenCalled(); + spyOn(Strophe.Connection.prototype, 'connect'); + await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); + const cbview = _converse.api.controlbox.get(); + cbview.el.querySelector('.toggle-register-login').click(); - stanza = $iq({ - 'type': 'result', - 'id': 'reg1' - }).c('query', {'xmlns': 'jabber:iq:register'}) - .c('instructions') - .t('Please choose a username, password and provide your email address').up() - .c('username').up() - .c('password').up() - .c('email'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(registerview.onRegistrationFields).toHaveBeenCalled(); - expect(registerview.renderRegistrationForm).toHaveBeenCalled(); - expect(registerview.el.querySelectorAll('input').length).toBe(5); - expect(registerview.el.querySelectorAll('input[type=submit]').length).toBe(1); - expect(registerview.el.querySelectorAll('input[type=button]').length).toBe(1); - done(); - })); + const registerview = _converse.chatboxviews.get('controlbox').registerpanel; + spyOn(registerview, 'onProviderChosen').and.callThrough(); + spyOn(registerview, 'getRegistrationFields').and.callThrough(); + spyOn(registerview, 'onRegistrationFields').and.callThrough(); + spyOn(registerview, 'renderRegistrationForm').and.callThrough(); + registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - it("will set form_type to legacy and submit it as legacy", - mock.initConverse( - ['chatBoxesInitialized'], - { auto_login: false, - discover_connection_methods: false, - allow_registration: true }, - async function (done, _converse) { + expect(registerview._registering).toBeFalsy(); + expect(_converse.api.connection.connected()).toBeFalsy(); + registerview.el.querySelector('input[name=domain]').value = 'conversejs.org'; + registerview.el.querySelector('input[type=submit]').click(); + expect(registerview.onProviderChosen).toHaveBeenCalled(); + expect(registerview._registering).toBeTruthy(); + await u.waitUntil(() => _converse.connection.connect.calls.count()); - await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); - const toggle = document.querySelector(".toggle-controlbox"); - if (!u.isVisible(document.querySelector("#controlbox"))) { - if (!u.isVisible(toggle)) { - u.removeClass('hidden', toggle); - } - toggle.click(); + let stanza = new Strophe.Builder("stream:features", { + 'xmlns:stream': "http://etherx.jabber.org/streams", + 'xmlns': "jabber:client" + }) + .c('register', {xmlns: "http://jabber.org/features/iq-register"}).up() + .c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"}); + _converse.connection._connect_cb(mock.createRequest(stanza)); + + expect(registerview.getRegistrationFields).toHaveBeenCalled(); + + stanza = $iq({ + 'type': 'result', + 'id': 'reg1' + }).c('query', {'xmlns': 'jabber:iq:register'}) + .c('instructions') + .t('Please choose a username, password and provide your email address').up() + .c('username').up() + .c('password').up() + .c('email'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(registerview.onRegistrationFields).toHaveBeenCalled(); + expect(registerview.renderRegistrationForm).toHaveBeenCalled(); + expect(registerview.el.querySelectorAll('input').length).toBe(5); + expect(registerview.el.querySelectorAll('input[type=submit]').length).toBe(1); + expect(registerview.el.querySelectorAll('input[type=button]').length).toBe(1); + done(); + })); + + it("will set form_type to legacy and submit it as legacy", + mock.initConverse( + ['chatBoxesInitialized'], + { auto_login: false, + discover_connection_methods: false, + allow_registration: true }, + async function (done, _converse) { + + await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); + const toggle = document.querySelector(".toggle-controlbox"); + if (!u.isVisible(document.querySelector("#controlbox"))) { + if (!u.isVisible(toggle)) { + u.removeClass('hidden', toggle); } - const cbview = _converse.api.controlbox.get(); - cbview.el.querySelector('.toggle-register-login').click(); + toggle.click(); + } + const cbview = _converse.api.controlbox.get(); + cbview.el.querySelector('.toggle-register-login').click(); - const registerview = cbview.registerpanel; - spyOn(registerview, 'onProviderChosen').and.callThrough(); - spyOn(registerview, 'getRegistrationFields').and.callThrough(); - spyOn(registerview, 'onRegistrationFields').and.callThrough(); - spyOn(registerview, 'renderRegistrationForm').and.callThrough(); - registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + const registerview = cbview.registerpanel; + spyOn(registerview, 'onProviderChosen').and.callThrough(); + spyOn(registerview, 'getRegistrationFields').and.callThrough(); + spyOn(registerview, 'onRegistrationFields').and.callThrough(); + spyOn(registerview, 'renderRegistrationForm').and.callThrough(); + registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - registerview.el.querySelector('input[name=domain]').value = 'conversejs.org'; - registerview.el.querySelector('input[type=submit]').click(); + registerview.el.querySelector('input[name=domain]').value = 'conversejs.org'; + registerview.el.querySelector('input[type=submit]').click(); - let stanza = new Strophe.Builder("stream:features", { - 'xmlns:stream': "http://etherx.jabber.org/streams", - 'xmlns': "jabber:client" - }) - .c('register', {xmlns: "http://jabber.org/features/iq-register"}).up() - .c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"}); - _converse.connection._connect_cb(test_utils.createRequest(stanza)); - stanza = $iq({ - 'type': 'result', - 'id': 'reg1' - }).c('query', {'xmlns': 'jabber:iq:register'}) - .c('instructions') - .t('Please choose a username, password and provide your email address').up() - .c('username').up() - .c('password').up() - .c('email'); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(registerview.form_type).toBe('legacy'); + let stanza = new Strophe.Builder("stream:features", { + 'xmlns:stream': "http://etherx.jabber.org/streams", + 'xmlns': "jabber:client" + }) + .c('register', {xmlns: "http://jabber.org/features/iq-register"}).up() + .c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"}); + _converse.connection._connect_cb(mock.createRequest(stanza)); + stanza = $iq({ + 'type': 'result', + 'id': 'reg1' + }).c('query', {'xmlns': 'jabber:iq:register'}) + .c('instructions') + .t('Please choose a username, password and provide your email address').up() + .c('username').up() + .c('password').up() + .c('email'); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(registerview.form_type).toBe('legacy'); - registerview.el.querySelector('input[name=username]').value = 'testusername'; - registerview.el.querySelector('input[name=password]').value = 'testpassword'; - registerview.el.querySelector('input[name=email]').value = 'test@email.local'; + registerview.el.querySelector('input[name=username]').value = 'testusername'; + registerview.el.querySelector('input[name=password]').value = 'testpassword'; + registerview.el.querySelector('input[name=email]').value = 'test@email.local'; - spyOn(_converse.connection, 'send'); - registerview.el.querySelector('input[type=submit]').click(); + spyOn(_converse.connection, 'send'); + registerview.el.querySelector('input[type=submit]').click(); - expect(_converse.connection.send).toHaveBeenCalled(); - stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); - expect(stanza.querySelector('query').childNodes.length).toBe(3); - expect(stanza.querySelector('query').firstElementChild.tagName).toBe('username'); + expect(_converse.connection.send).toHaveBeenCalled(); + stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); + expect(stanza.querySelector('query').childNodes.length).toBe(3); + expect(stanza.querySelector('query').firstElementChild.tagName).toBe('username'); - delete _converse.connection; - done(); - })); + delete _converse.connection; + done(); + })); - it("will set form_type to xform and submit it as xform", - mock.initConverse( - ['chatBoxesInitialized'], - { auto_login: false, - discover_connection_methods: false, - allow_registration: true }, - async function (done, _converse) { + it("will set form_type to xform and submit it as xform", + mock.initConverse( + ['chatBoxesInitialized'], + { auto_login: false, + discover_connection_methods: false, + allow_registration: true }, + async function (done, _converse) { - await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); - const toggle = document.querySelector(".toggle-controlbox"); - if (!u.isVisible(document.querySelector("#controlbox"))) { - if (!u.isVisible(toggle)) { - u.removeClass('hidden', toggle); - } - toggle.click(); + await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); + const toggle = document.querySelector(".toggle-controlbox"); + if (!u.isVisible(document.querySelector("#controlbox"))) { + if (!u.isVisible(toggle)) { + u.removeClass('hidden', toggle); } - const cbview = _converse.api.controlbox.get(); - cbview.el.querySelector('.toggle-register-login').click(); - const registerview = _converse.chatboxviews.get('controlbox').registerpanel; - spyOn(registerview, 'onProviderChosen').and.callThrough(); - spyOn(registerview, 'getRegistrationFields').and.callThrough(); - spyOn(registerview, 'onRegistrationFields').and.callThrough(); - spyOn(registerview, 'renderRegistrationForm').and.callThrough(); - registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + toggle.click(); + } + const cbview = _converse.api.controlbox.get(); + cbview.el.querySelector('.toggle-register-login').click(); + const registerview = _converse.chatboxviews.get('controlbox').registerpanel; + spyOn(registerview, 'onProviderChosen').and.callThrough(); + spyOn(registerview, 'getRegistrationFields').and.callThrough(); + spyOn(registerview, 'onRegistrationFields').and.callThrough(); + spyOn(registerview, 'renderRegistrationForm').and.callThrough(); + registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - registerview.el.querySelector('input[name=domain]').value = 'conversejs.org'; - registerview.el.querySelector('input[type=submit]').click(); + registerview.el.querySelector('input[name=domain]').value = 'conversejs.org'; + registerview.el.querySelector('input[type=submit]').click(); - let stanza = new Strophe.Builder("stream:features", { - 'xmlns:stream': "http://etherx.jabber.org/streams", - 'xmlns': "jabber:client" - }) - .c('register', {xmlns: "http://jabber.org/features/iq-register"}).up() - .c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"}); - _converse.connection._connect_cb(test_utils.createRequest(stanza)); - stanza = $iq({ - 'type': 'result', - 'id': 'reg1' - }).c('query', {'xmlns': 'jabber:iq:register'}) - .c('instructions') - .t('Using xform data').up() - .c('x', { 'xmlns': 'jabber:x:data', 'type': 'form' }) - .c('instructions').t('xform instructions').up() - .c('field', {'type': 'text-single', 'var': 'username'}).c('required').up().up() - .c('field', {'type': 'text-private', 'var': 'password'}).c('required').up().up() - .c('field', {'type': 'text-single', 'var': 'email'}).c('required').up().up(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(registerview.form_type).toBe('xform'); + let stanza = new Strophe.Builder("stream:features", { + 'xmlns:stream': "http://etherx.jabber.org/streams", + 'xmlns': "jabber:client" + }) + .c('register', {xmlns: "http://jabber.org/features/iq-register"}).up() + .c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"}); + _converse.connection._connect_cb(mock.createRequest(stanza)); + stanza = $iq({ + 'type': 'result', + 'id': 'reg1' + }).c('query', {'xmlns': 'jabber:iq:register'}) + .c('instructions') + .t('Using xform data').up() + .c('x', { 'xmlns': 'jabber:x:data', 'type': 'form' }) + .c('instructions').t('xform instructions').up() + .c('field', {'type': 'text-single', 'var': 'username'}).c('required').up().up() + .c('field', {'type': 'text-private', 'var': 'password'}).c('required').up().up() + .c('field', {'type': 'text-single', 'var': 'email'}).c('required').up().up(); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(registerview.form_type).toBe('xform'); - registerview.el.querySelector('input[name=username]').value = 'testusername'; - registerview.el.querySelector('input[name=password]').value = 'testpassword'; - registerview.el.querySelector('input[name=email]').value = 'test@email.local'; + registerview.el.querySelector('input[name=username]').value = 'testusername'; + registerview.el.querySelector('input[name=password]').value = 'testpassword'; + registerview.el.querySelector('input[name=email]').value = 'test@email.local'; - spyOn(_converse.connection, 'send'); + spyOn(_converse.connection, 'send'); - registerview.el.querySelector('input[type=submit]').click(); + registerview.el.querySelector('input[type=submit]').click(); - expect(_converse.connection.send).toHaveBeenCalled(); - stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); - expect(Strophe.serialize(stanza).toLocaleString().trim().replace(/(\n|\s{2,})/g, '')).toEqual( - ''+ - ''+ - ''+ - ''+ - 'testusername'+ - ''+ - ''+ - 'testpassword'+ - ''+ - ''+ - 'test@email.local'+ - ''+ - ''+ - ''+ - '' - ); + expect(_converse.connection.send).toHaveBeenCalled(); + stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); + expect(Strophe.serialize(stanza).toLocaleString().trim().replace(/(\n|\s{2,})/g, '')).toEqual( + ''+ + ''+ + ''+ + ''+ + 'testusername'+ + ''+ + ''+ + 'testpassword'+ + ''+ + ''+ + 'test@email.local'+ + ''+ + ''+ + ''+ + '' + ); - delete _converse.connection; - done(); - })); + delete _converse.connection; + done(); + })); - it("renders the account registration form", - mock.initConverse( - ['chatBoxesInitialized'], - { auto_login: false, - view_mode: 'fullscreen', - discover_connection_methods: false, - allow_registration: true }, - async function (done, _converse) { + it("renders the account registration form", + mock.initConverse( + ['chatBoxesInitialized'], + { auto_login: false, + view_mode: 'fullscreen', + discover_connection_methods: false, + allow_registration: true }, + async function (done, _converse) { - await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); - const toggle = document.querySelector(".toggle-controlbox"); - if (!u.isVisible(document.querySelector("#controlbox"))) { - if (!u.isVisible(toggle)) { - u.removeClass('hidden', toggle); - } - toggle.click(); + await u.waitUntil(() => _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel')); + const toggle = document.querySelector(".toggle-controlbox"); + if (!u.isVisible(document.querySelector("#controlbox"))) { + if (!u.isVisible(toggle)) { + u.removeClass('hidden', toggle); } - const cbview = _converse.chatboxviews.get('controlbox'); - cbview.el.querySelector('.toggle-register-login').click(); - const registerview = _converse.chatboxviews.get('controlbox').registerpanel; - spyOn(registerview, 'onProviderChosen').and.callThrough(); - spyOn(registerview, 'getRegistrationFields').and.callThrough(); - spyOn(registerview, 'onRegistrationFields').and.callThrough(); - spyOn(registerview, 'renderRegistrationForm').and.callThrough(); - registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called + toggle.click(); + } + const cbview = _converse.chatboxviews.get('controlbox'); + cbview.el.querySelector('.toggle-register-login').click(); + const registerview = _converse.chatboxviews.get('controlbox').registerpanel; + spyOn(registerview, 'onProviderChosen').and.callThrough(); + spyOn(registerview, 'getRegistrationFields').and.callThrough(); + spyOn(registerview, 'onRegistrationFields').and.callThrough(); + spyOn(registerview, 'renderRegistrationForm').and.callThrough(); + registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called - registerview.el.querySelector('input[name=domain]').value = 'conversejs.org'; - registerview.el.querySelector('input[type=submit]').click(); + registerview.el.querySelector('input[name=domain]').value = 'conversejs.org'; + registerview.el.querySelector('input[type=submit]').click(); - let stanza = new Strophe.Builder("stream:features", { - 'xmlns:stream': "http://etherx.jabber.org/streams", - 'xmlns': "jabber:client" - }) - .c('register', {xmlns: "http://jabber.org/features/iq-register"}).up() - .c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"}); - _converse.connection._connect_cb(test_utils.createRequest(stanza)); + let stanza = new Strophe.Builder("stream:features", { + 'xmlns:stream': "http://etherx.jabber.org/streams", + 'xmlns': "jabber:client" + }) + .c('register', {xmlns: "http://jabber.org/features/iq-register"}).up() + .c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"}); + _converse.connection._connect_cb(mock.createRequest(stanza)); - stanza = u.toStanza(` - - - - Choose a username and password to register with this server - urn:xmpp:captcha - - - conversations.im - 15376320046808160053 - ad1e0d50-5adb-4397-a997-5feab56fe418:sendIQ - - - cid:sha1+2df8c1b366f1e90ce60354f97d1fe75237290b8a@bob.xmpp.org - - - - - iVBORw0KGgoAAAANSUhEUgAAALQAAAA8BAMAAAA9AI20AAAAMFBMVEX///8AAADf39+fn59fX19/f3+/v78fHx8/Pz9PT08bGxsvLy9jY2NTU1MXFxcnJyc84bkWAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAERUlEQVRYhe1WTXMaRxDdDxY4JWpYvDinpVyxdATLin0MiRLlCHEi+7hYUcVHTSI7urhK6yr5//gn5N/4Z7inX89+CQkTcFUO6gOwS8/r7tdvesbzvoT5ROR5JJ9bB97xAK22XWAY1WznlnUr7QaAzSOsWufXQ6wH/FmO60b4D936LJr8TWRwW4SNgOsodZr8m4vZUoRt2xZ3xHXgna1FCE5+f5aWwPU//bXgg8eHjyqPp4aXJeOlwLUIt0O39zOvPWW3WfHmCCkli816FxlK0rnFGKZ484dN+eIXsw1R+G+JfjwgOpMnm+r5SxA63gS2Q8MchO1RLN8jSn4W4F5OPed2evhTthKLG3bsfjLL874XGBpWHLrU0953i/ev7JsfViHbhsWSQTunJDOppeAe0hVGokJUHBOphmjrbBlgabviJKXbIP0B//gKSBHZh2rvJnQp3wsapMFz+VsTPNhPr0Hn9N57YOjywaxFSU6S79fUF39KBDgnt6yjZOeSffk+4IXDZovbQl9E96m34EzQKMepQcbzijAGiBmDsO+LaqzqG3m3kEf+DQ2mY+vdk5c2n2Iaj5QGi6n59FHDmcuP4t8MGlRaF39P6ENyIaB2EXdpjLnQq9IgdVxfax3ilBc10u4gowX9K6BaKiZNmCC7CF/WpkJvWxN00OjuoqGYLqAnpILLE68Ymrt9M0S9hcznUJ8RykdlLalUfFaDjvA8pT2kxmsl5fuMaM6mSWUpUhDoudSucdhiZFDwphEHwsMwhEpH0jsm+/UBK2wCzFIiitalN7YjWkyIBgTNPgpDXX4rjk4UH+yPPgfK4HNZQCP/KZ0fGnrnKl8+pXl3X7FwZuwNUdwDGO+BjPUn6XaKtbkm+MJ6vtaXSnIz6wBT/m+VvZNIhz7ayabQLSeRQDmYkjt0KlmHDa555v9DzFxx+CCvCG4K3dbx6mTYtfPs1Dgdh0i3W+cl4lnnhblMKKBBA23X1Ezc3E5ZoPS5KHjPiU1rKTviYe1fTsa6e3UwXGWI4ykB8uiGqkmA6Cbf3K4JTH3LOBlbX+yPWll57LKVeH8CTEvyVPV2TXL8kPnPqtA51CaFYxOH2rJoZunSnvsSj48WiaDccl6KEgiMSarITsa+rWWBnqFloYlT1qWW2GKw9nPSbEvoVHFst967XgNQjxdA66Q6VFEUh488xfaSo7cHB52XYzA4eRlVteeT8ostWfuPea0oF6MwzlwgZE9gQI+uUV0gzK+WlpUrNI8juhhX/OyNwZnRrsDfxOqS1aDR+gC6NUPvJpvQeVZ9eiNr9aDUuddY3bLnA4tH4r/49UboznH1ia8PV/uP3WUB3dxtzj1uxfDZgbEbZx17Itwrf0Jyc8N4en+5dhivtKeYjGJ8yXgUzKvSU/uWJZmsuAYtseDku+K3zMHi4lC1h0suPmtZaEp2tm3hEV2lXwb6zu7szv6f9glF5rPGT5xR7AAAAABJRU5ErkJggg== - You need a client that supports x:data and CAPTCHA to register - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - expect(registerview.form_type).toBe('xform'); - expect(registerview.el.querySelectorAll('#converse-register input[required="required"]').length).toBe(3); - // Hide the controlbox so that we can see whether the test - // passed or failed - u.addClass('hidden', _converse.chatboxviews.get('controlbox').el); - delete _converse.connection; - done(); - })); - }); + stanza = u.toStanza(` + + + + Choose a username and password to register with this server + urn:xmpp:captcha + + + conversations.im + 15376320046808160053 + ad1e0d50-5adb-4397-a997-5feab56fe418:sendIQ + + + cid:sha1+2df8c1b366f1e90ce60354f97d1fe75237290b8a@bob.xmpp.org + + + + + iVBORw0KGgoAAAANSUhEUgAAALQAAAA8BAMAAAA9AI20AAAAMFBMVEX///8AAADf39+fn59fX19/f3+/v78fHx8/Pz9PT08bGxsvLy9jY2NTU1MXFxcnJyc84bkWAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAERUlEQVRYhe1WTXMaRxDdDxY4JWpYvDinpVyxdATLin0MiRLlCHEi+7hYUcVHTSI7urhK6yr5//gn5N/4Z7inX89+CQkTcFUO6gOwS8/r7tdvesbzvoT5ROR5JJ9bB97xAK22XWAY1WznlnUr7QaAzSOsWufXQ6wH/FmO60b4D936LJr8TWRwW4SNgOsodZr8m4vZUoRt2xZ3xHXgna1FCE5+f5aWwPU//bXgg8eHjyqPp4aXJeOlwLUIt0O39zOvPWW3WfHmCCkli816FxlK0rnFGKZ484dN+eIXsw1R+G+JfjwgOpMnm+r5SxA63gS2Q8MchO1RLN8jSn4W4F5OPed2evhTthKLG3bsfjLL874XGBpWHLrU0953i/ev7JsfViHbhsWSQTunJDOppeAe0hVGokJUHBOphmjrbBlgabviJKXbIP0B//gKSBHZh2rvJnQp3wsapMFz+VsTPNhPr0Hn9N57YOjywaxFSU6S79fUF39KBDgnt6yjZOeSffk+4IXDZovbQl9E96m34EzQKMepQcbzijAGiBmDsO+LaqzqG3m3kEf+DQ2mY+vdk5c2n2Iaj5QGi6n59FHDmcuP4t8MGlRaF39P6ENyIaB2EXdpjLnQq9IgdVxfax3ilBc10u4gowX9K6BaKiZNmCC7CF/WpkJvWxN00OjuoqGYLqAnpILLE68Ymrt9M0S9hcznUJ8RykdlLalUfFaDjvA8pT2kxmsl5fuMaM6mSWUpUhDoudSucdhiZFDwphEHwsMwhEpH0jsm+/UBK2wCzFIiitalN7YjWkyIBgTNPgpDXX4rjk4UH+yPPgfK4HNZQCP/KZ0fGnrnKl8+pXl3X7FwZuwNUdwDGO+BjPUn6XaKtbkm+MJ6vtaXSnIz6wBT/m+VvZNIhz7ayabQLSeRQDmYkjt0KlmHDa555v9DzFxx+CCvCG4K3dbx6mTYtfPs1Dgdh0i3W+cl4lnnhblMKKBBA23X1Ezc3E5ZoPS5KHjPiU1rKTviYe1fTsa6e3UwXGWI4ykB8uiGqkmA6Cbf3K4JTH3LOBlbX+yPWll57LKVeH8CTEvyVPV2TXL8kPnPqtA51CaFYxOH2rJoZunSnvsSj48WiaDccl6KEgiMSarITsa+rWWBnqFloYlT1qWW2GKw9nPSbEvoVHFst967XgNQjxdA66Q6VFEUh488xfaSo7cHB52XYzA4eRlVteeT8ostWfuPea0oF6MwzlwgZE9gQI+uUV0gzK+WlpUrNI8juhhX/OyNwZnRrsDfxOqS1aDR+gC6NUPvJpvQeVZ9eiNr9aDUuddY3bLnA4tH4r/49UboznH1ia8PV/uP3WUB3dxtzj1uxfDZgbEbZx17Itwrf0Jyc8N4en+5dhivtKeYjGJ8yXgUzKvSU/uWJZmsuAYtseDku+K3zMHi4lC1h0suPmtZaEp2tm3hEV2lXwb6zu7szv6f9glF5rPGT5xR7AAAAABJRU5ErkJggg== + You need a client that supports x:data and CAPTCHA to register + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + expect(registerview.form_type).toBe('xform'); + expect(registerview.el.querySelectorAll('#converse-register input[required="required"]').length).toBe(3); + // Hide the controlbox so that we can see whether the test + // passed or failed + u.addClass('hidden', _converse.chatboxviews.get('controlbox').el); + delete _converse.connection; + done(); + })); }); diff --git a/spec/retractions.js b/spec/retractions.js index ccabd6d95..c8a4bad00 100644 --- a/spec/retractions.js +++ b/spec/retractions.js @@ -1,1155 +1,1153 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const { Strophe, $iq } = converse.env; - const u = converse.env.utils; +/*global mock */ + +const { Strophe, $iq } = converse.env; +const u = converse.env.utils; - async function sendAndThenRetractMessage (_converse, view) { - view.model.sendMessage('hello world'); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text').length === 1); - const msg_obj = view.model.messages.last(); - const reflection_stanza = u.toStanza(` - - ${msg_obj.get('message')} - - - `); - await view.model.queueMessage(reflection_stanza); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); +async function sendAndThenRetractMessage (_converse, view) { + view.model.sendMessage('hello world'); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__text').length === 1); + const msg_obj = view.model.messages.last(); + const reflection_stanza = u.toStanza(` + + ${msg_obj.get('message')} + + + `); + await view.model.queueMessage(reflection_stanza); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); - const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract')); - retract_button.click(); - await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal'))); - const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]'); - submit_button.click(); - const sent_stanzas = _converse.connection.sent_stanzas; - return u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop()); - } + const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract')); + retract_button.click(); + await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal'))); + const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]'); + submit_button.click(); + const sent_stanzas = _converse.connection.sent_stanzas; + return u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop()); +} - describe("Message Retractions", function () { +describe("Message Retractions", function () { - describe("A groupchat message retraction", function () { + describe("A groupchat message retraction", function () { - it("is not applied if it's not from the right author", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("is not applied if it's not from the right author", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const received_stanza = u.toStanza(` - - Hello world - - - `); - const view = _converse.api.chatviews.get(muc_jid); - await view.model.queueMessage(received_stanza); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); - expect(view.model.messages.at(0).get('retracted')).toBeFalsy(); - expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy(); + const received_stanza = u.toStanza(` + + Hello world + + + `); + const view = _converse.api.chatviews.get(muc_jid); + await view.model.queueMessage(received_stanza); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); + expect(view.model.messages.at(0).get('retracted')).toBeFalsy(); + expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy(); - const retraction_stanza = u.toStanza(` - - - - - - `); - spyOn(view.model, 'handleRetraction').and.callThrough(); + const retraction_stanza = u.toStanza(` + + + + + + `); + spyOn(view.model, 'handleRetraction').and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza)); - await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1); - expect(await view.model.handleRetraction.calls.first().returnValue).toBe(true); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.model.messages.length).toBe(2); - expect(view.model.messages.at(1).get('retracted')).toBeTruthy(); - expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy(); - expect(view.model.messages.at(1).get('dangling_retraction')).toBe(true); + _converse.connection._dataRecv(mock.createRequest(retraction_stanza)); + await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1); + expect(await view.model.handleRetraction.calls.first().returnValue).toBe(true); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.model.messages.length).toBe(2); + expect(view.model.messages.at(1).get('retracted')).toBeTruthy(); + expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy(); + expect(view.model.messages.at(1).get('dangling_retraction')).toBe(true); - expect(view.model.messages.at(0).get('retracted')).toBeFalsy(); - expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy(); - done(); - })); + expect(view.model.messages.at(0).get('retracted')).toBeFalsy(); + expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy(); + done(); + })); - it("can be received before the message it pertains to", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("can be received before the message it pertains to", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - const date = (new Date()).toISOString(); - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const date = (new Date()).toISOString(); + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const retraction_stanza = u.toStanza(` - - - - - - `); - const view = _converse.api.chatviews.get(muc_jid); - spyOn(converse.env.log, 'warn'); - spyOn(view.model, 'handleRetraction').and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza)); + const retraction_stanza = u.toStanza(` + + + + + + `); + const view = _converse.api.chatviews.get(muc_jid); + spyOn(converse.env.log, 'warn'); + spyOn(view.model, 'handleRetraction').and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(retraction_stanza)); - await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1); - await u.waitUntil(() => view.model.messages.length === 1); - expect(await view.model.handleRetraction.calls.first().returnValue).toBe(true); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('retracted')).toBeTruthy(); - expect(view.model.messages.at(0).get('dangling_retraction')).toBe(true); + await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1); + await u.waitUntil(() => view.model.messages.length === 1); + expect(await view.model.handleRetraction.calls.first().returnValue).toBe(true); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('retracted')).toBeTruthy(); + expect(view.model.messages.at(0).get('dangling_retraction')).toBe(true); - const received_stanza = u.toStanza(` - - Hello world - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(received_stanza)); - await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2); + const received_stanza = u.toStanza(` + + Hello world + + + + + `); + _converse.connection._dataRecv(mock.createRequest(received_stanza)); + await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); - expect(view.model.messages.length).toBe(1); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); + expect(view.model.messages.length).toBe(1); - const message = view.model.messages.at(0) - expect(message.get('retracted')).toBeTruthy(); - expect(message.get('dangling_retraction')).toBe(false); - expect(message.get('origin_id')).toBe('origin-id-1'); - expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1'); - expect(message.get('time')).toBe(date); - expect(message.get('type')).toBe('groupchat'); - expect(await view.model.handleRetraction.calls.all().pop().returnValue).toBe(true); - done(); - })); - }); + const message = view.model.messages.at(0) + expect(message.get('retracted')).toBeTruthy(); + expect(message.get('dangling_retraction')).toBe(false); + expect(message.get('origin_id')).toBe('origin-id-1'); + expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1'); + expect(message.get('time')).toBe(date); + expect(message.get('type')).toBe('groupchat'); + expect(await view.model.handleRetraction.calls.all().pop().returnValue).toBe(true); + done(); + })); + }); - describe("A groupchat message moderator retraction", function () { + describe("A groupchat message moderator retraction", function () { - it("can be received before the message it pertains to", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("can be received before the message it pertains to", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - const date = (new Date()).toISOString(); - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const retraction_stanza = u.toStanza(` - - - - - Insults - - - - `); - const view = _converse.api.chatviews.get(muc_jid); - spyOn(converse.env.log, 'warn'); - spyOn(view.model, 'handleModeration').and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza)); - - await u.waitUntil(() => view.model.handleModeration.calls.count() === 1); - await u.waitUntil(() => view.model.messages.length === 1); - expect(await view.model.handleModeration.calls.first().returnValue).toBe(true); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); - expect(view.model.messages.at(0).get('dangling_moderation')).toBe(true); - - const received_stanza = u.toStanza(` - - Hello world - - - - - `); - - _converse.connection._dataRecv(test_utils.createRequest(received_stanza)); - await u.waitUntil(() => view.model.handleModeration.calls.count() === 2); - - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.model.messages.length).toBe(1); - - const message = view.model.messages.at(0) - expect(message.get('moderated')).toBe('retracted'); - expect(message.get('dangling_moderation')).toBe(false); - expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1'); - expect(message.get('time')).toBe(date); - expect(message.get('type')).toBe('groupchat'); - expect(await view.model.handleModeration.calls.all().pop().returnValue).toBe(true); - done(); - })); - }); - - - describe("A message retraction", function () { - - it("can be received before the message it pertains to", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const date = (new Date()).toISOString(); - 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'; - const view = await test_utils.openChatBoxFor(_converse, contact_jid); - spyOn(view.model, 'handleRetraction').and.callThrough(); - - const retraction_stanza = u.toStanza(` - - + const date = (new Date()).toISOString(); + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const retraction_stanza = u.toStanza(` + + + - - - `); + Insults + + + + `); + const view = _converse.api.chatviews.get(muc_jid); + spyOn(converse.env.log, 'warn'); + spyOn(view.model, 'handleModeration').and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(retraction_stanza)); - const promise = new Promise(resolve => _converse.api.listen.on('messageAdded', resolve)); - _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza)); - await u.waitUntil(() => view.model.messages.length === 1); - await promise; - const message = view.model.messages.at(0); - expect(message.get('dangling_retraction')).toBe(true); - expect(message.get('is_ephemeral')).toBe(false); - expect(message.get('retracted')).toBeTruthy(); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); + await u.waitUntil(() => view.model.handleModeration.calls.count() === 1); + await u.waitUntil(() => view.model.messages.length === 1); + expect(await view.model.handleModeration.calls.first().returnValue).toBe(true); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); + expect(view.model.messages.at(0).get('dangling_moderation')).toBe(true); - const stanza = u.toStanza(` - - Hello world - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2); - expect(view.model.messages.length).toBe(1); - expect(message.get('retracted')).toBeTruthy(); - expect(message.get('dangling_retraction')).toBe(false); - expect(message.get('origin_id')).toBe('2e972ea0-0050-44b7-a830-f6638a2595b3'); - expect(message.get('time')).toBe(date); - expect(message.get('type')).toBe('chat'); - done(); - })); - }); + const received_stanza = u.toStanza(` + + Hello world + + + - describe("A Received Chat Message", function () { + `); - it("can be followed up by a retraction", + _converse.connection._dataRecv(mock.createRequest(received_stanza)); + await u.waitUntil(() => view.model.handleModeration.calls.count() === 2); + + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.model.messages.length).toBe(1); + + const message = view.model.messages.at(0) + expect(message.get('moderated')).toBe('retracted'); + expect(message.get('dangling_moderation')).toBe(false); + expect(message.get(`stanza_id ${muc_jid}`)).toBe('stanza-id-1'); + expect(message.get('time')).toBe(date); + expect(message.get('type')).toBe('groupchat'); + expect(await view.model.handleModeration.calls.all().pop().returnValue).toBe(true); + done(); + })); + }); + + + describe("A message retraction", function () { + + it("can be received before the message it pertains to", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const date = (new Date()).toISOString(); + 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'; + const view = await mock.openChatBoxFor(_converse, contact_jid); + spyOn(view.model, 'handleRetraction').and.callThrough(); + + const retraction_stanza = u.toStanza(` + + + + + + `); + + const promise = new Promise(resolve => _converse.api.listen.on('messageAdded', resolve)); + _converse.connection._dataRecv(mock.createRequest(retraction_stanza)); + await u.waitUntil(() => view.model.messages.length === 1); + await promise; + const message = view.model.messages.at(0); + expect(message.get('dangling_retraction')).toBe(true); + expect(message.get('is_ephemeral')).toBe(false); + expect(message.get('retracted')).toBeTruthy(); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(0); + + const stanza = u.toStanza(` + + Hello world + + + + + `); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2); + expect(view.model.messages.length).toBe(1); + expect(message.get('retracted')).toBeTruthy(); + expect(message.get('dangling_retraction')).toBe(false); + expect(message.get('origin_id')).toBe('2e972ea0-0050-44b7-a830-f6638a2595b3'); + expect(message.get('time')).toBe(date); + expect(message.get('type')).toBe('chat'); + done(); + })); + }); + + describe("A Received Chat Message", function () { + + it("can be followed up by a retraction", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + 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'; + const view = await mock.openChatBoxFor(_converse, contact_jid); + + let stanza = u.toStanza(` + + 😊 + + + + `); + + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.messages.length === 1); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); + + stanza = u.toStanza(` + + This message will be retracted + + + + `); + + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => view.model.messages.length === 2); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2); + + const retraction_stanza = u.toStanza(` + + + + + + `); + _converse.connection._dataRecv(mock.createRequest(retraction_stanza)); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); + + expect(view.model.messages.length).toBe(2); + + const message = view.model.messages.at(1); + expect(message.get('retracted')).toBeTruthy(); + expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); + const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message'); + expect(msg_el.textContent.trim()).toBe('Mercutio has removed this message'); + expect(u.hasClass('chat-msg--followup', view.el.querySelector('.chat-msg--retracted'))).toBe(true); + done(); + })); + }); + + describe("A Sent Chat Message", function () { + + it("can be retracted by its author", + 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'; + const view = await mock.openChatBoxFor(_converse, contact_jid); + + view.model.sendMessage('hello world'); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); + + const message = view.model.messages.at(0); + expect(view.model.messages.length).toBe(1); + expect(message.get('retracted')).toBeFalsy(); + expect(message.get('editable')).toBeTruthy(); + + + const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract')); + retract_button.click(); + await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal'))); + const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]'); + submit_button.click(); + + const sent_stanzas = _converse.connection.sent_stanzas; + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); + + const msg_obj = view.model.messages.at(0); + const retraction_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop()); + expect(Strophe.serialize(retraction_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``+ + ``); + + expect(view.model.messages.length).toBe(1); + expect(message.get('retracted')).toBeTruthy(); + expect(message.get('editable')).toBeFalsy(); + expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); + const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message'); + expect(el.textContent.trim()).toBe('Romeo Montague has removed this message'); + done(); + })); + }); + + + describe("A Received Groupchat Message", function () { + + it("can be followed up by a retraction by the author", mock.initConverse( ['rosterGroupsFetched', 'chatBoxesFetched'], {}, 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'; - const view = await test_utils.openChatBoxFor(_converse, contact_jid); + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - let stanza = u.toStanza(` - - 😊 - - - - `); + const received_stanza = u.toStanza(` + + Hello world + + + + `); + const view = _converse.api.chatviews.get(muc_jid); + await view.model.queueMessage(received_stanza); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); + expect(view.model.messages.at(0).get('retracted')).toBeFalsy(); + expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.messages.length === 1); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); + const retraction_stanza = u.toStanza(` + + + + + + `); + _converse.connection._dataRecv(mock.createRequest(retraction_stanza)); - stanza = u.toStanza(` - - This message will be retracted - - - - `); - - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => view.model.messages.length === 2); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2); - - const retraction_stanza = u.toStanza(` - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); - - expect(view.model.messages.length).toBe(2); - - const message = view.model.messages.at(1); - expect(message.get('retracted')).toBeTruthy(); - expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); - const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message'); - expect(msg_el.textContent.trim()).toBe('Mercutio has removed this message'); - expect(u.hasClass('chat-msg--followup', view.el.querySelector('.chat-msg--retracted'))).toBe(true); - done(); - })); - }); - - describe("A Sent Chat Message", function () { - - it("can be retracted by its author", - 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'; - const view = await test_utils.openChatBoxFor(_converse, contact_jid); - - view.model.sendMessage('hello world'); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); - - const message = view.model.messages.at(0); - expect(view.model.messages.length).toBe(1); - expect(message.get('retracted')).toBeFalsy(); - expect(message.get('editable')).toBeTruthy(); + // We opportunistically save the message as retracted, even before receiving the retraction message + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('retracted')).toBeTruthy(); + expect(view.model.messages.at(0).get('editable')).toBe(false); + expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); + const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message'); + expect(msg_el.textContent.trim()).toBe('eve has removed this message'); + expect(msg_el.querySelector('.chat-msg--retracted q')).toBe(null); + done(); + })); - const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract')); - retract_button.click(); - await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal'))); - const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]'); - submit_button.click(); + it("can be retracted by a moderator, with the IQ response received before the retraction message", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - const sent_stanzas = _converse.connection.sent_stanzas; - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const msg_obj = view.model.messages.at(0); - const retraction_stanza = await u.waitUntil(() => sent_stanzas.filter(s => s.querySelector('message apply-to[xmlns="urn:xmpp:fasten:0"]')).pop()); - expect(Strophe.serialize(retraction_stanza)).toBe( - ``+ - ``+ - ``+ + const view = _converse.api.chatviews.get(muc_jid); + const occupant = view.model.getOwnOccupant(); + expect(occupant.get('role')).toBe('moderator'); + + const received_stanza = u.toStanza(` + + Visit this site to get free Bitcoin! + + + `); + await view.model.queueMessage(received_stanza); + await u.waitUntil(() => view.model.messages.length === 1); + expect(view.model.messages.at(0).get('retracted')).toBeFalsy(); + + const reason = "This content is inappropriate for this forum!" + const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract')); + retract_button.click(); + + await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal'))); + + const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]'); + reason_input.value = 'This content is inappropriate for this forum!'; + const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]'); + submit_button.click(); + + const sent_IQs = _converse.connection.IQ_stanzas; + const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop()); + const message = view.model.messages.at(0); + const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`); + + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``+ ``+ - ``+ - ``); + `This content is inappropriate for this forum!`+ + ``+ + ``+ + ``); - expect(view.model.messages.length).toBe(1); - expect(message.get('retracted')).toBeTruthy(); - expect(message.get('editable')).toBeFalsy(); - expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); - const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message'); - expect(el.textContent.trim()).toBe('Romeo Montague has removed this message'); - done(); - })); - }); + const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); + _converse.connection._dataRecv(mock.createRequest(result_iq)); + + // We opportunistically save the message as retracted, even before receiving the retraction message + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); + expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason); + expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); + expect(view.model.messages.at(0).get('editable')).toBe(false); + expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); + + const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message'); + expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message'); + + const qel = msg_el.querySelector('q'); + expect(qel.textContent.trim()).toBe('This content is inappropriate for this forum!'); + + // The server responds with a retraction message + const retraction = u.toStanza(` + + + + + ${reason} + + + `); + await view.model.queueMessage(retraction); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); + expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason); + expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); + expect(view.model.messages.at(0).get('editable')).toBe(false); + done(); + })); + + it("can not be retracted if the MUC doesn't support message moderation", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.api.chatviews.get(muc_jid); + const occupant = view.model.getOwnOccupant(); + expect(occupant.get('role')).toBe('moderator'); + + const received_stanza = u.toStanza(` + + Visit this site to get free Bitcoin! + + + `); + await view.model.queueMessage(received_stanza); + await u.waitUntil(() => view.el.querySelector('.chat-msg__content')); + expect(view.el.querySelector('.chat-msg__content .chat-msg__action-retract')).toBe(null); + const result = await view.model.canModerateMessages(); + expect(result).toBe(false); + done(); + })); - describe("A Received Groupchat Message", function () { + it("can be retracted by a moderator, with the retraction message received before the IQ response", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - it("can be followed up by a retraction by the author", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const view = _converse.api.chatviews.get(muc_jid); + const occupant = view.model.getOwnOccupant(); + expect(occupant.get('role')).toBe('moderator'); - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const received_stanza = u.toStanza(` + + Visit this site to get free Bitcoin! + + + `); + await view.model.queueMessage(received_stanza); + await u.waitUntil(() => view.model.messages.length === 1); + expect(view.model.messages.length).toBe(1); - const received_stanza = u.toStanza(` - - Hello world - - - - `); - const view = _converse.api.chatviews.get(muc_jid); - await view.model.queueMessage(received_stanza); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); - expect(view.model.messages.at(0).get('retracted')).toBeFalsy(); - expect(view.model.messages.at(0).get('is_ephemeral')).toBeFalsy(); + const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract')); + retract_button.click(); + await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal'))); - const retraction_stanza = u.toStanza(` - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(retraction_stanza)); + const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]'); + const reason = "This content is inappropriate for this forum!" + reason_input.value = reason; + const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]'); + submit_button.click(); - // We opportunistically save the message as retracted, even before receiving the retraction message - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('retracted')).toBeTruthy(); - expect(view.model.messages.at(0).get('editable')).toBe(false); - expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); - const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message'); - expect(msg_el.textContent.trim()).toBe('eve has removed this message'); - expect(msg_el.querySelector('.chat-msg--retracted q')).toBe(null); - done(); - })); - - - it("can be retracted by a moderator, with the IQ response received before the retraction message", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - - const view = _converse.api.chatviews.get(muc_jid); - const occupant = view.model.getOwnOccupant(); - expect(occupant.get('role')).toBe('moderator'); - - const received_stanza = u.toStanza(` - - Visit this site to get free Bitcoin! - - - `); - await view.model.queueMessage(received_stanza); - await u.waitUntil(() => view.model.messages.length === 1); - expect(view.model.messages.at(0).get('retracted')).toBeFalsy(); - - const reason = "This content is inappropriate for this forum!" - const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract')); - retract_button.click(); - - await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal'))); - - const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]'); - reason_input.value = 'This content is inappropriate for this forum!'; - const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]'); - submit_button.click(); - - const sent_IQs = _converse.connection.IQ_stanzas; - const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop()); - const message = view.model.messages.at(0); - const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`); - - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - `This content is inappropriate for this forum!`+ - ``+ - ``+ - ``); - - const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(result_iq)); - - // We opportunistically save the message as retracted, even before receiving the retraction message - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); - expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason); - expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); - expect(view.model.messages.at(0).get('editable')).toBe(false); - expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); - - const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message'); - expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message'); - - const qel = msg_el.querySelector('q'); - expect(qel.textContent.trim()).toBe('This content is inappropriate for this forum!'); - - // The server responds with a retraction message - const retraction = u.toStanza(` - - - + const sent_IQs = _converse.connection.IQ_stanzas; + const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop()); + const message = view.model.messages.at(0); + const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`); + // The server responds with a retraction message + const retraction = u.toStanza(` + + + ${reason} - - - `); - await view.model.queueMessage(retraction); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); - expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason); - expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); - expect(view.model.messages.at(0).get('editable')).toBe(false); - done(); - })); + + + `); + await view.model.queueMessage(retraction); - it("can not be retracted if the MUC doesn't support message moderation", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); + expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); + const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); + expect(msg_el.textContent).toBe('romeo has removed this message'); + const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q'); + expect(qel.textContent).toBe('This content is inappropriate for this forum!'); - const muc_jid = 'lounge@montague.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.api.chatviews.get(muc_jid); - const occupant = view.model.getOwnOccupant(); - expect(occupant.get('role')).toBe('moderator'); - - const received_stanza = u.toStanza(` - - Visit this site to get free Bitcoin! - - - `); - await view.model.queueMessage(received_stanza); - await u.waitUntil(() => view.el.querySelector('.chat-msg__content')); - expect(view.el.querySelector('.chat-msg__content .chat-msg__action-retract')).toBe(null); - const result = await view.model.canModerateMessages(); - expect(result).toBe(false); - done(); - })); + const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); + _converse.connection._dataRecv(mock.createRequest(result_iq)); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); + expect(view.model.messages.at(0).get('moderated_by')).toBe(_converse.bare_jid); + expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason); + expect(view.model.messages.at(0).get('editable')).toBe(false); + done(); + })); + }); - it("can be retracted by a moderator, with the retraction message received before the IQ response", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + describe("A Sent Groupchat Message", function () { - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const view = _converse.api.chatviews.get(muc_jid); - const occupant = view.model.getOwnOccupant(); - expect(occupant.get('role')).toBe('moderator'); + it("can be retracted by its author", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - const received_stanza = u.toStanza(` - - Visit this site to get free Bitcoin! - - - `); - await view.model.queueMessage(received_stanza); - await u.waitUntil(() => view.model.messages.length === 1); - expect(view.model.messages.length).toBe(1); + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const view = _converse.api.chatviews.get(muc_jid); + const occupant = view.model.getOwnOccupant(); + expect(occupant.get('role')).toBe('moderator'); + occupant.save('role', 'member'); + const retraction_stanza = await sendAndThenRetractMessage(_converse, view); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); - const retract_button = await u.waitUntil(() => view.el.querySelector('.chat-msg__content .chat-msg__action-retract')); - retract_button.click(); - await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal'))); + const msg_obj = view.model.messages.last(); + expect(Strophe.serialize(retraction_stanza)).toBe( + ``+ + ``+ + ``+ + ``+ + ``+ + ``); - const reason_input = document.querySelector('#converse-modals .modal input[name="reason"]'); - const reason = "This content is inappropriate for this forum!" - reason_input.value = reason; - const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]'); - submit_button.click(); + const message = view.model.messages.last(); + expect(message.get('retracted')).toBeTruthy(); + expect(message.get('is_ephemeral')).toBe(false); + expect(message.get('editable')).toBeFalsy(); - const sent_IQs = _converse.connection.IQ_stanzas; - const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop()); - const message = view.model.messages.at(0); - const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`); - // The server responds with a retraction message - const retraction = u.toStanza(` - - - - - ${reason} - - - `); - await view.model.queueMessage(retraction); + const stanza_id = message.get(`stanza_id ${muc_jid}`); + // The server responds with a retraction message + const reflection = u.toStanza(` + + + + + + + `); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); - expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); - const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); - expect(msg_el.textContent).toBe('romeo has removed this message'); - const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q'); - expect(qel.textContent).toBe('This content is inappropriate for this forum!'); + spyOn(view.model, 'handleRetraction').and.callThrough(); + _converse.connection._dataRecv(mock.createRequest(reflection)); + await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1); - const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(result_iq)); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); - expect(view.model.messages.at(0).get('moderated_by')).toBe(_converse.bare_jid); - expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason); - expect(view.model.messages.at(0).get('editable')).toBe(false); - done(); - })); - }); + expect(view.model.messages.length).toBe(2); + expect(view.model.messages.last().get('retracted')).toBeTruthy(); + expect(view.model.messages.last().get('is_ephemeral')).toBe(false); + expect(view.model.messages.last().get('editable')).toBe(false); + expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); + const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); + expect(el.textContent).toBe('romeo has removed this message'); + done(); + })); + + it("can be retracted by its author, causing an error message in response", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const view = _converse.api.chatviews.get(muc_jid); + const occupant = view.model.getOwnOccupant(); + expect(occupant.get('role')).toBe('moderator'); + occupant.save('role', 'member'); + await u.waitUntil(() => + Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === + "romeo is no longer a moderator" + ); + const retraction_stanza = await sendAndThenRetractMessage(_converse, view); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); + + expect(view.model.messages.length).toBe(2); + expect(view.model.messages.last().get('retracted')).toBeTruthy(); + const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); + expect(el.textContent.trim()).toBe('romeo has removed this message'); + + const message = view.model.messages.last(); + const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`); + // The server responds with an error message + const error = u.toStanza(` + + + + + + + + + + `); + + _converse.connection._dataRecv(mock.createRequest(error)); + await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length === 1); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0); + expect(view.model.messages.length).toBe(3); + expect(view.model.messages.at(1).get('retracted')).toBeFalsy(); + expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy(); + expect(view.model.messages.at(1).get('editable')).toBeTruthy(); + + const err_msg = "Sorry, something went wrong while trying to retract your message." + expect(view.model.messages.at(2).get('message')).toBe(err_msg); + expect(view.model.messages.at(2).get('type')).toBe('error'); + + expect(view.el.querySelectorAll('.chat-error').length).toBe(1); + const errmsg = view.el.querySelector('.chat-error'); + expect(errmsg.textContent.trim()).toBe("Sorry, something went wrong while trying to retract your message."); + done(); + })); + + it("can be retracted by its author, causing a timeout error in response", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { + + _converse.STANZA_TIMEOUT = 1; + + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const view = _converse.api.chatviews.get(muc_jid); + const occupant = view.model.getOwnOccupant(); + expect(occupant.get('role')).toBe('moderator'); + occupant.save('role', 'member'); + await u.waitUntil(() => + Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === + "romeo is no longer a moderator" + ); + await sendAndThenRetractMessage(_converse, view); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); + + expect(view.model.messages.length).toBe(2); + expect(view.model.messages.last().get('retracted')).toBeTruthy(); + const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); + expect(el.textContent.trim()).toBe('romeo has removed this message'); + + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); + + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0); + expect(view.model.messages.length).toBe(4); + expect(view.model.messages.at(1).get('retracted')).toBeFalsy(); + expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy(); + expect(view.model.messages.at(1).get('editable')).toBeTruthy(); + + const error_messages = view.el.querySelectorAll('.chat-error'); + expect(error_messages.length).toBe(2); + expect(error_messages[0].textContent.trim()).toBe("Sorry, something went wrong while trying to retract your message."); + expect(error_messages[1].textContent.trim()).toBe("Timeout Error: No response from server"); + done(); + })); - describe("A Sent Groupchat Message", function () { + it("can be retracted by a moderator", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - it("can be retracted by its author", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const view = _converse.api.chatviews.get(muc_jid); + const occupant = view.model.getOwnOccupant(); + expect(occupant.get('role')).toBe('moderator'); - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const view = _converse.api.chatviews.get(muc_jid); - const occupant = view.model.getOwnOccupant(); - expect(occupant.get('role')).toBe('moderator'); - occupant.save('role', 'member'); - const retraction_stanza = await sendAndThenRetractMessage(_converse, view); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); + view.model.sendMessage('Visit this site to get free bitcoin'); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); + const stanza_id = 'retraction-id-1'; + const msg_obj = view.model.messages.at(0); + const reflection_stanza = u.toStanza(` + + ${msg_obj.get('message')} + + + `); + await view.model.queueMessage(reflection_stanza); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('editable')).toBe(true); - const msg_obj = view.model.messages.last(); - expect(Strophe.serialize(retraction_stanza)).toBe( - ``+ - ``+ - ``+ + // The server responds with a retraction message + const reason = "This content is inappropriate for this forum!" + const retraction = u.toStanza(` + + + + + ${reason} + + + `); + await view.model.queueMessage(retraction); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); + expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason); + expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); + expect(view.model.messages.at(0).get('editable')).toBe(false); + done(); + })); + + it("can be retracted by the sender if they're a moderator", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'allow_message_retraction': 'moderator'}, + async function (done, _converse) { + + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const view = _converse.api.chatviews.get(muc_jid); + const occupant = view.model.getOwnOccupant(); + expect(occupant.get('role')).toBe('moderator'); + + view.model.sendMessage('Visit this site to get free bitcoin'); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); + const stanza_id = 'retraction-id-1'; + const msg_obj = view.model.messages.at(0); + const reflection_stanza = u.toStanza(` + + ${msg_obj.get('message')} + + + `); + await view.model.queueMessage(reflection_stanza); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('editable')).toBe(true); + + const retract_button = await u.waitUntil(() => view.msgs_container.querySelector('.chat-msg__content .chat-msg__action-retract')); + retract_button.click(); + await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal'))); + const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]'); + submit_button.click(); + + const sent_IQs = _converse.connection.IQ_stanzas; + const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop()); + + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``+ ``+ - ``+ - ``); + ``+ + ``+ + ``+ + ``); - const message = view.model.messages.last(); - expect(message.get('retracted')).toBeTruthy(); - expect(message.get('is_ephemeral')).toBe(false); - expect(message.get('editable')).toBeFalsy(); + const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); + _converse.connection._dataRecv(mock.createRequest(result_iq)); - const stanza_id = message.get(`stanza_id ${muc_jid}`); - // The server responds with a retraction message - const reflection = u.toStanza(` - - - - - - - `); + // We opportunistically save the message as retracted, even before receiving the retraction message + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); + expect(view.model.messages.at(0).get('moderation_reason')).toBe(undefined); + expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); + expect(view.model.messages.at(0).get('editable')).toBe(false); + expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); - spyOn(view.model, 'handleRetraction').and.callThrough(); - _converse.connection._dataRecv(test_utils.createRequest(reflection)); - await u.waitUntil(() => view.model.handleRetraction.calls.count() === 1); + const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message'); + expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message'); + expect(msg_el.querySelector('q')).toBe(null); - expect(view.model.messages.length).toBe(2); - expect(view.model.messages.last().get('retracted')).toBeTruthy(); - expect(view.model.messages.last().get('is_ephemeral')).toBe(false); - expect(view.model.messages.last().get('editable')).toBe(false); - expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); - const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); - expect(el.textContent).toBe('romeo has removed this message'); - done(); - })); - - it("can be retracted by its author, causing an error message in response", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const view = _converse.api.chatviews.get(muc_jid); - const occupant = view.model.getOwnOccupant(); - expect(occupant.get('role')).toBe('moderator'); - occupant.save('role', 'member'); - await u.waitUntil(() => - Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === - "romeo is no longer a moderator" - ); - const retraction_stanza = await sendAndThenRetractMessage(_converse, view); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); - - expect(view.model.messages.length).toBe(2); - expect(view.model.messages.last().get('retracted')).toBeTruthy(); - const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); - expect(el.textContent.trim()).toBe('romeo has removed this message'); - - const message = view.model.messages.last(); - const stanza_id = message.get(`stanza_id ${view.model.get('jid')}`); - // The server responds with an error message - const error = u.toStanza(` - - - - - - - - - - `); - - _converse.connection._dataRecv(test_utils.createRequest(error)); - await u.waitUntil(() => view.el.querySelectorAll('.chat-error').length === 1); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0); - expect(view.model.messages.length).toBe(3); - expect(view.model.messages.at(1).get('retracted')).toBeFalsy(); - expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy(); - expect(view.model.messages.at(1).get('editable')).toBeTruthy(); - - const err_msg = "Sorry, something went wrong while trying to retract your message." - expect(view.model.messages.at(2).get('message')).toBe(err_msg); - expect(view.model.messages.at(2).get('type')).toBe('error'); - - expect(view.el.querySelectorAll('.chat-error').length).toBe(1); - const errmsg = view.el.querySelector('.chat-error'); - expect(errmsg.textContent.trim()).toBe("Sorry, something went wrong while trying to retract your message."); - done(); - })); - - it("can be retracted by its author, causing a timeout error in response", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - _converse.STANZA_TIMEOUT = 1; - - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const view = _converse.api.chatviews.get(muc_jid); - const occupant = view.model.getOwnOccupant(); - expect(occupant.get('role')).toBe('moderator'); - occupant.save('role', 'member'); - await u.waitUntil(() => - Array.from(view.el.querySelectorAll('.chat-info__message')).pop()?.textContent.trim() === - "romeo is no longer a moderator" - ); - await sendAndThenRetractMessage(_converse, view); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); - - expect(view.model.messages.length).toBe(2); - expect(view.model.messages.last().get('retracted')).toBeTruthy(); - const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); - expect(el.textContent.trim()).toBe('romeo has removed this message'); - - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); - - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 0); - expect(view.model.messages.length).toBe(4); - expect(view.model.messages.at(1).get('retracted')).toBeFalsy(); - expect(view.model.messages.at(1).get('is_ephemeral')).toBeFalsy(); - expect(view.model.messages.at(1).get('editable')).toBeTruthy(); - - const error_messages = view.el.querySelectorAll('.chat-error'); - expect(error_messages.length).toBe(2); - expect(error_messages[0].textContent.trim()).toBe("Sorry, something went wrong while trying to retract your message."); - expect(error_messages[1].textContent.trim()).toBe("Timeout Error: No response from server"); - done(); - })); + // The server responds with a retraction message + const retraction = u.toStanza(` + + + + + + + `); + await view.model.queueMessage(retraction); + expect(view.model.messages.length).toBe(1); + expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); + expect(view.model.messages.at(0).get('moderation_reason')).toBe(undefined); + expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); + expect(view.model.messages.at(0).get('editable')).toBe(false); + done(); + })); + }); - it("can be retracted by a moderator", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + describe("when archived", function () { - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const view = _converse.api.chatviews.get(muc_jid); - const occupant = view.model.getOwnOccupant(); - expect(occupant.get('role')).toBe('moderator'); + it("may be returned as a tombstone message", + mock.initConverse( + ['discoInitialized'], {}, + async function (done, _converse) { - view.model.sendMessage('Visit this site to get free bitcoin'); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); - const stanza_id = 'retraction-id-1'; - const msg_obj = view.model.messages.at(0); - const reflection_stanza = u.toStanza(` - - ${msg_obj.get('message')} - - - `); - await view.model.queueMessage(reflection_stanza); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('editable')).toBe(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 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'); + const view = _converse.chatboxviews.get(contact_jid); + const first_id = u.getUniqueId(); - // The server responds with a retraction message - const reason = "This content is inappropriate for this forum!" - const retraction = u.toStanza(` - - - - - ${reason} - - - `); - await view.model.queueMessage(retraction); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); - expect(view.model.messages.at(0).get('moderation_reason')).toBe(reason); - expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); - expect(view.model.messages.at(0).get('editable')).toBe(false); - done(); - })); + spyOn(view.model, 'handleRetraction').and.callThrough(); + const first_message = u.toStanza(` + + + + + + + 😊 + + + + + `); + _converse.connection._dataRecv(mock.createRequest(first_message)); - it("can be retracted by the sender if they're a moderator", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'allow_message_retraction': 'moderator'}, - async function (done, _converse) { + const tombstone = u.toStanza(` + + + + + + + + + + + + `); + _converse.connection._dataRecv(mock.createRequest(tombstone)); - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const view = _converse.api.chatviews.get(muc_jid); - const occupant = view.model.getOwnOccupant(); - expect(occupant.get('role')).toBe('moderator'); + const last_id = u.getUniqueId(); + const retraction = u.toStanza(` + + + + + + + + + + + + + `); + _converse.connection._dataRecv(mock.createRequest(retraction)); - view.model.sendMessage('Visit this site to get free bitcoin'); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 1); - const stanza_id = 'retraction-id-1'; - const msg_obj = view.model.messages.at(0); - const reflection_stanza = u.toStanza(` - - ${msg_obj.get('message')} - - - `); - await view.model.queueMessage(reflection_stanza); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg__body.chat-msg__body--received').length, 500); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('editable')).toBe(true); + 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(first_id).up() + .c('last').t(last_id).up() + .c('count').t('2'); + _converse.connection._dataRecv(mock.createRequest(iq_result)); - const retract_button = await u.waitUntil(() => view.msgs_container.querySelector('.chat-msg__content .chat-msg__action-retract')); - retract_button.click(); - await u.waitUntil(() => u.isVisible(document.querySelector('#converse-modals .modal'))); - const submit_button = document.querySelector('#converse-modals .modal button[type="submit"]'); - submit_button.click(); + await u.waitUntil(() => view.model.handleRetraction.calls.count() === 3); - const sent_IQs = _converse.connection.IQ_stanzas; - const stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector('iq apply-to[xmlns="urn:xmpp:fasten:0"]')).pop()); + expect(view.model.messages.length).toBe(2); + const message = view.model.messages.at(1); + expect(message.get('retracted')).toBeTruthy(); + expect(message.get('is_tombstone')).toBe(true); + expect(await view.model.handleRetraction.calls.first().returnValue).toBe(false); + expect(await view.model.handleRetraction.calls.all()[1].returnValue).toBe(false); + expect(await view.model.handleRetraction.calls.all()[2].returnValue).toBe(true); + await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2); + expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); + const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); + expect(el.textContent.trim()).toBe('Mercutio has removed this message'); + expect(u.hasClass('chat-msg--followup', el.parentElement)).toBe(false); + done(); + })); - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``+ - ``); + it("may be returned as a tombstone groupchat message", + mock.initConverse( + ['discoInitialized'], {}, + async function (done, _converse) { - const result_iq = $iq({'from': muc_jid, 'id': stanza.getAttribute('id'), 'to': _converse.bare_jid, 'type': 'result'}); - _converse.connection._dataRecv(test_utils.createRequest(result_iq)); + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const view = _converse.chatboxviews.get(muc_jid); - // We opportunistically save the message as retracted, even before receiving the retraction message - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg--retracted').length === 1); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); - expect(view.model.messages.at(0).get('moderation_reason')).toBe(undefined); - expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); - expect(view.model.messages.at(0).get('editable')).toBe(false); - expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); + 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'); - const msg_el = view.el.querySelector('.chat-msg--retracted .chat-msg__message'); - expect(msg_el.firstElementChild.textContent.trim()).toBe('romeo has removed this message'); - expect(msg_el.querySelector('q')).toBe(null); + const first_id = u.getUniqueId(); + const tombstone = u.toStanza(` + + + + + + + + + + + + `); + spyOn(view.model, 'handleRetraction').and.callThrough(); + const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve)); + _converse.connection._dataRecv(mock.createRequest(tombstone)); - // The server responds with a retraction message - const retraction = u.toStanza(` - - - - - - - `); - await view.model.queueMessage(retraction); - expect(view.model.messages.length).toBe(1); - expect(view.model.messages.at(0).get('moderated')).toBe('retracted'); - expect(view.model.messages.at(0).get('moderation_reason')).toBe(undefined); - expect(view.model.messages.at(0).get('is_ephemeral')).toBe(false); - expect(view.model.messages.at(0).get('editable')).toBe(false); - done(); - })); - }); + const last_id = u.getUniqueId(); + const retraction = u.toStanza(` + + + + + + + + + + + + + `); + _converse.connection._dataRecv(mock.createRequest(retraction)); + 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(first_id).up() + .c('last').t(last_id).up() + .c('count').t('2'); + _converse.connection._dataRecv(mock.createRequest(iq_result)); - describe("when archived", function () { + await promise; + expect(view.model.messages.length).toBe(1); + let message = view.model.messages.at(0); + expect(message.get('retracted')).toBeTruthy(); + expect(message.get('is_tombstone')).toBe(true); - it("may be returned as a tombstone message", - mock.initConverse( - ['discoInitialized'], {}, - async function (done, _converse) { + await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2); + expect(await view.model.handleRetraction.calls.first().returnValue).toBe(false); + expect(await view.model.handleRetraction.calls.all()[1].returnValue).toBe(true); + expect(view.model.messages.length).toBe(1); + message = view.model.messages.at(0); + expect(message.get('retracted')).toBeTruthy(); + expect(message.get('is_tombstone')).toBe(true); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); + expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); + const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); + expect(el.textContent.trim()).toBe('eve has removed this message'); + done(); + })); - 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'); - const view = _converse.chatboxviews.get(contact_jid); - const first_id = u.getUniqueId(); + it("may be returned as a tombstone moderated groupchat message", + mock.initConverse( + ['discoInitialized', 'chatBoxesFetched'], {}, + async function (done, _converse) { - spyOn(view.model, 'handleRetraction').and.callThrough(); - const first_message = u.toStanza(` - - - - - - - 😊 - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(first_message)); + const muc_jid = 'lounge@montague.lit'; + const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); + const view = _converse.chatboxviews.get(muc_jid); - const tombstone = u.toStanza(` - - - - - - - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(tombstone)); + 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'); - const last_id = u.getUniqueId(); - const retraction = u.toStanza(` - - - - - - - - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(retraction)); + const first_id = u.getUniqueId(); + const tombstone = u.toStanza(` + + + + + + + + This message contains inappropriate content + + + + + + `); + spyOn(view.model, 'handleModeration').and.callThrough(); + const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve)); + _converse.connection._dataRecv(mock.createRequest(tombstone)); - 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(first_id).up() - .c('last').t(last_id).up() - .c('count').t('2'); - _converse.connection._dataRecv(test_utils.createRequest(iq_result)); - - await u.waitUntil(() => view.model.handleRetraction.calls.count() === 3); - - expect(view.model.messages.length).toBe(2); - const message = view.model.messages.at(1); - expect(message.get('retracted')).toBeTruthy(); - expect(message.get('is_tombstone')).toBe(true); - expect(await view.model.handleRetraction.calls.first().returnValue).toBe(false); - expect(await view.model.handleRetraction.calls.all()[1].returnValue).toBe(false); - expect(await view.model.handleRetraction.calls.all()[2].returnValue).toBe(true); - await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 2); - expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); - const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); - expect(el.textContent.trim()).toBe('Mercutio has removed this message'); - expect(u.hasClass('chat-msg--followup', el.parentElement)).toBe(false); - done(); - })); - - it("may be returned as a tombstone groupchat message", - mock.initConverse( - ['discoInitialized'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const view = _converse.chatboxviews.get(muc_jid); - - 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'); - - const first_id = u.getUniqueId(); - const tombstone = u.toStanza(` - - - - - - - - - - - - `); - spyOn(view.model, 'handleRetraction').and.callThrough(); - const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve)); - _converse.connection._dataRecv(test_utils.createRequest(tombstone)); - - const last_id = u.getUniqueId(); - const retraction = u.toStanza(` - - - - - - + const last_id = u.getUniqueId(); + const retraction = u.toStanza(` + + + + + + + - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(retraction)); - - 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(first_id).up() - .c('last').t(last_id).up() - .c('count').t('2'); - _converse.connection._dataRecv(test_utils.createRequest(iq_result)); - - await promise; - expect(view.model.messages.length).toBe(1); - let message = view.model.messages.at(0); - expect(message.get('retracted')).toBeTruthy(); - expect(message.get('is_tombstone')).toBe(true); - - await u.waitUntil(() => view.model.handleRetraction.calls.count() === 2); - expect(await view.model.handleRetraction.calls.first().returnValue).toBe(false); - expect(await view.model.handleRetraction.calls.all()[1].returnValue).toBe(true); - expect(view.model.messages.length).toBe(1); - message = view.model.messages.at(0); - expect(message.get('retracted')).toBeTruthy(); - expect(message.get('is_tombstone')).toBe(true); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); - const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); - expect(el.textContent.trim()).toBe('eve has removed this message'); - done(); - })); - - it("may be returned as a tombstone moderated groupchat message", - mock.initConverse( - ['discoInitialized', 'chatBoxesFetched'], {}, - async function (done, _converse) { - - const muc_jid = 'lounge@montague.lit'; - const features = [...mock.default_muc_features, Strophe.NS.MODERATE]; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features); - const view = _converse.chatboxviews.get(muc_jid); - - 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'); - - const first_id = u.getUniqueId(); - const tombstone = u.toStanza(` - - - - - - - This message contains inappropriate content - - - - - `); - spyOn(view.model, 'handleModeration').and.callThrough(); - const promise = new Promise(resolve => _converse.api.listen.once('messageAdded', resolve)); - _converse.connection._dataRecv(test_utils.createRequest(tombstone)); + + + + + + `); + _converse.connection._dataRecv(mock.createRequest(retraction)); - const last_id = u.getUniqueId(); - const retraction = u.toStanza(` - - - - - - - - - This message contains inappropriate content - - - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(retraction)); + 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(first_id).up() + .c('last').t(last_id).up() + .c('count').t('2'); + _converse.connection._dataRecv(mock.createRequest(iq_result)); - 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(first_id).up() - .c('last').t(last_id).up() - .c('count').t('2'); - _converse.connection._dataRecv(test_utils.createRequest(iq_result)); + await promise; + expect(view.model.messages.length).toBe(1); + let message = view.model.messages.at(0); + expect(message.get('retracted')).toBeTruthy(); + expect(message.get('is_tombstone')).toBe(true); - await promise; - expect(view.model.messages.length).toBe(1); - let message = view.model.messages.at(0); - expect(message.get('retracted')).toBeTruthy(); - expect(message.get('is_tombstone')).toBe(true); + await u.waitUntil(() => view.model.handleModeration.calls.count() === 2); + expect(await view.model.handleModeration.calls.first().returnValue).toBe(false); + expect(await view.model.handleModeration.calls.all()[1].returnValue).toBe(true); - await u.waitUntil(() => view.model.handleModeration.calls.count() === 2); - expect(await view.model.handleModeration.calls.first().returnValue).toBe(false); - expect(await view.model.handleModeration.calls.all()[1].returnValue).toBe(true); + expect(view.model.messages.length).toBe(1); + message = view.model.messages.at(0); + expect(message.get('retracted')).toBeTruthy(); + expect(message.get('is_tombstone')).toBe(true); + expect(message.get('moderation_reason')).toBe("This message contains inappropriate content"); + expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - expect(view.model.messages.length).toBe(1); - message = view.model.messages.at(0); - expect(message.get('retracted')).toBeTruthy(); - expect(message.get('is_tombstone')).toBe(true); - expect(message.get('moderation_reason')).toBe("This message contains inappropriate content"); - expect(view.el.querySelectorAll('.chat-msg').length).toBe(1); - - expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); - const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); - expect(el.textContent.trim()).toBe('A moderator has removed this message'); - const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q'); - expect(qel.textContent.trim()).toBe('This message contains inappropriate content'); - done(); - })); - }); - }) -}); + expect(view.el.querySelectorAll('.chat-msg--retracted').length).toBe(1); + const el = view.el.querySelector('.chat-msg--retracted .chat-msg__message div'); + expect(el.textContent.trim()).toBe('A moderator has removed this message'); + const qel = view.el.querySelector('.chat-msg--retracted .chat-msg__message q'); + expect(qel.textContent.trim()).toBe('This message contains inappropriate content'); + done(); + })); + }); +}) diff --git a/spec/room_registration.js b/spec/room_registration.js index b853a8c3d..d20751c18 100644 --- a/spec/room_registration.js +++ b/spec/room_registration.js @@ -1,121 +1,119 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const _ = converse.env._, - $iq = converse.env.$iq, - Strophe = converse.env.Strophe, - sizzle = converse.env.sizzle, - u = converse.env.utils; +/*global mock */ - describe("Chatrooms", function () { +const _ = converse.env._, + $iq = converse.env.$iq, + Strophe = converse.env.Strophe, + sizzle = converse.env.sizzle, + u = converse.env.utils; + +describe("Chatrooms", function () { - describe("The /register commmand", function () { + describe("The /register commmand", function () { - it("allows you to register your nickname in a room", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true}, - async function (done, _converse) { + it("allows you to register your nickname in a room", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true}, + async function (done, _converse) { - const muc_jid = 'coven@chat.shakespeare.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo') - const view = _converse.chatboxviews.get(muc_jid); - const textarea = view.el.querySelector('.chat-textarea') - textarea.value = '/register'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - let stanza = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => sizzle(`iq[to="${muc_jid}"][type="get"] query[xmlns="jabber:iq:register"]`, iq).length - ).pop()); - expect(Strophe.serialize(stanza)) - .toBe(``+ - ``); - const result = $iq({ - 'from': view.model.get('jid'), - 'id': stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('query', {'type': 'jabber:iq:register'}) - .c('x', {'xmlns': 'jabber:x:data', 'type': 'form'}) - .c('field', { - 'label': 'Desired Nickname', - 'type': 'text-single', - 'var': 'muc#register_roomnick' - }).c('required'); - _converse.connection._dataRecv(test_utils.createRequest(result)); - stanza = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => sizzle(`iq[to="${muc_jid}"][type="set"] query[xmlns="jabber:iq:register"]`, iq).length - ).pop()); - - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``+ - `http://jabber.org/protocol/muc#register`+ - `romeo`+ - ``+ - ``+ - ``); - done(); - })); - - }); - - describe("The auto_register_muc_nickname option", function () { - - it("allows you to automatically register your nickname when joining a room", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true}, - async function (done, _converse) { - - const muc_jid = 'coven@chat.shakespeare.lit'; - await test_utils.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); - const view = _converse.chatboxviews.get(muc_jid); - - let stanza = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => sizzle(`iq[to="coven@chat.shakespeare.lit"][type="get"] query[xmlns="jabber:iq:register"]`, iq).length - ).pop()); - - expect(Strophe.serialize(stanza)) + const muc_jid = 'coven@chat.shakespeare.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo') + const view = _converse.chatboxviews.get(muc_jid); + const textarea = view.el.querySelector('.chat-textarea') + textarea.value = '/register'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + let stanza = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => sizzle(`iq[to="${muc_jid}"][type="get"] query[xmlns="jabber:iq:register"]`, iq).length + ).pop()); + expect(Strophe.serialize(stanza)) .toBe(``+ ``); - const result = $iq({ - 'from': view.model.get('jid'), - 'id': stanza.getAttribute('id'), - 'to': _converse.bare_jid, - 'type': 'result', - }).c('query', {'type': 'jabber:iq:register'}) - .c('x', {'xmlns': 'jabber:x:data', 'type': 'form'}) - .c('field', { - 'label': 'Desired Nickname', - 'type': 'text-single', - 'var': 'muc#register_roomnick' - }).c('required'); - _converse.connection._dataRecv(test_utils.createRequest(result)); - stanza = await u.waitUntil(() => _.filter( - _converse.connection.IQ_stanzas, - iq => sizzle(`iq[to="coven@chat.shakespeare.lit"][type="set"] query[xmlns="jabber:iq:register"]`, iq).length - ).pop()); + const result = $iq({ + 'from': view.model.get('jid'), + 'id': stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('query', {'type': 'jabber:iq:register'}) + .c('x', {'xmlns': 'jabber:x:data', 'type': 'form'}) + .c('field', { + 'label': 'Desired Nickname', + 'type': 'text-single', + 'var': 'muc#register_roomnick' + }).c('required'); + _converse.connection._dataRecv(mock.createRequest(result)); + stanza = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => sizzle(`iq[to="${muc_jid}"][type="set"] query[xmlns="jabber:iq:register"]`, iq).length + ).pop()); - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``+ - `http://jabber.org/protocol/muc#register`+ - `romeo`+ - ``+ - ``+ - ``); - done(); - })); - }); + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``+ + `http://jabber.org/protocol/muc#register`+ + `romeo`+ + ``+ + ``+ + ``); + done(); + })); + + }); + + describe("The auto_register_muc_nickname option", function () { + + it("allows you to automatically register your nickname when joining a room", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {'auto_register_muc_nickname': true}, + async function (done, _converse) { + + const muc_jid = 'coven@chat.shakespeare.lit'; + await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo'); + const view = _converse.chatboxviews.get(muc_jid); + + let stanza = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => sizzle(`iq[to="coven@chat.shakespeare.lit"][type="get"] query[xmlns="jabber:iq:register"]`, iq).length + ).pop()); + + expect(Strophe.serialize(stanza)) + .toBe(``+ + ``); + const result = $iq({ + 'from': view.model.get('jid'), + 'id': stanza.getAttribute('id'), + 'to': _converse.bare_jid, + 'type': 'result', + }).c('query', {'type': 'jabber:iq:register'}) + .c('x', {'xmlns': 'jabber:x:data', 'type': 'form'}) + .c('field', { + 'label': 'Desired Nickname', + 'type': 'text-single', + 'var': 'muc#register_roomnick' + }).c('required'); + _converse.connection._dataRecv(mock.createRequest(result)); + stanza = await u.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => sizzle(`iq[to="coven@chat.shakespeare.lit"][type="set"] query[xmlns="jabber:iq:register"]`, iq).length + ).pop()); + + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``+ + `http://jabber.org/protocol/muc#register`+ + `romeo`+ + ``+ + ``+ + ``); + done(); + })); }); }); diff --git a/spec/roomslist.js b/spec/roomslist.js index a27f36129..300084212 100644 --- a/spec/roomslist.js +++ b/spec/roomslist.js @@ -1,333 +1,338 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const { Strophe, $iq, $msg, $pres, sizzle, _ } = converse.env; - const u = converse.env.utils; +/* global mock */ +describe("A list of open groupchats", function () { - describe("A list of open groupchats", function () { - - it("is shown in controlbox", mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], - { allow_bookmarks: false // Makes testing easier, otherwise we - // have to mock stanza traffic. - }, async function (done, _converse) { - - await test_utils.openControlBox(_converse); - const controlbox = _converse.chatboxviews.get('controlbox'); - let list = controlbox.el.querySelector('.list-container--openrooms'); - expect(u.hasClass('hidden', list)).toBeTruthy(); - await test_utils.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC'); - - const lview = _converse.rooms_list_view - await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length); - let room_els = lview.el.querySelectorAll(".open-room"); - expect(room_els.length).toBe(1); - expect(room_els[0].innerText).toBe('room@conference.shakespeare.lit'); - - await test_utils.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); - await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length > 1); - room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); - expect(room_els.length).toBe(2); - - let view = _converse.chatboxviews.get('room@conference.shakespeare.lit'); - await view.close(); - room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); - expect(room_els.length).toBe(1); - expect(room_els[0].innerText).toBe('lounge@montague.lit'); - list = controlbox.el.querySelector('.list-container--openrooms'); - u.waitUntil(() => _.includes(list.classList, 'hidden')); - - view = _converse.chatboxviews.get('lounge@montague.lit'); - await view.close(); - room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); - expect(room_els.length).toBe(0); - - list = controlbox.el.querySelector('.list-container--openrooms'); - expect(_.includes(list.classList, 'hidden')).toBeTruthy(); - done(); - })); - - it("uses bookmarks to determine groupchat names", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], - {'view_mode': 'fullscreen'}, - async function (done, _converse) { - - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - let stanza = $pres({ - to: 'romeo@montague.lit/orchard', - from: 'lounge@montague.lit/newguy' - }) - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@montague.lit/_converse.js-290929789', - 'role': 'participant' - }).tree(); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - spyOn(_converse.Bookmarks.prototype, 'fetchBookmarks').and.callThrough(); - - await test_utils.waitUntilDiscoConfirmed( - _converse, _converse.bare_jid, - [{'category': 'pubsub', 'type':'pep'}], - [`${Strophe.NS.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()); - expect(Strophe.serialize(sent_stanza)).toBe( - ``+ - ''+ - ''+ - ''+ - ''); - - 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': 'Bookmarked Lounge', - 'jid': 'lounge@montague.lit' - }); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - - await _converse.api.waitUntil('roomsListInitialized'); - const controlbox = _converse.chatboxviews.get('controlbox'); - const list = controlbox.el.querySelector('.list-container--openrooms'); - expect(_.includes(list.classList, 'hidden')).toBeFalsy(); - const items = list.querySelectorAll('.list-item'); - expect(items.length).toBe(1); - expect(items[0].textContent.trim()).toBe('Bookmarked Lounge'); - expect(_converse.bookmarks.fetchBookmarks).toHaveBeenCalled(); - done(); - })); - }); - - describe("A groupchat shown in the groupchats list", function () { - - it("is highlighted if it's currently open", mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], - { view_mode: 'fullscreen', - allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic. - }, async function (done, _converse) { - - const muc_jid = 'coven@chat.shakespeare.lit'; - await _converse.api.rooms.open(muc_jid, {'nick': 'some1'}, true); - const lview = _converse.rooms_list_view - await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length); - let room_els = lview.el.querySelectorAll(".available-chatroom"); - expect(room_els.length).toBe(1); - - let item = room_els[0]; - await u.waitUntil(() => lview.model.get(muc_jid).get('hidden') === false); - await u.waitUntil(() => u.hasClass('open', item), 1000); - expect(item.textContent.trim()).toBe('coven@chat.shakespeare.lit'); - await _converse.api.rooms.open('balcony@chat.shakespeare.lit', {'nick': 'some1'}, true); - await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length > 1); - room_els = lview.el.querySelectorAll(".open-room"); - expect(room_els.length).toBe(2); - - room_els = lview.el.querySelectorAll(".available-chatroom.open"); - expect(room_els.length).toBe(1); - item = room_els[0]; - expect(item.textContent.trim()).toBe('balcony@chat.shakespeare.lit'); - done(); - })); - - it("has an info icon which opens a details modal when clicked", mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], - { whitelisted_plugins: ['converse-roomslist'], - allow_bookmarks: false // Makes testing easier, otherwise we + it("is shown in controlbox", mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], + { allow_bookmarks: false // Makes testing easier, otherwise we // have to mock stanza traffic. - }, async function (done, _converse) { + }, async function (done, _converse) { - const IQ_stanzas = _converse.connection.IQ_stanzas; - const room_jid = 'coven@chat.shakespeare.lit'; - await test_utils.openControlBox(_converse); - await _converse.api.rooms.open(room_jid, {'nick': 'some1'}); - const view = _converse.chatboxviews.get(room_jid); + const u = converse.env.utils; - const selector = `iq[to="${room_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`; - const features_query = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).pop()); - const features_stanza = $iq({ - 'from': 'coven@chat.shakespeare.lit', - 'id': features_query.getAttribute('id'), - 'to': 'romeo@montague.lit/desktop', - 'type': 'result' - }) - .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', { - 'category': 'conference', - 'name': 'A Dark Cave', - 'type': 'text' - }).up() - .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() - .c('feature', {'var': 'muc_passwordprotected'}).up() - .c('feature', {'var': 'muc_hidden'}).up() - .c('feature', {'var': 'muc_temporary'}).up() - .c('feature', {'var': 'muc_open'}).up() - .c('feature', {'var': 'muc_unmoderated'}).up() - .c('feature', {'var': 'muc_nonanonymous'}).up() - .c('feature', {'var': 'urn:xmpp:mam:0'}).up() - .c('x', { 'xmlns':'jabber:x:data', 'type':'result'}) - .c('field', {'var':'FORM_TYPE', 'type':'hidden'}) - .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up() - .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'}) - .c('value').t('This is the description').up().up() - .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'}) - .c('value').t(0); - _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); - await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING) - let presence = $pres({ - to: _converse.connection.jid, - from: 'coven@chat.shakespeare.lit/some1', - id: 'DC352437-C019-40EC-B590-AF29E879AF97' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - affiliation: 'member', - jid: _converse.bare_jid, - role: 'participant' - }).up() - .c('status').attrs({code:'110'}); - _converse.connection._dataRecv(test_utils.createRequest(presence)); + await mock.openControlBox(_converse); + const controlbox = _converse.chatboxviews.get('controlbox'); + let list = controlbox.el.querySelector('.list-container--openrooms'); + expect(u.hasClass('hidden', list)).toBeTruthy(); + await mock.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC'); - await u.waitUntil(() => _converse.rooms_list_view.el.querySelectorAll(".open-room").length, 500); - const room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); - expect(room_els.length).toBe(1); - const info_el = _converse.rooms_list_view.el.querySelector(".room-info"); - info_el.click(); + const lview = _converse.rooms_list_view + await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length); + let room_els = lview.el.querySelectorAll(".open-room"); + expect(room_els.length).toBe(1); + expect(room_els[0].innerText).toBe('room@conference.shakespeare.lit'); - const modal = view.model.room_details_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - let els = modal.el.querySelectorAll('p.room-info'); - expect(els[0].textContent).toBe("Name: A Dark Cave") - expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit") - expect(els[2].textContent).toBe("Description: This is the description") - expect(els[3].textContent).toBe("Online users: 1") - const features_list = modal.el.querySelector('.features-list'); - expect(features_list.textContent.replace(/(\n|\s{2,})/g, '')).toBe( - 'Password protected - This groupchat requires a password before entry'+ - 'Hidden - This groupchat is not publicly searchable'+ - 'Open - Anyone can join this groupchat'+ - 'Temporary - This groupchat will disappear once the last person leaves'+ - 'Not anonymous - All other groupchat participants can see your XMPP address'+ - 'Not moderated - Participants entering this groupchat can write right away' - ); - 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 mock.openChatRoom(_converse, 'lounge', 'montague.lit', 'romeo'); + await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length > 1); + room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); + expect(room_els.length).toBe(2); - els = modal.el.querySelectorAll('p.room-info'); - expect(els[3].textContent).toBe("Online users: 2") + let view = _converse.chatboxviews.get('room@conference.shakespeare.lit'); + await view.close(); + room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); + expect(room_els.length).toBe(1); + expect(room_els[0].innerText).toBe('lounge@montague.lit'); + list = controlbox.el.querySelector('.list-container--openrooms'); + u.waitUntil(() => Array.from(list.classList).includes('hidden')); - view.model.set({'subject': {'author': 'someone', 'text': 'Hatching dark plots'}}); - els = modal.el.querySelectorAll('p.room-info'); - expect(els[0].textContent).toBe("Name: A Dark Cave") - expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit") - expect(els[2].textContent).toBe("Description: This is the description") - expect(els[3].textContent).toBe("Topic: Hatching dark plots") - expect(els[4].textContent).toBe("Topic author: someone") - expect(els[5].textContent).toBe("Online users: 2") - done(); - })); + view = _converse.chatboxviews.get('lounge@montague.lit'); + await view.close(); + room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); + expect(room_els.length).toBe(0); - it("can be closed", mock.initConverse( - ['rosterGroupsFetched'], - { whitelisted_plugins: ['converse-roomslist'], - allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic. - }, - async function (done, _converse) { + list = controlbox.el.querySelector('.list-container--openrooms'); + expect(Array.from(list.classList).includes('hidden')).toBeTruthy(); + done(); + })); - spyOn(window, 'confirm').and.callFake(() => true); - expect(_converse.chatboxes.length).toBe(1); - await test_utils.openChatRoom(_converse, 'lounge', 'conference.shakespeare.lit', 'JC'); - expect(_converse.chatboxes.length).toBe(2); - const lview = _converse.rooms_list_view - await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length); - let room_els = lview.el.querySelectorAll(".open-room"); - expect(room_els.length).toBe(1); - const close_el = _converse.rooms_list_view.el.querySelector(".close-room"); - close_el.click(); - expect(window.confirm).toHaveBeenCalledWith( - 'Are you sure you want to leave the groupchat lounge@conference.shakespeare.lit?'); + it("uses bookmarks to determine groupchat names", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], + {'view_mode': 'fullscreen'}, + async function (done, _converse) { - await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve)); - room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); - expect(room_els.length).toBe(0); - expect(_converse.chatboxes.length).toBe(1); - done(); - })); + const { Strophe, $iq, $pres, sizzle } = converse.env; + const u = converse.env.utils; - it("shows unread messages directed at the user", mock.initConverse( - null, - { whitelisted_plugins: ['converse-roomslist'], - allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic. - }, async (done, _converse) => { + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + let stanza = $pres({ + to: 'romeo@montague.lit/orchard', + from: 'lounge@montague.lit/newguy' + }) + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@montague.lit/_converse.js-290929789', + 'role': 'participant' + }).tree(); + _converse.connection._dataRecv(mock.createRequest(stanza)); - await test_utils.openControlBox(_converse); - const room_jid = 'kitchen@conference.shakespeare.lit'; - await u.waitUntil(() => _converse.rooms_list_view !== undefined, 500); - await test_utils.openAndEnterChatRoom(_converse, room_jid, 'romeo'); - const view = _converse.chatboxviews.get(room_jid); - view.model.set({'minimized': true}); - const nick = mock.chatroom_names[0]; - await view.model.queueMessage( - $msg({ - from: room_jid+'/'+nick, - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('foo').tree()); + spyOn(_converse.Bookmarks.prototype, 'fetchBookmarks').and.callThrough(); - // If the user isn't mentioned, the counter doesn't get incremented, but the text of the groupchat is bold - const lview = _converse.rooms_list_view - let room_el = await u.waitUntil(() => lview.el.querySelector(".available-chatroom")); - expect(Array.from(room_el.classList).includes('unread-msgs')).toBeTruthy(); + await mock.waitUntilDiscoConfirmed( + _converse, _converse.bare_jid, + [{'category': 'pubsub', 'type':'pep'}], + [`${Strophe.NS.PUBSUB}#publish-options`] + ); - // If the user is mentioned, the counter also gets updated - await view.model.queueMessage( - $msg({ - from: room_jid+'/'+nick, - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('romeo: Your attention is required').tree() - ); + 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( + ``+ + ''+ + ''+ + ''+ + ''); - let indicator_el = await u.waitUntil(() => lview.el.querySelector(".msgs-indicator")); - expect(indicator_el.textContent).toBe('1'); + 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': 'Bookmarked Lounge', + 'jid': 'lounge@montague.lit' + }); + _converse.connection._dataRecv(mock.createRequest(stanza)); - spyOn(view.model, 'incrementUnreadMsgCounter').and.callThrough(); - await view.model.queueMessage( - $msg({ - from: room_jid+'/'+nick, - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('romeo: and another thing...').tree() - ); - await u.waitUntil(() => view.model.incrementUnreadMsgCounter.calls.count()); - await u.waitUntil(() => lview.el.querySelector(".msgs-indicator").textContent === '2', 1000); - - // When the chat gets maximized again, the unread indicators are removed - view.model.set({'minimized': false}); - indicator_el = lview.el.querySelector(".msgs-indicator"); - expect(indicator_el === null); - room_el = lview.el.querySelector(".available-chatroom"); - expect(_.includes(room_el.classList, 'unread-msgs')).toBeFalsy(); - done(); - })); - }); + await _converse.api.waitUntil('roomsListInitialized'); + const controlbox = _converse.chatboxviews.get('controlbox'); + const list = controlbox.el.querySelector('.list-container--openrooms'); + expect(Array.from(list.classList).includes('hidden')).toBeFalsy(); + const items = list.querySelectorAll('.list-item'); + expect(items.length).toBe(1); + expect(items[0].textContent.trim()).toBe('Bookmarked Lounge'); + expect(_converse.bookmarks.fetchBookmarks).toHaveBeenCalled(); + done(); + })); +}); + +describe("A groupchat shown in the groupchats list", function () { + + it("is highlighted if it's currently open", mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], + { view_mode: 'fullscreen', + allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic. + }, async function (done, _converse) { + + const u = converse.env.utils; + const muc_jid = 'coven@chat.shakespeare.lit'; + await _converse.api.rooms.open(muc_jid, {'nick': 'some1'}, true); + const lview = _converse.rooms_list_view + await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length); + let room_els = lview.el.querySelectorAll(".available-chatroom"); + expect(room_els.length).toBe(1); + + let item = room_els[0]; + await u.waitUntil(() => lview.model.get(muc_jid).get('hidden') === false); + await u.waitUntil(() => u.hasClass('open', item), 1000); + expect(item.textContent.trim()).toBe('coven@chat.shakespeare.lit'); + await _converse.api.rooms.open('balcony@chat.shakespeare.lit', {'nick': 'some1'}, true); + await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length > 1); + room_els = lview.el.querySelectorAll(".open-room"); + expect(room_els.length).toBe(2); + + room_els = lview.el.querySelectorAll(".available-chatroom.open"); + expect(room_els.length).toBe(1); + item = room_els[0]; + expect(item.textContent.trim()).toBe('balcony@chat.shakespeare.lit'); + done(); + })); + + it("has an info icon which opens a details modal when clicked", mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], + { whitelisted_plugins: ['converse-roomslist'], + allow_bookmarks: false // Makes testing easier, otherwise we + // have to mock stanza traffic. + }, async function (done, _converse) { + + const { Strophe, $iq, $pres } = converse.env; + const u = converse.env.utils; + const IQ_stanzas = _converse.connection.IQ_stanzas; + const room_jid = 'coven@chat.shakespeare.lit'; + await mock.openControlBox(_converse); + await _converse.api.rooms.open(room_jid, {'nick': 'some1'}); + const view = _converse.chatboxviews.get(room_jid); + + const selector = `iq[to="${room_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`; + const features_query = await u.waitUntil(() => IQ_stanzas.filter(iq => iq.querySelector(selector)).pop()); + const features_stanza = $iq({ + 'from': 'coven@chat.shakespeare.lit', + 'id': features_query.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }) + .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', { + 'category': 'conference', + 'name': 'A Dark Cave', + 'type': 'text' + }).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + .c('feature', {'var': 'muc_passwordprotected'}).up() + .c('feature', {'var': 'muc_hidden'}).up() + .c('feature', {'var': 'muc_temporary'}).up() + .c('feature', {'var': 'muc_open'}).up() + .c('feature', {'var': 'muc_unmoderated'}).up() + .c('feature', {'var': 'muc_nonanonymous'}).up() + .c('feature', {'var': 'urn:xmpp:mam:0'}).up() + .c('x', { 'xmlns':'jabber:x:data', 'type':'result'}) + .c('field', {'var':'FORM_TYPE', 'type':'hidden'}) + .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up() + .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'}) + .c('value').t('This is the description').up().up() + .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'}) + .c('value').t(0); + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + await u.waitUntil(() => view.model.session.get('connection_status') === converse.ROOMSTATUS.CONNECTING) + let presence = $pres({ + to: _converse.connection.jid, + from: 'coven@chat.shakespeare.lit/some1', + id: 'DC352437-C019-40EC-B590-AF29E879AF97' + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: 'member', + jid: _converse.bare_jid, + role: 'participant' + }).up() + .c('status').attrs({code:'110'}); + _converse.connection._dataRecv(mock.createRequest(presence)); + + await u.waitUntil(() => _converse.rooms_list_view.el.querySelectorAll(".open-room").length, 500); + const room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); + expect(room_els.length).toBe(1); + const info_el = _converse.rooms_list_view.el.querySelector(".room-info"); + info_el.click(); + + const modal = view.model.room_details_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + let els = modal.el.querySelectorAll('p.room-info'); + expect(els[0].textContent).toBe("Name: A Dark Cave") + expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit") + expect(els[2].textContent).toBe("Description: This is the description") + expect(els[3].textContent).toBe("Online users: 1") + const features_list = modal.el.querySelector('.features-list'); + expect(features_list.textContent.replace(/(\n|\s{2,})/g, '')).toBe( + 'Password protected - This groupchat requires a password before entry'+ + 'Hidden - This groupchat is not publicly searchable'+ + 'Open - Anyone can join this groupchat'+ + 'Temporary - This groupchat will disappear once the last person leaves'+ + 'Not anonymous - All other groupchat participants can see your XMPP address'+ + 'Not moderated - Participants entering this groupchat can write right away' + ); + 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)); + + els = modal.el.querySelectorAll('p.room-info'); + expect(els[3].textContent).toBe("Online users: 2") + + view.model.set({'subject': {'author': 'someone', 'text': 'Hatching dark plots'}}); + els = modal.el.querySelectorAll('p.room-info'); + expect(els[0].textContent).toBe("Name: A Dark Cave") + expect(els[1].textContent).toBe("Groupchat address (JID): coven@chat.shakespeare.lit") + expect(els[2].textContent).toBe("Description: This is the description") + expect(els[3].textContent).toBe("Topic: Hatching dark plots") + expect(els[4].textContent).toBe("Topic author: someone") + expect(els[5].textContent).toBe("Online users: 2") + done(); + })); + + it("can be closed", mock.initConverse( + ['rosterGroupsFetched'], + { whitelisted_plugins: ['converse-roomslist'], + allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic. + }, + async function (done, _converse) { + + const u = converse.env.utils; + spyOn(window, 'confirm').and.callFake(() => true); + expect(_converse.chatboxes.length).toBe(1); + await mock.openChatRoom(_converse, 'lounge', 'conference.shakespeare.lit', 'JC'); + expect(_converse.chatboxes.length).toBe(2); + const lview = _converse.rooms_list_view + await u.waitUntil(() => lview.el.querySelectorAll(".open-room").length); + let room_els = lview.el.querySelectorAll(".open-room"); + expect(room_els.length).toBe(1); + const close_el = _converse.rooms_list_view.el.querySelector(".close-room"); + close_el.click(); + expect(window.confirm).toHaveBeenCalledWith( + 'Are you sure you want to leave the groupchat lounge@conference.shakespeare.lit?'); + + await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve)); + room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room"); + expect(room_els.length).toBe(0); + expect(_converse.chatboxes.length).toBe(1); + done(); + })); + + it("shows unread messages directed at the user", mock.initConverse( + null, + { whitelisted_plugins: ['converse-roomslist'], + allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic. + }, async (done, _converse) => { + + const { $msg } = converse.env; + const u = converse.env.utils; + await mock.openControlBox(_converse); + const room_jid = 'kitchen@conference.shakespeare.lit'; + await u.waitUntil(() => _converse.rooms_list_view !== undefined, 500); + await mock.openAndEnterChatRoom(_converse, room_jid, 'romeo'); + const view = _converse.chatboxviews.get(room_jid); + view.model.set({'minimized': true}); + const nick = mock.chatroom_names[0]; + await view.model.queueMessage( + $msg({ + from: room_jid+'/'+nick, + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('foo').tree()); + + // If the user isn't mentioned, the counter doesn't get incremented, but the text of the groupchat is bold + const lview = _converse.rooms_list_view + let room_el = await u.waitUntil(() => lview.el.querySelector(".available-chatroom")); + expect(Array.from(room_el.classList).includes('unread-msgs')).toBeTruthy(); + + // If the user is mentioned, the counter also gets updated + await view.model.queueMessage( + $msg({ + from: room_jid+'/'+nick, + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('romeo: Your attention is required').tree() + ); + + let indicator_el = await u.waitUntil(() => lview.el.querySelector(".msgs-indicator")); + expect(indicator_el.textContent).toBe('1'); + + spyOn(view.model, 'incrementUnreadMsgCounter').and.callThrough(); + await view.model.queueMessage( + $msg({ + from: room_jid+'/'+nick, + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('romeo: and another thing...').tree() + ); + await u.waitUntil(() => view.model.incrementUnreadMsgCounter.calls.count()); + await u.waitUntil(() => lview.el.querySelector(".msgs-indicator").textContent === '2', 1000); + + // When the chat gets maximized again, the unread indicators are removed + view.model.set({'minimized': false}); + indicator_el = lview.el.querySelector(".msgs-indicator"); + expect(indicator_el === null); + room_el = lview.el.querySelector(".available-chatroom"); + expect(Array.from(room_el.classList).includes('unread-msgs')).toBeFalsy(); + done(); + })); }); diff --git a/spec/roster.js b/spec/roster.js index 839a39461..be51e95ff 100644 --- a/spec/roster.js +++ b/spec/roster.js @@ -1,1320 +1,1318 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const $iq = converse.env.$iq; - const $pres = converse.env.$pres; - const Strophe = converse.env.Strophe; - const _ = converse.env._; - const sizzle = converse.env.sizzle; - const u = converse.env.utils; +/*global mock */ - const checkHeaderToggling = async function (group) { - const toggle = group.querySelector('a.group-toggle'); - expect(u.isVisible(group)).toBeTruthy(); - expect(group.querySelectorAll('ul.collapsed').length).toBe(0); - expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeFalsy(); - expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeTruthy(); - toggle.click(); +const $iq = converse.env.$iq; +const $pres = converse.env.$pres; +const Strophe = converse.env.Strophe; +const _ = converse.env._; +const sizzle = converse.env.sizzle; +const u = converse.env.utils; - await u.waitUntil(() => group.querySelectorAll('ul.collapsed').length === 1); - expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeTruthy(); - expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeFalsy(); - toggle.click(); - await u.waitUntil(() => group.querySelectorAll('li').length === _.filter(group.querySelectorAll('li'), u.isVisible).length); - expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeFalsy(); - expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeTruthy(); - }; +const checkHeaderToggling = async function (group) { + const toggle = group.querySelector('a.group-toggle'); + expect(u.isVisible(group)).toBeTruthy(); + expect(group.querySelectorAll('ul.collapsed').length).toBe(0); + expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeFalsy(); + expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeTruthy(); + toggle.click(); + + await u.waitUntil(() => group.querySelectorAll('ul.collapsed').length === 1); + expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeTruthy(); + expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeFalsy(); + toggle.click(); + await u.waitUntil(() => group.querySelectorAll('li').length === _.filter(group.querySelectorAll('li'), u.isVisible).length); + expect(u.hasClass('fa-caret-right', toggle.firstElementChild)).toBeFalsy(); + expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeTruthy(); +}; - describe("The Contacts Roster", function () { +describe("The Contacts Roster", function () { - it("verifies the origin of roster pushes", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { + it("verifies the origin of roster pushes", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async function (done, _converse) { - // See: https://gultsch.de/gajim_roster_push_and_message_interception.html - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await test_utils.waitForRoster(_converse, 'current', 1); - expect(_converse.roster.models.length).toBe(1); - expect(_converse.roster.at(0).get('jid')).toBe(contact_jid); + // See: https://gultsch.de/gajim_roster_push_and_message_interception.html + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.waitForRoster(_converse, 'current', 1); + expect(_converse.roster.models.length).toBe(1); + expect(_converse.roster.at(0).get('jid')).toBe(contact_jid); - spyOn(converse.env.log, 'warn'); - let roster_push = u.toStanza(` - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(roster_push)); - expect(converse.env.log.warn.calls.count()).toBe(1); - expect(converse.env.log.warn).toHaveBeenCalledWith( - `Ignoring roster illegitimate roster push message from ${roster_push.getAttribute('from')}` - ); - roster_push = u.toStanza(` - - - - - `); - _converse.connection._dataRecv(test_utils.createRequest(roster_push)); - expect(converse.env.log.warn.calls.count()).toBe(2); - expect(converse.env.log.warn).toHaveBeenCalledWith( - `Ignoring roster illegitimate roster push message from ${roster_push.getAttribute('from')}` - ); - expect(_converse.roster.models.length).toBe(1); - expect(_converse.roster.at(0).get('jid')).toBe(contact_jid); - done(); - })); + spyOn(converse.env.log, 'warn'); + let roster_push = u.toStanza(` + + + + + `); + _converse.connection._dataRecv(mock.createRequest(roster_push)); + expect(converse.env.log.warn.calls.count()).toBe(1); + expect(converse.env.log.warn).toHaveBeenCalledWith( + `Ignoring roster illegitimate roster push message from ${roster_push.getAttribute('from')}` + ); + roster_push = u.toStanza(` + + + + + `); + _converse.connection._dataRecv(mock.createRequest(roster_push)); + expect(converse.env.log.warn.calls.count()).toBe(2); + expect(converse.env.log.warn).toHaveBeenCalledWith( + `Ignoring roster illegitimate roster push message from ${roster_push.getAttribute('from')}` + ); + expect(_converse.roster.models.length).toBe(1); + expect(_converse.roster.at(0).get('jid')).toBe(contact_jid); + done(); + })); - it("is populated once we have registered a presence handler", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("is populated once we have registered a presence handler", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - const IQs = _converse.connection.IQ_stanzas; - const stanza = await u.waitUntil( - () => _.filter(IQs, iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop()); + const IQs = _converse.connection.IQ_stanzas; + const stanza = await u.waitUntil( + () => _.filter(IQs, iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop()); - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``); - const result = $iq({ - 'to': _converse.connection.jid, - 'type': 'result', - 'id': stanza.getAttribute('id') - }).c('query', { - 'xmlns': 'jabber:iq:roster' - }).c('item', {'jid': 'nurse@example.com'}).up() - .c('item', {'jid': 'romeo@example.com'}) - _converse.connection._dataRecv(test_utils.createRequest(result)); - await u.waitUntil(() => _converse.promises['rosterContactsFetched'].isResolved === true); - done(); - })); + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``); + const result = $iq({ + 'to': _converse.connection.jid, + 'type': 'result', + 'id': stanza.getAttribute('id') + }).c('query', { + 'xmlns': 'jabber:iq:roster' + }).c('item', {'jid': 'nurse@example.com'}).up() + .c('item', {'jid': 'romeo@example.com'}) + _converse.connection._dataRecv(mock.createRequest(result)); + await u.waitUntil(() => _converse.promises['rosterContactsFetched'].isResolved === true); + done(); + })); - it("supports roster versioning", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("supports roster versioning", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - const IQ_stanzas = _converse.connection.IQ_stanzas; - let stanza = await u.waitUntil( - () => _.filter(IQ_stanzas, iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop() - ); - expect(_converse.roster.data.get('version')).toBeUndefined(); - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``); - let result = $iq({ - 'to': _converse.connection.jid, - 'type': 'result', - 'id': stanza.getAttribute('id') - }).c('query', { - 'xmlns': 'jabber:iq:roster', - 'ver': 'ver7' - }).c('item', {'jid': 'nurse@example.com'}).up() - .c('item', {'jid': 'romeo@example.com'}) - _converse.connection._dataRecv(test_utils.createRequest(result)); + const IQ_stanzas = _converse.connection.IQ_stanzas; + let stanza = await u.waitUntil( + () => _.filter(IQ_stanzas, iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop() + ); + expect(_converse.roster.data.get('version')).toBeUndefined(); + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``); + let result = $iq({ + 'to': _converse.connection.jid, + 'type': 'result', + 'id': stanza.getAttribute('id') + }).c('query', { + 'xmlns': 'jabber:iq:roster', + 'ver': 'ver7' + }).c('item', {'jid': 'nurse@example.com'}).up() + .c('item', {'jid': 'romeo@example.com'}) + _converse.connection._dataRecv(mock.createRequest(result)); - await u.waitUntil(() => _converse.roster.models.length > 1); - expect(_converse.roster.data.get('version')).toBe('ver7'); - expect(_converse.roster.models.length).toBe(2); + await u.waitUntil(() => _converse.roster.models.length > 1); + expect(_converse.roster.data.get('version')).toBe('ver7'); + expect(_converse.roster.models.length).toBe(2); - _converse.roster.fetchFromServer(); - stanza = _converse.connection.IQ_stanzas.pop(); - expect(Strophe.serialize(stanza)).toBe( - ``+ - ``+ - ``); + _converse.roster.fetchFromServer(); + stanza = _converse.connection.IQ_stanzas.pop(); + expect(Strophe.serialize(stanza)).toBe( + ``+ + ``+ + ``); - result = $iq({ - 'to': _converse.connection.jid, - 'type': 'result', - 'id': stanza.getAttribute('id') - }); - _converse.connection._dataRecv(test_utils.createRequest(result)); - - const roster_push = $iq({ - 'to': _converse.connection.jid, - 'type': 'set', - }).c('query', {'xmlns': 'jabber:iq:roster', 'ver': 'ver34'}) - .c('item', {'jid': 'romeo@example.com', 'subscription': 'remove'}); - _converse.connection._dataRecv(test_utils.createRequest(roster_push)); - expect(_converse.roster.data.get('version')).toBe('ver34'); - expect(_converse.roster.models.length).toBe(1); - expect(_converse.roster.at(0).get('jid')).toBe('nurse@example.com'); - done(); - })); - - describe("The live filter", function () { - - it("will only appear when roster contacts flow over the visible area", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - const filter = _converse.rosterview.el.querySelector('.roster-filter'); - expect(filter === null).toBe(false); - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - - const view = _converse.chatboxviews.get('controlbox'); - const flyout = view.el.querySelector('.box-flyout'); - const panel = flyout.querySelector('.controlbox-pane'); - function hasScrollBar (el) { - return el.isConnected && flyout.offsetHeight < panel.scrollHeight; - } - const el = _converse.rosterview.roster_el; - await u.waitUntil(() => hasScrollBar(el) ? u.isVisible(filter) : !u.isVisible(filter), 900); - done(); - })); - - it("can be used to filter the contacts shown", - mock.initConverse( - ['rosterGroupsFetched'], {'roster_groups': true}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current'); - let filter = _converse.rosterview.el.querySelector('.roster-filter'); - const roster = _converse.rosterview.roster_el; - _converse.rosterview.filter_view.delegateEvents(); - - await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600); - expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5); - filter.value = "juliet"; - u.triggerEvent(filter, "keydown", "KeyboardEvent"); - await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 1), 600); - // Only one roster contact is now visible - let visible_contacts = sizzle('li', roster).filter(u.isVisible); - expect(visible_contacts.length).toBe(1); - expect(visible_contacts.pop().textContent.trim()).toBe('Juliet Capulet'); - // Only one foster group is still visible - expect(sizzle('.roster-group', roster).filter(u.isVisible).length).toBe(1); - const visible_group = sizzle('.roster-group', roster).filter(u.isVisible).pop(); - expect(visible_group.querySelector('a.group-toggle').textContent.trim()).toBe('friends & acquaintences'); - - filter = _converse.rosterview.el.querySelector('.roster-filter'); - filter.value = "j"; - u.triggerEvent(filter, "keydown", "KeyboardEvent"); - await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 2), 700); - - visible_contacts = sizzle('li', roster).filter(u.isVisible); - expect(visible_contacts.length).toBe(2); - - let visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle')); - expect(visible_groups.length).toBe(2); - expect(visible_groups[0].textContent.trim()).toBe('friends & acquaintences'); - expect(visible_groups[1].textContent.trim()).toBe('Ungrouped'); - - filter = _converse.rosterview.el.querySelector('.roster-filter'); - filter.value = "xxx"; - u.triggerEvent(filter, "keydown", "KeyboardEvent"); - await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 0), 600); - visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle')); - expect(visible_groups.length).toBe(0); - - filter = _converse.rosterview.el.querySelector('.roster-filter'); - filter.value = ""; - u.triggerEvent(filter, "keydown", "KeyboardEvent"); - await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600); - expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5); - done(); - })); - - it("will also filter out contacts added afterwards", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current'); - - const filter = _converse.rosterview.el.querySelector('.roster-filter'); - const roster = _converse.rosterview.roster_el; - _converse.rosterview.filter_view.delegateEvents(); - - await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 800); - filter.value = "la"; - u.triggerEvent(filter, "keydown", "KeyboardEvent"); - await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 4), 800); - - // Five roster contact is now visible - const visible_contacts = sizzle('li', roster).filter(u.isVisible); - expect(visible_contacts.length).toBe(4); - let visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle')); - expect(visible_groups.length).toBe(4); - expect(visible_groups[0].textContent.trim()).toBe('Colleagues'); - expect(visible_groups[1].textContent.trim()).toBe('Family'); - expect(visible_groups[2].textContent.trim()).toBe('friends & acquaintences'); - expect(visible_groups[3].textContent.trim()).toBe('ænemies'); - - _converse.roster.create({ - jid: 'valentine@montague.lit', - subscription: 'both', - ask: null, - groups: ['newgroup'], - fullname: 'Valentine' - }); - await u.waitUntil(() => sizzle('.roster-group[data-group="newgroup"] li', roster).length, 300); - visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle')); - // The "newgroup" group doesn't appear - expect(visible_groups.length).toBe(4); - expect(visible_groups[0].textContent.trim()).toBe('Colleagues'); - expect(visible_groups[1].textContent.trim()).toBe('Family'); - expect(visible_groups[2].textContent.trim()).toBe('friends & acquaintences'); - expect(visible_groups[3].textContent.trim()).toBe('ænemies'); - expect(roster.querySelectorAll('.roster-group').length).toBe(6); - done(); - })); - - it("can be used to filter the groups shown", - mock.initConverse( - ['rosterGroupsFetched'], {'roster_groups': true}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current'); - _converse.rosterview.filter_view.delegateEvents(); - var roster = _converse.rosterview.roster_el; - - var button = _converse.rosterview.el.querySelector('span[data-type="groups"]'); - button.click(); - - await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600); - expect(sizzle('.roster-group', roster).filter(u.isVisible).length).toBe(5); - - var filter = _converse.rosterview.el.querySelector('.roster-filter'); - filter.value = "colleagues"; - u.triggerEvent(filter, "keydown", "KeyboardEvent"); - - await u.waitUntil(() => (sizzle('div.roster-group:not(.collapsed)', roster).length === 1), 600); - expect(sizzle('div.roster-group:not(.collapsed)', roster).pop().firstElementChild.textContent.trim()).toBe('Colleagues'); - expect(sizzle('div.roster-group:not(.collapsed) li', roster).filter(u.isVisible).length).toBe(6); - // Check that all contacts under the group are shown - expect(sizzle('div.roster-group:not(.collapsed) li', roster).filter(l => !u.isVisible(l)).length).toBe(0); - - filter = _converse.rosterview.el.querySelector('.roster-filter'); - filter.value = "xxx"; - u.triggerEvent(filter, "keydown", "KeyboardEvent"); - - await u.waitUntil(() => (roster.querySelectorAll('div.roster-group.collapsed').length === 5), 700); - expect(roster.querySelectorAll('div.roster-group:not(.collapsed) a').length).toBe(0); - - filter = _converse.rosterview.el.querySelector('.roster-filter'); - filter.value = ""; // Check that groups are shown again, when the filter string is cleared. - u.triggerEvent(filter, "keydown", "KeyboardEvent"); - await u.waitUntil(() => (roster.querySelectorAll('div.roster-group.collapsed').length === 0), 700); - expect(sizzle('div.roster-group:not(collapsed)', roster).length).toBe(5); - expect(sizzle('div.roster-group:not(collapsed) li', roster).length).toBe(17); - done(); - })); - - it("has a button with which its contents can be cleared", - mock.initConverse( - ['rosterGroupsFetched'], {'roster_groups': true}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current'); - - const filter = _converse.rosterview.el.querySelector('.roster-filter'); - filter.value = "xxx"; - u.triggerEvent(filter, "keydown", "KeyboardEvent"); - expect(_.includes(filter.classList, "x")).toBeFalsy(); - expect(u.hasClass('hidden', _converse.rosterview.el.querySelector('.roster-filter-form .clear-input'))).toBeTruthy(); - - const isHidden = _.partial(u.hasClass, 'hidden'); - await u.waitUntil(() => !isHidden(_converse.rosterview.el.querySelector('.roster-filter-form .clear-input')), 900); - _converse.rosterview.el.querySelector('.clear-input').click(); - expect(document.querySelector('.roster-filter').value).toBe(""); - done(); - })); - - // Disabling for now, because since recently this test consistently - // fails on Travis and I couldn't get it to pass there. - xit("can be used to filter contacts by their chat state", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - test_utils.waitForRoster(_converse, 'all'); - let jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - _converse.roster.get(jid).presence.set('show', 'online'); - jid = mock.cur_names[4].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - _converse.roster.get(jid).presence.set('show', 'dnd'); - test_utils.openControlBox(_converse); - const button = _converse.rosterview.el.querySelector('span[data-type="state"]'); - button.click(); - const roster = _converse.rosterview.roster_el; - await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).length === 15, 900); - const filter = _converse.rosterview.el.querySelector('.state-type'); - expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5); - filter.value = "online"; - u.triggerEvent(filter, 'change'); - - await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).length === 1, 900); - expect(sizzle('li', roster).filter(u.isVisible).pop().textContent.trim()).toBe('Lord Montague'); - await u.waitUntil(() => sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length === 1, 900); - const ul = sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).pop(); - expect(ul.parentElement.firstElementChild.textContent.trim()).toBe('friends & acquaintences'); - filter.value = "dnd"; - u.triggerEvent(filter, 'change'); - await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).pop().textContent.trim() === 'Friar Laurence', 900); - expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(1); - done(); - })); + result = $iq({ + 'to': _converse.connection.jid, + 'type': 'result', + 'id': stanza.getAttribute('id') }); + _converse.connection._dataRecv(mock.createRequest(result)); - describe("A Roster Group", function () { + const roster_push = $iq({ + 'to': _converse.connection.jid, + 'type': 'set', + }).c('query', {'xmlns': 'jabber:iq:roster', 'ver': 'ver34'}) + .c('item', {'jid': 'romeo@example.com', 'subscription': 'remove'}); + _converse.connection._dataRecv(mock.createRequest(roster_push)); + expect(_converse.roster.data.get('version')).toBe('ver34'); + expect(_converse.roster.models.length).toBe(1); + expect(_converse.roster.at(0).get('jid')).toBe('nurse@example.com'); + done(); + })); - it("is created to show contacts with unread messages", - mock.initConverse( - ['rosterGroupsFetched'], {'roster_groups': true}, - async function (done, _converse) { + describe("The live filter", function () { - spyOn(_converse.rosterview, 'update').and.callThrough(); - _converse.rosterview.render(); - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'all'); - await test_utils.createContacts(_converse, 'requesting'); + it("will only appear when roster contacts flow over the visible area", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + const filter = _converse.rosterview.el.querySelector('.roster-filter'); + expect(filter === null).toBe(false); + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + + const view = _converse.chatboxviews.get('controlbox'); + const flyout = view.el.querySelector('.box-flyout'); + const panel = flyout.querySelector('.controlbox-pane'); + function hasScrollBar (el) { + return el.isConnected && flyout.offsetHeight < panel.scrollHeight; + } + const el = _converse.rosterview.roster_el; + await u.waitUntil(() => hasScrollBar(el) ? u.isVisible(filter) : !u.isVisible(filter), 900); + done(); + })); + + it("can be used to filter the contacts shown", + mock.initConverse( + ['rosterGroupsFetched'], {'roster_groups': true}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + let filter = _converse.rosterview.el.querySelector('.roster-filter'); + const roster = _converse.rosterview.roster_el; + _converse.rosterview.filter_view.delegateEvents(); + + await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600); + expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5); + filter.value = "juliet"; + u.triggerEvent(filter, "keydown", "KeyboardEvent"); + await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 1), 600); + // Only one roster contact is now visible + let visible_contacts = sizzle('li', roster).filter(u.isVisible); + expect(visible_contacts.length).toBe(1); + expect(visible_contacts.pop().textContent.trim()).toBe('Juliet Capulet'); + // Only one foster group is still visible + expect(sizzle('.roster-group', roster).filter(u.isVisible).length).toBe(1); + const visible_group = sizzle('.roster-group', roster).filter(u.isVisible).pop(); + expect(visible_group.querySelector('a.group-toggle').textContent.trim()).toBe('friends & acquaintences'); + + filter = _converse.rosterview.el.querySelector('.roster-filter'); + filter.value = "j"; + u.triggerEvent(filter, "keydown", "KeyboardEvent"); + await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 2), 700); + + visible_contacts = sizzle('li', roster).filter(u.isVisible); + expect(visible_contacts.length).toBe(2); + + let visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle')); + expect(visible_groups.length).toBe(2); + expect(visible_groups[0].textContent.trim()).toBe('friends & acquaintences'); + expect(visible_groups[1].textContent.trim()).toBe('Ungrouped'); + + filter = _converse.rosterview.el.querySelector('.roster-filter'); + filter.value = "xxx"; + u.triggerEvent(filter, "keydown", "KeyboardEvent"); + await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 0), 600); + visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle')); + expect(visible_groups.length).toBe(0); + + filter = _converse.rosterview.el.querySelector('.roster-filter'); + filter.value = ""; + u.triggerEvent(filter, "keydown", "KeyboardEvent"); + await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600); + expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5); + done(); + })); + + it("will also filter out contacts added afterwards", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + + const filter = _converse.rosterview.el.querySelector('.roster-filter'); + const roster = _converse.rosterview.roster_el; + _converse.rosterview.filter_view.delegateEvents(); + + await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 800); + filter.value = "la"; + u.triggerEvent(filter, "keydown", "KeyboardEvent"); + await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 4), 800); + + // Five roster contact is now visible + const visible_contacts = sizzle('li', roster).filter(u.isVisible); + expect(visible_contacts.length).toBe(4); + let visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle')); + expect(visible_groups.length).toBe(4); + expect(visible_groups[0].textContent.trim()).toBe('Colleagues'); + expect(visible_groups[1].textContent.trim()).toBe('Family'); + expect(visible_groups[2].textContent.trim()).toBe('friends & acquaintences'); + expect(visible_groups[3].textContent.trim()).toBe('ænemies'); + + _converse.roster.create({ + jid: 'valentine@montague.lit', + subscription: 'both', + ask: null, + groups: ['newgroup'], + fullname: 'Valentine' + }); + await u.waitUntil(() => sizzle('.roster-group[data-group="newgroup"] li', roster).length, 300); + visible_groups = sizzle('.roster-group', roster).filter(u.isVisible).map(el => el.querySelector('a.group-toggle')); + // The "newgroup" group doesn't appear + expect(visible_groups.length).toBe(4); + expect(visible_groups[0].textContent.trim()).toBe('Colleagues'); + expect(visible_groups[1].textContent.trim()).toBe('Family'); + expect(visible_groups[2].textContent.trim()).toBe('friends & acquaintences'); + expect(visible_groups[3].textContent.trim()).toBe('ænemies'); + expect(roster.querySelectorAll('.roster-group').length).toBe(6); + done(); + })); + + it("can be used to filter the groups shown", + mock.initConverse( + ['rosterGroupsFetched'], {'roster_groups': true}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + _converse.rosterview.filter_view.delegateEvents(); + var roster = _converse.rosterview.roster_el; + + var button = _converse.rosterview.el.querySelector('span[data-type="groups"]'); + button.click(); + + await u.waitUntil(() => (sizzle('li', roster).filter(u.isVisible).length === 17), 600); + expect(sizzle('.roster-group', roster).filter(u.isVisible).length).toBe(5); + + var filter = _converse.rosterview.el.querySelector('.roster-filter'); + filter.value = "colleagues"; + u.triggerEvent(filter, "keydown", "KeyboardEvent"); + + await u.waitUntil(() => (sizzle('div.roster-group:not(.collapsed)', roster).length === 1), 600); + expect(sizzle('div.roster-group:not(.collapsed)', roster).pop().firstElementChild.textContent.trim()).toBe('Colleagues'); + expect(sizzle('div.roster-group:not(.collapsed) li', roster).filter(u.isVisible).length).toBe(6); + // Check that all contacts under the group are shown + expect(sizzle('div.roster-group:not(.collapsed) li', roster).filter(l => !u.isVisible(l)).length).toBe(0); + + filter = _converse.rosterview.el.querySelector('.roster-filter'); + filter.value = "xxx"; + u.triggerEvent(filter, "keydown", "KeyboardEvent"); + + await u.waitUntil(() => (roster.querySelectorAll('div.roster-group.collapsed').length === 5), 700); + expect(roster.querySelectorAll('div.roster-group:not(.collapsed) a').length).toBe(0); + + filter = _converse.rosterview.el.querySelector('.roster-filter'); + filter.value = ""; // Check that groups are shown again, when the filter string is cleared. + u.triggerEvent(filter, "keydown", "KeyboardEvent"); + await u.waitUntil(() => (roster.querySelectorAll('div.roster-group.collapsed').length === 0), 700); + expect(sizzle('div.roster-group:not(collapsed)', roster).length).toBe(5); + expect(sizzle('div.roster-group:not(collapsed) li', roster).length).toBe(17); + done(); + })); + + it("has a button with which its contents can be cleared", + mock.initConverse( + ['rosterGroupsFetched'], {'roster_groups': true}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + + const filter = _converse.rosterview.el.querySelector('.roster-filter'); + filter.value = "xxx"; + u.triggerEvent(filter, "keydown", "KeyboardEvent"); + expect(_.includes(filter.classList, "x")).toBeFalsy(); + expect(u.hasClass('hidden', _converse.rosterview.el.querySelector('.roster-filter-form .clear-input'))).toBeTruthy(); + + const isHidden = _.partial(u.hasClass, 'hidden'); + await u.waitUntil(() => !isHidden(_converse.rosterview.el.querySelector('.roster-filter-form .clear-input')), 900); + _converse.rosterview.el.querySelector('.clear-input').click(); + expect(document.querySelector('.roster-filter').value).toBe(""); + done(); + })); + + // Disabling for now, because since recently this test consistently + // fails on Travis and I couldn't get it to pass there. + xit("can be used to filter contacts by their chat state", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + mock.waitForRoster(_converse, 'all'); + let jid = mock.cur_names[3].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + _converse.roster.get(jid).presence.set('show', 'online'); + jid = mock.cur_names[4].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + _converse.roster.get(jid).presence.set('show', 'dnd'); + mock.openControlBox(_converse); + const button = _converse.rosterview.el.querySelector('span[data-type="state"]'); + button.click(); + const roster = _converse.rosterview.roster_el; + await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).length === 15, 900); + const filter = _converse.rosterview.el.querySelector('.state-type'); + expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(5); + filter.value = "online"; + u.triggerEvent(filter, 'change'); + + await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).length === 1, 900); + expect(sizzle('li', roster).filter(u.isVisible).pop().textContent.trim()).toBe('Lord Montague'); + await u.waitUntil(() => sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length === 1, 900); + const ul = sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).pop(); + expect(ul.parentElement.firstElementChild.textContent.trim()).toBe('friends & acquaintences'); + filter.value = "dnd"; + u.triggerEvent(filter, 'change'); + await u.waitUntil(() => sizzle('li', roster).filter(u.isVisible).pop().textContent.trim() === 'Friar Laurence', 900); + expect(sizzle('ul.roster-group-contacts', roster).filter(u.isVisible).length).toBe(1); + done(); + })); + }); + + describe("A Roster Group", function () { + + it("is created to show contacts with unread messages", + mock.initConverse( + ['rosterGroupsFetched'], {'roster_groups': true}, + async function (done, _converse) { + + spyOn(_converse.rosterview, 'update').and.callThrough(); + _converse.rosterview.render(); + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'all'); + await mock.createContacts(_converse, 'requesting'); - // Check that the groups appear alphabetically and that - // requesting and pending contacts are last. - await u.waitUntil(() => sizzle('.roster-group a.group-toggle', _converse.rosterview.el).length); - let group_titles = sizzle('.roster-group a.group-toggle', _converse.rosterview.el).map(o => o.textContent.trim()); - expect(group_titles).toEqual([ - "Contact requests", - "Colleagues", - "Family", - "friends & acquaintences", - "ænemies", - "Ungrouped", - "Pending contacts" - ]); + // Check that the groups appear alphabetically and that + // requesting and pending contacts are last. + await u.waitUntil(() => sizzle('.roster-group a.group-toggle', _converse.rosterview.el).length); + let group_titles = sizzle('.roster-group a.group-toggle', _converse.rosterview.el).map(o => o.textContent.trim()); + expect(group_titles).toEqual([ + "Contact requests", + "Colleagues", + "Family", + "friends & acquaintences", + "ænemies", + "Ungrouped", + "Pending contacts" + ]); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const contact = await _converse.api.contacts.get(contact_jid); - contact.save({'num_unread': 5}); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const contact = await _converse.api.contacts.get(contact_jid); + contact.save({'num_unread': 5}); - await u.waitUntil(() => sizzle('.roster-group a.group-toggle', _converse.rosterview.el).length === 8); - group_titles = sizzle('.roster-group a.group-toggle', _converse.rosterview.el).map(o => o.textContent.trim()); + await u.waitUntil(() => sizzle('.roster-group a.group-toggle', _converse.rosterview.el).length === 8); + group_titles = sizzle('.roster-group a.group-toggle', _converse.rosterview.el).map(o => o.textContent.trim()); - expect(group_titles).toEqual([ - "New messages", - "Contact requests", - "Colleagues", - "Family", - "friends & acquaintences", - "ænemies", - "Ungrouped", - "Pending contacts" - ]); - const contacts = sizzle('.roster-group[data-group="New messages"] li', _converse.rosterview.el); - expect(contacts.length).toBe(1); - expect(contacts[0].querySelector('.contact-name').textContent).toBe("Mercutio"); - expect(contacts[0].querySelector('.msgs-indicator').textContent).toBe("5"); + expect(group_titles).toEqual([ + "New messages", + "Contact requests", + "Colleagues", + "Family", + "friends & acquaintences", + "ænemies", + "Ungrouped", + "Pending contacts" + ]); + const contacts = sizzle('.roster-group[data-group="New messages"] li', _converse.rosterview.el); + expect(contacts.length).toBe(1); + expect(contacts[0].querySelector('.contact-name').textContent).toBe("Mercutio"); + expect(contacts[0].querySelector('.msgs-indicator').textContent).toBe("5"); - contact.save({'num_unread': 0}); - await u.waitUntil(() => sizzle('.roster-group a.group-toggle', _converse.rosterview.el).length === 7); - group_titles = sizzle('.roster-group a.group-toggle', _converse.rosterview.el).map(o => o.textContent.trim()); - expect(group_titles).toEqual([ - "Contact requests", - "Colleagues", - "Family", - "friends & acquaintences", - "ænemies", - "Ungrouped", - "Pending contacts" - ]); - done(); - })); + contact.save({'num_unread': 0}); + await u.waitUntil(() => sizzle('.roster-group a.group-toggle', _converse.rosterview.el).length === 7); + group_titles = sizzle('.roster-group a.group-toggle', _converse.rosterview.el).map(o => o.textContent.trim()); + expect(group_titles).toEqual([ + "Contact requests", + "Colleagues", + "Family", + "friends & acquaintences", + "ænemies", + "Ungrouped", + "Pending contacts" + ]); + done(); + })); - it("can be used to organize existing contacts", - mock.initConverse( - ['rosterGroupsFetched'], {'roster_groups': true}, - async function (done, _converse) { + it("can be used to organize existing contacts", + mock.initConverse( + ['rosterGroupsFetched'], {'roster_groups': true}, + async function (done, _converse) { - spyOn(_converse.rosterview, 'update').and.callThrough(); - _converse.rosterview.render(); - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'all'); - await test_utils.createContacts(_converse, 'requesting'); - // Check that the groups appear alphabetically and that - // requesting and pending contacts are last. - await u.waitUntil(() => sizzle('.roster-group a.group-toggle', _converse.rosterview.el).length); - const group_titles = sizzle('.roster-group a.group-toggle', _converse.rosterview.el).map(o => o.textContent.trim()); - expect(group_titles).toEqual([ - "Contact requests", - "Colleagues", - "Family", - "friends & acquaintences", - "ænemies", - "Ungrouped", - "Pending contacts" - ]); - // Check that usernames appear alphabetically per group - Object.keys(mock.groups).forEach(name => { - const contacts = sizzle('.roster-group[data-group="'+name+'"] ul', _converse.rosterview.el); - const names = contacts.map(o => o.textContent.trim()); - expect(names).toEqual(_.clone(names).sort()); - }); - done(); - })); + spyOn(_converse.rosterview, 'update').and.callThrough(); + _converse.rosterview.render(); + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'all'); + await mock.createContacts(_converse, 'requesting'); + // Check that the groups appear alphabetically and that + // requesting and pending contacts are last. + await u.waitUntil(() => sizzle('.roster-group a.group-toggle', _converse.rosterview.el).length); + const group_titles = sizzle('.roster-group a.group-toggle', _converse.rosterview.el).map(o => o.textContent.trim()); + expect(group_titles).toEqual([ + "Contact requests", + "Colleagues", + "Family", + "friends & acquaintences", + "ænemies", + "Ungrouped", + "Pending contacts" + ]); + // Check that usernames appear alphabetically per group + Object.keys(mock.groups).forEach(name => { + const contacts = sizzle('.roster-group[data-group="'+name+'"] ul', _converse.rosterview.el); + const names = contacts.map(o => o.textContent.trim()); + expect(names).toEqual(_.clone(names).sort()); + }); + done(); + })); - it("gets created when a contact's \"groups\" attribute changes", - mock.initConverse( - ['rosterGroupsFetched'], {'roster_groups': true}, - async function (done, _converse) { + it("gets created when a contact's \"groups\" attribute changes", + mock.initConverse( + ['rosterGroupsFetched'], {'roster_groups': true}, + async function (done, _converse) { - spyOn(_converse.rosterview, 'update').and.callThrough(); - _converse.rosterview.render(); + spyOn(_converse.rosterview, 'update').and.callThrough(); + _converse.rosterview.render(); - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current', 0); + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 0); + _converse.roster.create({ + jid: 'groupchanger@montague.lit', + subscription: 'both', + ask: null, + groups: ['firstgroup'], + fullname: 'George Groupchanger' + }); + + // Check that the groups appear alphabetically and that + // requesting and pending contacts are last. + let group_titles = await u.waitUntil(() => { + const toggles = sizzle('.roster-group a.group-toggle', _converse.rosterview.el); + if (_.reduce(toggles, (result, t) => result && u.isVisible(t), true)) { + return _.map(toggles, o => o.textContent.trim()); + } else { + return false; + } + }, 1000); + expect(group_titles).toEqual(['firstgroup']); + + const contact = _converse.roster.get('groupchanger@montague.lit'); + contact.set({'groups': ['secondgroup']}); + group_titles = await u.waitUntil(() => { + const toggles = sizzle('.roster-group[data-group="secondgroup"] a.group-toggle', _converse.rosterview.el); + if (_.reduce(toggles, (result, t) => result && u.isVisible(t), true)) { + return _.map(toggles, o => o.textContent.trim()); + } else { + return false; + } + }, 1000); + expect(group_titles).toEqual(['secondgroup']); + done(); + })); + + it("can share contacts with other roster groups", + mock.initConverse( + ['rosterGroupsFetched'], {'roster_groups': true}, + async function (done, _converse) { + + const groups = ['Colleagues', 'friends']; + spyOn(_converse.rosterview, 'update').and.callThrough(); + mock.openControlBox(_converse); + _converse.rosterview.render(); + for (var i=0; i (sizzle('li', _converse.rosterview.el).filter(u.isVisible).length === 30), 600); + // Check that usernames appear alphabetically per group + _.each(groups, function (name) { + const contacts = sizzle('.roster-group[data-group="'+name+'"] ul li', _converse.rosterview.el); + const names = contacts.map(o => o.textContent.trim()); + expect(names).toEqual(_.clone(names).sort()); + expect(names.length).toEqual(mock.cur_names.length); + }); + done(); + })); - // Check that the groups appear alphabetically and that - // requesting and pending contacts are last. - let group_titles = await u.waitUntil(() => { - const toggles = sizzle('.roster-group a.group-toggle', _converse.rosterview.el); - if (_.reduce(toggles, (result, t) => result && u.isVisible(t), true)) { - return _.map(toggles, o => o.textContent.trim()); - } else { - return false; - } - }, 1000); - expect(group_titles).toEqual(['firstgroup']); + it("remembers whether it is closed or opened", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - const contact = _converse.roster.get('groupchanger@montague.lit'); - contact.set({'groups': ['secondgroup']}); - group_titles = await u.waitUntil(() => { - const toggles = sizzle('.roster-group[data-group="secondgroup"] a.group-toggle', _converse.rosterview.el); - if (_.reduce(toggles, (result, t) => result && u.isVisible(t), true)) { - return _.map(toggles, o => o.textContent.trim()); - } else { - return false; - } - }, 1000); - expect(group_titles).toEqual(['secondgroup']); - done(); - })); + _converse.roster_groups = true; + mock.openControlBox(_converse); - it("can share contacts with other roster groups", - mock.initConverse( - ['rosterGroupsFetched'], {'roster_groups': true}, - async function (done, _converse) { - - const groups = ['Colleagues', 'friends']; - spyOn(_converse.rosterview, 'update').and.callThrough(); - test_utils.openControlBox(_converse); - _converse.rosterview.render(); - for (var i=0; i (sizzle('li', _converse.rosterview.el).filter(u.isVisible).length === 30), 600); - // Check that usernames appear alphabetically per group - _.each(groups, function (name) { - const contacts = sizzle('.roster-group[data-group="'+name+'"] ul li', _converse.rosterview.el); - const names = contacts.map(o => o.textContent.trim()); - expect(names).toEqual(_.clone(names).sort()); - expect(names.length).toEqual(mock.cur_names.length); - }); - done(); - })); + }); + const view = _converse.rosterview.get('Colleagues'); + const toggle = view.el.querySelector('a.group-toggle'); + expect(view.model.get('state')).toBe('opened'); + toggle.click(); + await u.waitUntil(() => view.model.get('state') === 'closed'); + toggle.click(); + await u.waitUntil(() => view.model.get('state') === 'opened'); + done(); + })); + }); - it("remembers whether it is closed or opened", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + describe("Pending Contacts", function () { - _converse.roster_groups = true; - test_utils.openControlBox(_converse); + it("can be collapsed under their own header", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - var i=0, j=0; - var groups = { - 'Colleagues': 3, - 'friends & acquaintences': 3, - 'Ungrouped': 2 - }; - _.each(_.keys(groups), function (name) { - j = i; - for (i=j; i view.model.get('state') === 'closed'); - toggle.click(); - await u.waitUntil(() => view.model.get('state') === 'opened'); - done(); - })); - }); + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'all'); + await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname')))); + await u.waitUntil(() => sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).map(e => e.querySelector('li')).length, 1000); + await checkHeaderToggling.apply( + _converse, + [_converse.rosterview.get('Pending contacts').el] + ); + done(); + })); - describe("Pending Contacts", function () { + it("can be added to the roster", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - it("can be collapsed under their own header", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + spyOn(_converse.rosterview, 'update').and.callThrough(); + await mock.openControlBox(_converse); + _converse.roster.create({ + jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', + subscription: 'none', + ask: 'subscribe', + fullname: mock.pend_names[0] + }); + expect(_converse.rosterview.update).toHaveBeenCalled(); + done(); + })); - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'all'); - await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname')))); - await u.waitUntil(() => sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).map(e => e.querySelector('li')).length, 1000); - await checkHeaderToggling.apply( - _converse, - [_converse.rosterview.get('Pending contacts').el] - ); - done(); - })); + it("are shown in the roster when hide_offline_users", + mock.initConverse( + ['rosterGroupsFetched'], {'hide_offline_users': true}, + async function (done, _converse) { - it("can be added to the roster", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + spyOn(_converse.rosterview, 'update').and.callThrough(); + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'pending'); + await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname')))); + await u.waitUntil(() => sizzle('li', _converse.rosterview.el).filter(u.isVisible).length, 500) + expect(_converse.rosterview.update).toHaveBeenCalled(); + expect(u.isVisible(_converse.rosterview.el)).toBe(true); + expect(sizzle('li', _converse.rosterview.el).filter(u.isVisible).length).toBe(3); + expect(sizzle('ul.roster-group-contacts', _converse.rosterview.el).filter(u.isVisible).length).toBe(1); + done(); + })); - spyOn(_converse.rosterview, 'update').and.callThrough(); - await test_utils.openControlBox(_converse); - _converse.roster.create({ - jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit', - subscription: 'none', - ask: 'subscribe', - fullname: mock.pend_names[0] - }); - expect(_converse.rosterview.update).toHaveBeenCalled(); - done(); - })); + it("can be removed by the user", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - it("are shown in the roster when hide_offline_users", - mock.initConverse( - ['rosterGroupsFetched'], {'hide_offline_users': true}, - async function (done, _converse) { + await mock.waitForRoster(_converse, 'all'); + await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname')))); + const name = mock.pend_names[0]; + const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const contact = _converse.roster.get(jid); + var sent_IQ; + spyOn(window, 'confirm').and.returnValue(true); + spyOn(contact, 'unauthorize').and.callFake(function () { return contact; }); + spyOn(contact, 'removeFromRoster').and.callThrough(); + await u.waitUntil(() => sizzle(".pending-contact-name:contains('"+name+"')", _converse.rosterview.el).length, 700); + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) { + sent_IQ = iq; + callback(); + }); + sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, _converse.rosterview.el).pop().click(); + await u.waitUntil(() => (sizzle(".pending-contact-name:contains('"+name+"')", _converse.rosterview.el).length === 0), 1000); + expect(window.confirm).toHaveBeenCalled(); + expect(contact.removeFromRoster).toHaveBeenCalled(); + expect(sent_IQ.toLocaleString()).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + done(); + })); - spyOn(_converse.rosterview, 'update').and.callThrough(); - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'pending'); - await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname')))); - await u.waitUntil(() => sizzle('li', _converse.rosterview.el).filter(u.isVisible).length, 500) - expect(_converse.rosterview.update).toHaveBeenCalled(); - expect(u.isVisible(_converse.rosterview.el)).toBe(true); - expect(sizzle('li', _converse.rosterview.el).filter(u.isVisible).length).toBe(3); - expect(sizzle('ul.roster-group-contacts', _converse.rosterview.el).filter(u.isVisible).length).toBe(1); - done(); - })); + it("do not have a header if there aren't any", + mock.initConverse( + ['rosterGroupsFetched', 'VCardsInitialized'], {}, + async function (done, _converse) { - it("can be removed by the user", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 0); + const name = mock.pend_names[0]; + _converse.roster.create({ + jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', + subscription: 'none', + ask: 'subscribe', + fullname: name + }); + await u.waitUntil(() => { + const el = _converse.rosterview.get('Pending contacts').el; + return u.isVisible(el) && _.filter(el.querySelectorAll('li'), li => u.isVisible(li)).length; + }, 700) - await test_utils.waitForRoster(_converse, 'all'); - await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname')))); - const name = mock.pend_names[0]; - const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const contact = _converse.roster.get(jid); - var sent_IQ; - spyOn(window, 'confirm').and.returnValue(true); - spyOn(contact, 'unauthorize').and.callFake(function () { return contact; }); - spyOn(contact, 'removeFromRoster').and.callThrough(); - await u.waitUntil(() => sizzle(".pending-contact-name:contains('"+name+"')", _converse.rosterview.el).length, 700); - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) { - sent_IQ = iq; - callback(); - }); + const remove_el = await u.waitUntil(() => sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, _converse.rosterview.el).pop()); + spyOn(window, 'confirm').and.returnValue(true); + remove_el.click(); + expect(window.confirm).toHaveBeenCalled(); + + const iq = _converse.connection.IQ_stanzas.pop(); + expect(Strophe.serialize(iq)).toBe( + ``+ + ``+ + ``+ + ``+ + ``); + + const stanza = u.toStanza(``); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await u.waitUntil(() => !u.isVisible(_converse.rosterview.get('Pending contacts').el)); + done(); + })); + + it("is shown when a new private message is received", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'all'); + await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname')))); + await u.waitUntil(() => _converse.roster.at(0).vcard.get('fullname')) + spyOn(window, 'confirm').and.returnValue(true); + for (var i=0; i (sizzle(".pending-contact-name:contains('"+name+"')", _converse.rosterview.el).length === 0), 1000); - expect(window.confirm).toHaveBeenCalled(); - expect(contact.removeFromRoster).toHaveBeenCalled(); - expect(sent_IQ.toLocaleString()).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - done(); - })); - - it("do not have a header if there aren't any", - mock.initConverse( - ['rosterGroupsFetched', 'VCardsInitialized'], {}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current', 0); - const name = mock.pend_names[0]; - _converse.roster.create({ - jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', - subscription: 'none', - ask: 'subscribe', - fullname: name - }); - await u.waitUntil(() => { - const el = _converse.rosterview.get('Pending contacts').el; - return u.isVisible(el) && _.filter(el.querySelectorAll('li'), li => u.isVisible(li)).length; - }, 700) - - const remove_el = await u.waitUntil(() => sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, _converse.rosterview.el).pop()); - spyOn(window, 'confirm').and.returnValue(true); - remove_el.click(); - expect(window.confirm).toHaveBeenCalled(); - - const iq = _converse.connection.IQ_stanzas.pop(); - expect(Strophe.serialize(iq)).toBe( - ``+ - ``+ - ``+ - ``+ - ``); - - const stanza = u.toStanza(``); - _converse.connection._dataRecv(test_utils.createRequest(stanza)); - await u.waitUntil(() => !u.isVisible(_converse.rosterview.get('Pending contacts').el)); - done(); - })); - - it("is shown when a new private message is received", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'all'); - await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname')))); - await u.waitUntil(() => _converse.roster.at(0).vcard.get('fullname')) - spyOn(window, 'confirm').and.returnValue(true); - for (var i=0; i u.waitUntil(() => contact.vcard.get('fullname')))); - spyOn(_converse.rosterview, 'update').and.callThrough(); - let i; - for (i=0; i sizzle('li', _converse.rosterview.get('Pending contacts').el).filter(u.isVisible).length, 900); - // Check that they are sorted alphabetically - const view = _converse.rosterview.get('Pending contacts'); - const spans = view.el.querySelectorAll('.pending-xmpp-contact span'); - const t = _.reduce(spans, (result, value) => result + _.trim(value.textContent), ''); - expect(t).toEqual(mock.pend_names.slice(0,i+1).sort().join('')); - done(); - })); - }); - - describe("Existing Contacts", function () { - async function _addContacts (_converse) { - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); - await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname')))); } + expect(u.isVisible(_converse.rosterview.get('Pending contacts').el)).toBe(false); + done(); + })); - it("can be collapsed under their own header", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("can be added to the roster and they will be sorted alphabetically", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - await _addContacts(_converse); - await u.waitUntil(() => sizzle('li', _converse.rosterview.el).filter(u.isVisible).length, 500); - await checkHeaderToggling.apply(_converse, [_converse.rosterview.el.querySelector('.roster-group')]); - done(); - })); - - it("will be hidden when appearing under a collapsed group", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - _converse.roster_groups = false; - await _addContacts(_converse); - await u.waitUntil(() => sizzle('li', _converse.rosterview.el).filter(u.isVisible).length, 500); - _converse.rosterview.el.querySelector('.roster-group a.group-toggle').click(); - const name = "Romeo Montague"; - const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current'); + await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname')))); + spyOn(_converse.rosterview, 'update').and.callThrough(); + let i; + for (i=0; i sizzle('li', _converse.rosterview.get('Pending contacts').el).filter(u.isVisible).length, 900); + // Check that they are sorted alphabetically + const view = _converse.rosterview.get('Pending contacts'); + const spans = view.el.querySelectorAll('.pending-xmpp-contact span'); + const t = _.reduce(spans, (result, value) => result + _.trim(value.textContent), ''); + expect(t).toEqual(mock.pend_names.slice(0,i+1).sort().join('')); + done(); + })); + }); - it("can be added to the roster and they will be sorted alphabetically", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + describe("Existing Contacts", function () { + async function _addContacts (_converse) { + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); + await Promise.all(_converse.roster.map(contact => u.waitUntil(() => contact.vcard.get('fullname')))); + } - await test_utils.openControlBox(_converse); - spyOn(_converse.rosterview, 'update').and.callThrough(); - await Promise.all(mock.cur_names.map(name => { - const contact = _converse.roster.create({ - jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', - subscription: 'both', - ask: null, - fullname: name - }); - return u.waitUntil(() => contact.initialized); - })); - await u.waitUntil(() => sizzle('li', _converse.rosterview.el).length, 600); - // Check that they are sorted alphabetically - const els = sizzle('.roster-group .current-xmpp-contact.offline a.open-chat', _converse.rosterview.el) - const t = els.reduce((result, value) => (result + value.textContent.trim()), ''); - expect(t).toEqual(mock.cur_names.slice(0,mock.cur_names.length).sort().join('')); - done(); - })); + it("can be collapsed under their own header", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - it("can be removed by the user", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + await _addContacts(_converse); + await u.waitUntil(() => sizzle('li', _converse.rosterview.el).filter(u.isVisible).length, 500); + await checkHeaderToggling.apply(_converse, [_converse.rosterview.el.querySelector('.roster-group')]); + done(); + })); - await _addContacts(_converse); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('li').length); - const name = mock.cur_names[0]; - const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const contact = _converse.roster.get(jid); - spyOn(window, 'confirm').and.returnValue(true); - spyOn(contact, 'removeFromRoster').and.callThrough(); + it("will be hidden when appearing under a collapsed group", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - let sent_IQ; - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) { - sent_IQ = iq; - callback(); - }); - sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, _converse.rosterview.el).pop().click(); - expect(window.confirm).toHaveBeenCalled(); - expect(sent_IQ.toLocaleString()).toBe( - ``+ - ``+ - ``); - expect(contact.removeFromRoster).toHaveBeenCalled(); - await u.waitUntil(() => sizzle(".open-chat:contains('"+name+"')", _converse.rosterview.el).length === 0); - done(); - })); + _converse.roster_groups = false; + await _addContacts(_converse); + await u.waitUntil(() => sizzle('li', _converse.rosterview.el).filter(u.isVisible).length, 500); + _converse.rosterview.el.querySelector('.roster-group a.group-toggle').click(); + const name = "Romeo Montague"; + const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + _converse.roster.create({ + ask: null, + fullname: name, + jid: jid, + requesting: false, + subscription: 'both' + }); + const view = _converse.rosterview.get('My contacts').get(jid); + expect(u.isVisible(view.el)).toBe(false); + done(); + })); - it("do not have a header if there aren't any", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("can be added to the roster and they will be sorted alphabetically", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current', 0); - const name = mock.cur_names[0]; + await mock.openControlBox(_converse); + spyOn(_converse.rosterview, 'update').and.callThrough(); + await Promise.all(mock.cur_names.map(name => { const contact = _converse.roster.create({ jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', subscription: 'both', ask: null, fullname: name }); - await u.waitUntil(() => sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).map(e => e.querySelector('li')).length, 1000); - spyOn(window, 'confirm').and.returnValue(true); - spyOn(contact, 'removeFromRoster').and.callThrough(); - spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) { - if (typeof callback === "function") { return callback(); } - }); - expect(u.isVisible(_converse.rosterview.el.querySelector('.roster-group'))).toBe(true); - sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, _converse.rosterview.el).pop().click(); - expect(window.confirm).toHaveBeenCalled(); - expect(_converse.connection.sendIQ).toHaveBeenCalled(); - expect(contact.removeFromRoster).toHaveBeenCalled(); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length === 0); - done(); + return u.waitUntil(() => contact.initialized); })); + await u.waitUntil(() => sizzle('li', _converse.rosterview.el).length, 600); + // Check that they are sorted alphabetically + const els = sizzle('.roster-group .current-xmpp-contact.offline a.open-chat', _converse.rosterview.el) + const t = els.reduce((result, value) => (result + value.textContent.trim()), ''); + expect(t).toEqual(mock.cur_names.slice(0,mock.cur_names.length).sort().join('')); + done(); + })); - it("can change their status to online and be sorted alphabetically", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("can be removed by the user", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - await _addContacts(_converse); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length, 700); - const roster = _converse.rosterview.el; - const groups = roster.querySelectorAll('.roster-group'); - const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); - expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); - for (let i=0; i result + _.trim(value.textContent), ''); - expect(t).toEqual(mock.groups_map[groupname].slice(0, els.length).sort().join('')); - } - } - done(); - })); + await _addContacts(_converse); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('li').length); + const name = mock.cur_names[0]; + const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const contact = _converse.roster.get(jid); + spyOn(window, 'confirm').and.returnValue(true); + spyOn(contact, 'removeFromRoster').and.callThrough(); - it("can change their status to busy and be sorted alphabetically", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + let sent_IQ; + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) { + sent_IQ = iq; + callback(); + }); + sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, _converse.rosterview.el).pop().click(); + expect(window.confirm).toHaveBeenCalled(); + expect(sent_IQ.toLocaleString()).toBe( + ``+ + ``+ + ``); + expect(contact.removeFromRoster).toHaveBeenCalled(); + await u.waitUntil(() => sizzle(".open-chat:contains('"+name+"')", _converse.rosterview.el).length === 0); + done(); + })); - await _addContacts(_converse); - await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 700); - const roster = _converse.rosterview.el; - const groups = roster.querySelectorAll('.roster-group'); - const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); - expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); - for (let i=0; i result + _.trim(value.textContent), ''); - expect(t).toEqual(mock.groups_map[groupname].slice(0, els.length).sort().join('')); - } - } - done(); - })); + it("do not have a header if there aren't any", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - it("can change their status to away and be sorted alphabetically", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 0); + const name = mock.cur_names[0]; + const contact = _converse.roster.create({ + jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', + subscription: 'both', + ask: null, + fullname: name + }); + await u.waitUntil(() => sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).map(e => e.querySelector('li')).length, 1000); + spyOn(window, 'confirm').and.returnValue(true); + spyOn(contact, 'removeFromRoster').and.callThrough(); + spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback) { + if (typeof callback === "function") { return callback(); } + }); + expect(u.isVisible(_converse.rosterview.el.querySelector('.roster-group'))).toBe(true); + sizzle(`.remove-xmpp-contact[title="Click to remove ${name} as a contact"]`, _converse.rosterview.el).pop().click(); + expect(window.confirm).toHaveBeenCalled(); + expect(_converse.connection.sendIQ).toHaveBeenCalled(); + expect(contact.removeFromRoster).toHaveBeenCalled(); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length === 0); + done(); + })); - await _addContacts(_converse); - await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 700); - const roster = _converse.rosterview.el; - const groups = roster.querySelectorAll('.roster-group'); - const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); - expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); - for (let i=0; i result + _.trim(value.textContent), ''); - expect(t).toEqual(mock.groups_map[groupname].slice(0, els.length).sort().join('')); - } - } - done(); - })); + it("can change their status to online and be sorted alphabetically", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - it("can change their status to xa and be sorted alphabetically", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await _addContacts(_converse); - await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 700); - const roster = _converse.rosterview.el; - const groups = roster.querySelectorAll('.roster-group'); - const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); - expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); - for (let i=0; i result + _.trim(value.textContent), ''); - expect(t).toEqual(mock.groups_map[groupname].slice(0, els.length).sort().join('')); - } - } - done(); - })); - - it("can change their status to unavailable and be sorted alphabetically", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await _addContacts(_converse); - await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 500) - const roster = _converse.rosterview.el; - const groups = roster.querySelectorAll('.roster-group'); - const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); - expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); - for (let i=0; i result + _.trim(value.textContent), ''); - expect(t).toEqual(mock.groups_map[groupname].slice(0, els.length).sort().join('')); - } - } - done(); - })); - - it("are ordered according to status: online, busy, away, xa, unavailable, offline", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await _addContacts(_converse); - await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 700); - let i, jid; - for (i=0; i<3; i++) { - jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - _converse.roster.get(jid).presence.set('show', 'online'); - } - for (i=3; i<6; i++) { - jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - _converse.roster.get(jid).presence.set('show', 'dnd'); - } - for (i=6; i<9; i++) { - jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - _converse.roster.get(jid).presence.set('show', 'away'); - } - for (i=9; i<12; i++) { - jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - _converse.roster.get(jid).presence.set('show', 'xa'); - } - for (i=12; i<15; i++) { - jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - _converse.roster.get(jid).presence.set('show', 'unavailable'); - } - - await u.waitUntil(() => u.isVisible(_converse.rosterview.el.querySelector('li:first-child')), 900); - const roster = _converse.rosterview.el; - const groups = roster.querySelectorAll('.roster-group'); - const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); - expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); + await _addContacts(_converse); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length, 700); + const roster = _converse.rosterview.el; + const groups = roster.querySelectorAll('.roster-group'); + const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); + expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); + for (let i=0; i e.getAttribute('data-status')); - const subscription_classes = els.map(e => e.classList[3]); - const status_classes = els.map(e => e.classList[4]); - expect(statuses.join(" ")).toBe("online online away xa xa xa"); - expect(status_classes.join(" ")).toBe("online online away xa xa xa"); - expect(subscription_classes.join(" ")).toBe("both both both both both both"); - } else if (groupname === "friends & acquaintences") { - const statuses = els.map(e => e.getAttribute('data-status')); - const subscription_classes = els.map(e => e.classList[3]); - const status_classes = els.map(e => e.classList[4]); - expect(statuses.join(" ")).toBe("online online dnd dnd away unavailable"); - expect(status_classes.join(" ")).toBe("online online dnd dnd away unavailable"); - expect(subscription_classes.join(" ")).toBe("both both both both both both"); - } else if (groupname === "Family") { - const statuses = els.map(e => e.getAttribute('data-status')); - const subscription_classes = els.map(e => e.classList[3]); - const status_classes = els.map(e => e.classList[4]); - expect(statuses.join(" ")).toBe("online dnd"); - expect(status_classes.join(" ")).toBe("online dnd"); - expect(subscription_classes.join(" ")).toBe("both both"); - } else if (groupname === "ænemies") { - const statuses = els.map(e => e.getAttribute('data-status')); - const subscription_classes = els.map(e => e.classList[3]); - const status_classes = els.map(e => e.classList[4]); - expect(statuses.join(" ")).toBe("away"); - expect(status_classes.join(" ")).toBe("away"); - expect(subscription_classes.join(" ")).toBe("both"); - } else if (groupname === "Ungrouped") { - const statuses = els.map(e => e.getAttribute('data-status')); - const subscription_classes = els.map(e => e.classList[3]); - const status_classes = els.map(e => e.classList[4]); - expect(statuses.join(" ")).toBe("unavailable unavailable"); - expect(status_classes.join(" ")).toBe("unavailable unavailable"); - expect(subscription_classes.join(" ")).toBe("both both"); - } + const els = group.querySelectorAll('.current-xmpp-contact.online a.open-chat'); + const t = _.reduce(els, (result, value) => result + _.trim(value.textContent), ''); + expect(t).toEqual(mock.groups_map[groupname].slice(0, els.length).sort().join('')); } - done(); - })); - }); + } + done(); + })); - describe("Requesting Contacts", function () { + it("can change their status to busy and be sorted alphabetically", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - it("can be added to the roster and they will be sorted alphabetically", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - test_utils.openControlBox(_converse); - let names = []; - const addName = function (item) { - if (!u.hasClass('request-actions', item)) { - names.push(item.textContent.replace(/^\s+|\s+$/g, '')); - } - }; - spyOn(_converse.rosterview, 'update').and.callThrough(); - await Promise.all(mock.req_names.map(name => { - const contact = _converse.roster.create({ - jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', - subscription: 'none', - ask: null, - requesting: true, - nickname: name - }); - return u.waitUntil(() => contact.initialized); - })); - await u.waitUntil(() => _converse.rosterview.get('Contact requests').el.querySelectorAll('li').length, 700); - expect(_converse.rosterview.update).toHaveBeenCalled(); + await _addContacts(_converse); + await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 700); + const roster = _converse.rosterview.el; + const groups = roster.querySelectorAll('.roster-group'); + const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); + expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); + for (let i=0; i sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).length, 900); - expect(u.isVisible(_converse.rosterview.get('Contact requests').el)).toEqual(true); - expect(sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).map(e => e.querySelector('li')).length).toBe(1); - sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).map(e => e.querySelector('li .decline-xmpp-request'))[0].click(); - expect(window.confirm).toHaveBeenCalled(); - expect(u.isVisible(_converse.rosterview.get('Contact requests').el)).toEqual(false); - done(); - })); - - it("can be collapsed under their own header", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 0); - test_utils.createContacts(_converse, 'requesting'); - await test_utils.openControlBox(_converse); - await u.waitUntil(() => sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).length, 700); - await checkHeaderToggling.apply( - _converse, - [_converse.rosterview.get('Contact requests').el] - ); - done(); - })); - - it("can have their requests accepted by the user", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.openControlBox(_converse); - await test_utils.waitForRoster(_converse, 'current', 0); - await test_utils.createContacts(_converse, 'requesting'); - const name = mock.req_names.sort()[0]; - const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const contact = _converse.roster.get(jid); - spyOn(contact, 'authorize').and.callFake(() => contact); - await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length) - // TODO: Testing can be more thorough here, the user is - // actually not accepted/authorized because of - // mock_connection. - spyOn(_converse.roster, 'sendContactAddIQ').and.callFake(() => Promise.resolve()); - const req_contact = sizzle(`.req-contact-name:contains("${contact.getDisplayName()}")`, _converse.rosterview.el).pop(); - req_contact.parentElement.parentElement.querySelector('.accept-xmpp-request').click(); - expect(_converse.roster.sendContactAddIQ).toHaveBeenCalled(); - await u.waitUntil(() => contact.authorize.calls.count()); - expect(contact.authorize).toHaveBeenCalled(); - done(); - })); - - it("can have their requests denied by the user", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 0); - await test_utils.createContacts(_converse, 'requesting'); - await test_utils.openControlBox(_converse); - await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 700); - _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced. - const name = mock.req_names.sort()[1]; - const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const contact = _converse.roster.get(jid); - spyOn(window, 'confirm').and.returnValue(true); - spyOn(contact, 'unauthorize').and.callFake(function () { return contact; }); - const req_contact = await u.waitUntil(() => sizzle(".req-contact-name:contains('"+name+"')", _converse.rosterview.el).pop()); - req_contact.parentElement.parentElement.querySelector('.decline-xmpp-request').click(); - expect(window.confirm).toHaveBeenCalled(); - expect(contact.unauthorize).toHaveBeenCalled(); - // There should now be one less contact - expect(_converse.roster.length).toEqual(mock.req_names.length-1); - done(); - })); - - it("are persisted even if other contacts' change their presence ", mock.initConverse( - ['rosterGroupsFetched'], {}, async function (done, _converse) { - - /* This is a regression test. - * https://github.com/jcbrand/_converse.js/issues/262 - */ - expect(_converse.roster.pluck('jid').length).toBe(0); - - const sent_IQs = _converse.connection.IQ_stanzas; - const stanza = await u.waitUntil(() => _.filter(sent_IQs, iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop()); - // Taken from the spec - // https://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3 - const result = $iq({ - to: _converse.connection.jid, - type: 'result', - id: stanza.getAttribute('id') - }).c('query', { - xmlns: 'jabber:iq:roster', - }).c('item', { - jid: 'juliet@example.net', - name: 'Juliet', - subscription:'both' - }).c('group').t('Friends').up().up() - .c('item', { - jid: 'mercutio@example.org', - name: 'Mercutio', - subscription:'from' - }).c('group').t('Friends').up().up() - _converse.connection._dataRecv(test_utils.createRequest(result)); - - const pres = $pres({from: 'data@enterprise/resource', type: 'subscribe'}); - _converse.connection._dataRecv(test_utils.createRequest(pres)); - expect(_converse.roster.pluck('jid').length).toBe(1); - await u.waitUntil(() => sizzle('a:contains("Contact requests")', _converse.rosterview.el).length, 700); - expect(_converse.roster.pluck('jid').includes('data@enterprise')).toBeTruthy(); - - const roster_push = $iq({ - 'to': _converse.connection.jid, - 'type': 'set', - }).c('query', {'xmlns': 'jabber:iq:roster', 'ver': 'ver34'}) - .c('item', { - jid: 'benvolio@example.org', - name: 'Benvolio', - subscription:'both' - }).c('group').t('Friends'); - _converse.connection._dataRecv(test_utils.createRequest(roster_push)); - expect(_converse.roster.data.get('version')).toBe('ver34'); - expect(_converse.roster.models.length).toBe(4); - expect(_converse.roster.pluck('jid').includes('data@enterprise')).toBeTruthy(); - done(); - })); - }); - - describe("All Contacts", function () { - - it("are saved to, and can be retrieved from browserStorage", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - await test_utils.waitForRoster(_converse, 'current', 0); - await test_utils.createContacts(_converse, 'requesting'); - await test_utils.openControlBox(_converse); - var new_attrs, old_attrs, attrs; - var num_contacts = _converse.roster.length; - var new_roster = new _converse.RosterContacts(); - // Roster items are yet to be fetched from browserStorage - expect(new_roster.length).toEqual(0); - new_roster.browserStorage = _converse.roster.browserStorage; - await new Promise(success => new_roster.fetch({success})); - expect(new_roster.length).toEqual(num_contacts); - // Check that the roster items retrieved from browserStorage - // have the same attributes values as the original ones. - attrs = ['jid', 'fullname', 'subscription', 'ask']; - for (var i=0; i result + _.trim(value.textContent), ''); + expect(t).toEqual(mock.groups_map[groupname].slice(0, els.length).sort().join('')); } - done(); - })); + } + done(); + })); - it("will show fullname and jid properties on tooltip", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + it("can change their status to away and be sorted alphabetically", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { - await test_utils.waitForRoster(_converse, 'current', 'all'); - await test_utils.createContacts(_converse, 'requesting'); - await test_utils.openControlBox(_converse); - await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 700); - await Promise.all(mock.cur_names.map(async name => { - const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const el = await u.waitUntil(() => sizzle("li:contains('"+name+"')", _converse.rosterview.el).pop()); - const child = el.firstElementChild; - expect(child.textContent.trim()).toBe(name); - expect(child.getAttribute('title')).toContain(name); - expect(child.getAttribute('title')).toContain(jid); - })); - await Promise.all(mock.req_names.map(async name => { - const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; - const el = await u.waitUntil(() => sizzle("li:contains('"+name+"')", _converse.rosterview.el).pop()); - const child = el.firstElementChild; - expect(child.textContent.trim()).toBe(name); - expect(child.firstElementChild.getAttribute('title')).toContain(jid); - })); - done(); + await _addContacts(_converse); + await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 700); + const roster = _converse.rosterview.el; + const groups = roster.querySelectorAll('.roster-group'); + const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); + expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); + for (let i=0; i result + _.trim(value.textContent), ''); + expect(t).toEqual(mock.groups_map[groupname].slice(0, els.length).sort().join('')); + } + } + done(); + })); + + it("can change their status to xa and be sorted alphabetically", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await _addContacts(_converse); + await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 700); + const roster = _converse.rosterview.el; + const groups = roster.querySelectorAll('.roster-group'); + const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); + expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); + for (let i=0; i result + _.trim(value.textContent), ''); + expect(t).toEqual(mock.groups_map[groupname].slice(0, els.length).sort().join('')); + } + } + done(); + })); + + it("can change their status to unavailable and be sorted alphabetically", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await _addContacts(_converse); + await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 500) + const roster = _converse.rosterview.el; + const groups = roster.querySelectorAll('.roster-group'); + const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); + expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); + for (let i=0; i result + _.trim(value.textContent), ''); + expect(t).toEqual(mock.groups_map[groupname].slice(0, els.length).sort().join('')); + } + } + done(); + })); + + it("are ordered according to status: online, busy, away, xa, unavailable, offline", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await _addContacts(_converse); + await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 700); + let i, jid; + for (i=0; i<3; i++) { + jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + _converse.roster.get(jid).presence.set('show', 'online'); + } + for (i=3; i<6; i++) { + jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + _converse.roster.get(jid).presence.set('show', 'dnd'); + } + for (i=6; i<9; i++) { + jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + _converse.roster.get(jid).presence.set('show', 'away'); + } + for (i=9; i<12; i++) { + jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + _converse.roster.get(jid).presence.set('show', 'xa'); + } + for (i=12; i<15; i++) { + jid = mock.cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + _converse.roster.get(jid).presence.set('show', 'unavailable'); + } + + await u.waitUntil(() => u.isVisible(_converse.rosterview.el.querySelector('li:first-child')), 900); + const roster = _converse.rosterview.el; + const groups = roster.querySelectorAll('.roster-group'); + const groupnames = Array.from(groups).map(g => g.getAttribute('data-group')); + expect(groupnames.join(' ')).toBe("Colleagues Family friends & acquaintences ænemies Ungrouped"); + for (let j=0; j e.getAttribute('data-status')); + const subscription_classes = els.map(e => e.classList[3]); + const status_classes = els.map(e => e.classList[4]); + expect(statuses.join(" ")).toBe("online online away xa xa xa"); + expect(status_classes.join(" ")).toBe("online online away xa xa xa"); + expect(subscription_classes.join(" ")).toBe("both both both both both both"); + } else if (groupname === "friends & acquaintences") { + const statuses = els.map(e => e.getAttribute('data-status')); + const subscription_classes = els.map(e => e.classList[3]); + const status_classes = els.map(e => e.classList[4]); + expect(statuses.join(" ")).toBe("online online dnd dnd away unavailable"); + expect(status_classes.join(" ")).toBe("online online dnd dnd away unavailable"); + expect(subscription_classes.join(" ")).toBe("both both both both both both"); + } else if (groupname === "Family") { + const statuses = els.map(e => e.getAttribute('data-status')); + const subscription_classes = els.map(e => e.classList[3]); + const status_classes = els.map(e => e.classList[4]); + expect(statuses.join(" ")).toBe("online dnd"); + expect(status_classes.join(" ")).toBe("online dnd"); + expect(subscription_classes.join(" ")).toBe("both both"); + } else if (groupname === "ænemies") { + const statuses = els.map(e => e.getAttribute('data-status')); + const subscription_classes = els.map(e => e.classList[3]); + const status_classes = els.map(e => e.classList[4]); + expect(statuses.join(" ")).toBe("away"); + expect(status_classes.join(" ")).toBe("away"); + expect(subscription_classes.join(" ")).toBe("both"); + } else if (groupname === "Ungrouped") { + const statuses = els.map(e => e.getAttribute('data-status')); + const subscription_classes = els.map(e => e.classList[3]); + const status_classes = els.map(e => e.classList[4]); + expect(statuses.join(" ")).toBe("unavailable unavailable"); + expect(status_classes.join(" ")).toBe("unavailable unavailable"); + expect(subscription_classes.join(" ")).toBe("both both"); + } + } + done(); + })); + }); + + describe("Requesting Contacts", function () { + + it("can be added to the roster and they will be sorted alphabetically", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + mock.openControlBox(_converse); + let names = []; + const addName = function (item) { + if (!u.hasClass('request-actions', item)) { + names.push(item.textContent.replace(/^\s+|\s+$/g, '')); + } + }; + spyOn(_converse.rosterview, 'update').and.callThrough(); + await Promise.all(mock.req_names.map(name => { + const contact = _converse.roster.create({ + jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', + subscription: 'none', + ask: null, + requesting: true, + nickname: name + }); + return u.waitUntil(() => contact.initialized); })); - }); + await u.waitUntil(() => _converse.rosterview.get('Contact requests').el.querySelectorAll('li').length, 700); + expect(_converse.rosterview.update).toHaveBeenCalled(); + // Check that they are sorted alphabetically + const children = _converse.rosterview.get('Contact requests').el.querySelectorAll('.requesting-xmpp-contact span'); + names = []; + Array.from(children).forEach(addName); + expect(names.join('')).toEqual(mock.req_names.slice(0,mock.req_names.length+1).sort().join('')); + done(); + })); + + it("do not have a header if there aren't any", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, "current", 0); + const name = mock.req_names[0]; + spyOn(window, 'confirm').and.returnValue(true); + _converse.roster.create({ + 'jid': name.replace(/ /g,'.').toLowerCase() + '@montague.lit', + 'subscription': 'none', + 'ask': null, + 'requesting': true, + 'nickname': name + }); + await u.waitUntil(() => sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).length, 900); + expect(u.isVisible(_converse.rosterview.get('Contact requests').el)).toEqual(true); + expect(sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).map(e => e.querySelector('li')).length).toBe(1); + sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).map(e => e.querySelector('li .decline-xmpp-request'))[0].click(); + expect(window.confirm).toHaveBeenCalled(); + expect(u.isVisible(_converse.rosterview.get('Contact requests').el)).toEqual(false); + done(); + })); + + it("can be collapsed under their own header", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 0); + mock.createContacts(_converse, 'requesting'); + await mock.openControlBox(_converse); + await u.waitUntil(() => sizzle('.roster-group', _converse.rosterview.el).filter(u.isVisible).length, 700); + await checkHeaderToggling.apply( + _converse, + [_converse.rosterview.get('Contact requests').el] + ); + done(); + })); + + it("can have their requests accepted by the user", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.openControlBox(_converse); + await mock.waitForRoster(_converse, 'current', 0); + await mock.createContacts(_converse, 'requesting'); + const name = mock.req_names.sort()[0]; + const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const contact = _converse.roster.get(jid); + spyOn(contact, 'authorize').and.callFake(() => contact); + await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group li').length) + // TODO: Testing can be more thorough here, the user is + // actually not accepted/authorized because of + // mock_connection. + spyOn(_converse.roster, 'sendContactAddIQ').and.callFake(() => Promise.resolve()); + const req_contact = sizzle(`.req-contact-name:contains("${contact.getDisplayName()}")`, _converse.rosterview.el).pop(); + req_contact.parentElement.parentElement.querySelector('.accept-xmpp-request').click(); + expect(_converse.roster.sendContactAddIQ).toHaveBeenCalled(); + await u.waitUntil(() => contact.authorize.calls.count()); + expect(contact.authorize).toHaveBeenCalled(); + done(); + })); + + it("can have their requests denied by the user", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 0); + await mock.createContacts(_converse, 'requesting'); + await mock.openControlBox(_converse); + await u.waitUntil(() => sizzle('.roster-group li', _converse.rosterview.el).length, 700); + _converse.rosterview.update(); // XXX: Hack to make sure $roster element is attaced. + const name = mock.req_names.sort()[1]; + const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const contact = _converse.roster.get(jid); + spyOn(window, 'confirm').and.returnValue(true); + spyOn(contact, 'unauthorize').and.callFake(function () { return contact; }); + const req_contact = await u.waitUntil(() => sizzle(".req-contact-name:contains('"+name+"')", _converse.rosterview.el).pop()); + req_contact.parentElement.parentElement.querySelector('.decline-xmpp-request').click(); + expect(window.confirm).toHaveBeenCalled(); + expect(contact.unauthorize).toHaveBeenCalled(); + // There should now be one less contact + expect(_converse.roster.length).toEqual(mock.req_names.length-1); + done(); + })); + + it("are persisted even if other contacts' change their presence ", mock.initConverse( + ['rosterGroupsFetched'], {}, async function (done, _converse) { + + /* This is a regression test. + * https://github.com/jcbrand/_converse.js/issues/262 + */ + expect(_converse.roster.pluck('jid').length).toBe(0); + + const sent_IQs = _converse.connection.IQ_stanzas; + const stanza = await u.waitUntil(() => _.filter(sent_IQs, iq => iq.querySelector('iq query[xmlns="jabber:iq:roster"]')).pop()); + // Taken from the spec + // https://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3 + const result = $iq({ + to: _converse.connection.jid, + type: 'result', + id: stanza.getAttribute('id') + }).c('query', { + xmlns: 'jabber:iq:roster', + }).c('item', { + jid: 'juliet@example.net', + name: 'Juliet', + subscription:'both' + }).c('group').t('Friends').up().up() + .c('item', { + jid: 'mercutio@example.org', + name: 'Mercutio', + subscription:'from' + }).c('group').t('Friends').up().up() + _converse.connection._dataRecv(mock.createRequest(result)); + + const pres = $pres({from: 'data@enterprise/resource', type: 'subscribe'}); + _converse.connection._dataRecv(mock.createRequest(pres)); + expect(_converse.roster.pluck('jid').length).toBe(1); + await u.waitUntil(() => sizzle('a:contains("Contact requests")', _converse.rosterview.el).length, 700); + expect(_converse.roster.pluck('jid').includes('data@enterprise')).toBeTruthy(); + + const roster_push = $iq({ + 'to': _converse.connection.jid, + 'type': 'set', + }).c('query', {'xmlns': 'jabber:iq:roster', 'ver': 'ver34'}) + .c('item', { + jid: 'benvolio@example.org', + name: 'Benvolio', + subscription:'both' + }).c('group').t('Friends'); + _converse.connection._dataRecv(mock.createRequest(roster_push)); + expect(_converse.roster.data.get('version')).toBe('ver34'); + expect(_converse.roster.models.length).toBe(4); + expect(_converse.roster.pluck('jid').includes('data@enterprise')).toBeTruthy(); + done(); + })); + }); + + describe("All Contacts", function () { + + it("are saved to, and can be retrieved from browserStorage", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.waitForRoster(_converse, 'current', 0); + await mock.createContacts(_converse, 'requesting'); + await mock.openControlBox(_converse); + var new_attrs, old_attrs, attrs; + var num_contacts = _converse.roster.length; + var new_roster = new _converse.RosterContacts(); + // Roster items are yet to be fetched from browserStorage + expect(new_roster.length).toEqual(0); + new_roster.browserStorage = _converse.roster.browserStorage; + await new Promise(success => new_roster.fetch({success})); + expect(new_roster.length).toEqual(num_contacts); + // Check that the roster items retrieved from browserStorage + // have the same attributes values as the original ones. + attrs = ['jid', 'fullname', 'subscription', 'ask']; + for (var i=0; i sizzle('.roster-group li', _converse.rosterview.el).length, 700); + await Promise.all(mock.cur_names.map(async name => { + const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const el = await u.waitUntil(() => sizzle("li:contains('"+name+"')", _converse.rosterview.el).pop()); + const child = el.firstElementChild; + expect(child.textContent.trim()).toBe(name); + expect(child.getAttribute('title')).toContain(name); + expect(child.getAttribute('title')).toContain(jid); + })); + await Promise.all(mock.req_names.map(async name => { + const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + const el = await u.waitUntil(() => sizzle("li:contains('"+name+"')", _converse.rosterview.el).pop()); + const child = el.firstElementChild; + expect(child.textContent.trim()).toBe(name); + expect(child.firstElementChild.getAttribute('title')).toContain(jid); + })); + done(); + })); }); }); diff --git a/spec/smacks.js b/spec/smacks.js index 737199658..53720e79d 100644 --- a/spec/smacks.js +++ b/spec/smacks.js @@ -1,282 +1,280 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const $iq = converse.env.$iq; - const $msg = converse.env.$msg; - const Strophe = converse.env.Strophe; - const sizzle = converse.env.sizzle; - const u = converse.env.utils; +/*global mock */ - describe("XEP-0198 Stream Management", function () { +const $iq = converse.env.$iq; +const $msg = converse.env.$msg; +const Strophe = converse.env.Strophe; +const sizzle = converse.env.sizzle; +const u = converse.env.utils; - it("gets enabled with an stanza and resumed with a stanza", - mock.initConverse( - ['chatBoxesInitialized'], - { 'auto_login': false, - 'enable_smacks': true, - 'show_controlbox_by_default': true, - 'smacks_max_unacked_stanzas': 2 - }, - async function (done, _converse) { +describe("XEP-0198 Stream Management", function () { - const view = _converse.chatboxviews.get('controlbox'); - spyOn(view, 'renderControlBoxPane').and.callThrough(); + it("gets enabled with an stanza and resumed with a stanza", + mock.initConverse( + ['chatBoxesInitialized'], + { 'auto_login': false, + 'enable_smacks': true, + 'show_controlbox_by_default': true, + 'smacks_max_unacked_stanzas': 2 + }, + async function (done, _converse) { - await _converse.api.user.login('romeo@montague.lit/orchard', 'secret'); - const sent_stanzas = _converse.connection.sent_stanzas; - let stanza = await u.waitUntil(() => - sent_stanzas.filter(s => (s.tagName === 'enable')).pop()); + const view = _converse.chatboxviews.get('controlbox'); + spyOn(view, 'renderControlBoxPane').and.callThrough(); - expect(_converse.session.get('smacks_enabled')).toBe(false); - expect(Strophe.serialize(stanza)).toEqual(''); + await _converse.api.user.login('romeo@montague.lit/orchard', 'secret'); + const sent_stanzas = _converse.connection.sent_stanzas; + let stanza = await u.waitUntil(() => + sent_stanzas.filter(s => (s.tagName === 'enable')).pop()); - let result = u.toStanza(``); - _converse.connection._dataRecv(test_utils.createRequest(result)); - expect(_converse.session.get('smacks_enabled')).toBe(true); + expect(_converse.session.get('smacks_enabled')).toBe(false); + expect(Strophe.serialize(stanza)).toEqual(''); - await u.waitUntil(() => view.renderControlBoxPane.calls.count()); + let result = u.toStanza(``); + _converse.connection._dataRecv(mock.createRequest(result)); + expect(_converse.session.get('smacks_enabled')).toBe(true); - let IQ_stanzas = _converse.connection.IQ_stanzas; - await u.waitUntil(() => IQ_stanzas.length === 4); + await u.waitUntil(() => view.renderControlBoxPane.calls.count()); - let iq = IQ_stanzas[IQ_stanzas.length-1]; - expect(Strophe.serialize(iq)).toBe( - ``); - await test_utils.waitForRoster(_converse, 'current', 1); - IQ_stanzas.pop(); + let IQ_stanzas = _converse.connection.IQ_stanzas; + await u.waitUntil(() => IQ_stanzas.length === 4); - const expected_IQs = disco_iq => ([ - ``+ - ``, + let iq = IQ_stanzas[IQ_stanzas.length-1]; + expect(Strophe.serialize(iq)).toBe( + ``); + await mock.waitForRoster(_converse, 'current', 1); + IQ_stanzas.pop(); - ``+ - ``, + const expected_IQs = disco_iq => ([ + ``+ + ``, - ``+ - ``]); + ``+ + ``, - const disco_iq = IQ_stanzas.pop(); - expect(expected_IQs(disco_iq).includes(Strophe.serialize(disco_iq))).toBe(true); - iq = IQ_stanzas.pop(); - expect(expected_IQs(disco_iq).includes(Strophe.serialize(disco_iq))).toBe(true); - iq = IQ_stanzas.pop(); - expect(expected_IQs(disco_iq).includes(Strophe.serialize(disco_iq))).toBe(true); + ``+ + ``]); - expect(sent_stanzas.filter(s => (s.nodeName === 'r')).length).toBe(2); - expect(_converse.session.get('unacked_stanzas').length).toBe(5); + const disco_iq = IQ_stanzas.pop(); + expect(expected_IQs(disco_iq).includes(Strophe.serialize(disco_iq))).toBe(true); + iq = IQ_stanzas.pop(); + expect(expected_IQs(disco_iq).includes(Strophe.serialize(disco_iq))).toBe(true); + iq = IQ_stanzas.pop(); + expect(expected_IQs(disco_iq).includes(Strophe.serialize(disco_iq))).toBe(true); - // test handling of acks - let ack = u.toStanza(``); - _converse.connection._dataRecv(test_utils.createRequest(ack)); - expect(_converse.session.get('unacked_stanzas').length).toBe(3); + expect(sent_stanzas.filter(s => (s.nodeName === 'r')).length).toBe(2); + expect(_converse.session.get('unacked_stanzas').length).toBe(5); - // test handling of ack requests - let r = u.toStanza(``); - _converse.connection._dataRecv(test_utils.createRequest(r)); + // test handling of acks + let ack = u.toStanza(``); + _converse.connection._dataRecv(mock.createRequest(ack)); + expect(_converse.session.get('unacked_stanzas').length).toBe(3); - ack = await u.waitUntil(() => sent_stanzas.filter(s => (s.nodeName === 'a')).pop()); - expect(Strophe.serialize(ack)).toBe(''); + // test handling of ack requests + let r = u.toStanza(``); + _converse.connection._dataRecv(mock.createRequest(r)); + + ack = await u.waitUntil(() => sent_stanzas.filter(s => (s.nodeName === 'a')).pop()); + expect(Strophe.serialize(ack)).toBe(''); - const disco_result = $iq({ - 'type': 'result', - 'from': 'montague.lit', - 'to': 'romeo@montague.lit/orchard', - 'id': disco_iq.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(disco_result)); + const disco_result = $iq({ + 'type': 'result', + 'from': 'montague.lit', + 'to': 'romeo@montague.lit/orchard', + 'id': disco_iq.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(disco_result)); - ack = u.toStanza(``); - _converse.connection._dataRecv(test_utils.createRequest(ack)); - expect(_converse.session.get('unacked_stanzas').length).toBe(2); + ack = u.toStanza(``); + _converse.connection._dataRecv(mock.createRequest(ack)); + expect(_converse.session.get('unacked_stanzas').length).toBe(2); - r = u.toStanza(``); - _converse.connection._dataRecv(test_utils.createRequest(r)); - ack = await u.waitUntil(() => sent_stanzas.filter(s => (s.nodeName === 'a' && s.getAttribute('h') === '1')).pop()); - expect(Strophe.serialize(ack)).toBe(''); - await _converse.api.waitUntil('rosterInitialized'); + r = u.toStanza(``); + _converse.connection._dataRecv(mock.createRequest(r)); + ack = await u.waitUntil(() => sent_stanzas.filter(s => (s.nodeName === 'a' && s.getAttribute('h') === '1')).pop()); + expect(Strophe.serialize(ack)).toBe(''); + await _converse.api.waitUntil('rosterInitialized'); - // test session resumption - _converse.connection.IQ_stanzas = []; - IQ_stanzas = _converse.connection.IQ_stanzas; - await _converse.api.connection.reconnect(); - stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop()); - expect(Strophe.serialize(stanza)).toEqual(''); + // test session resumption + _converse.connection.IQ_stanzas = []; + IQ_stanzas = _converse.connection.IQ_stanzas; + await _converse.api.connection.reconnect(); + stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop()); + expect(Strophe.serialize(stanza)).toEqual(''); - result = u.toStanza(``); - _converse.connection._dataRecv(test_utils.createRequest(result)); + result = u.toStanza(``); + _converse.connection._dataRecv(mock.createRequest(result)); - // Another stanza doesn't get sent out - expect(sent_stanzas.filter(s => (s.tagName === 'enable')).length).toBe(1); - expect(_converse.session.get('smacks_enabled')).toBe(true); + // Another stanza doesn't get sent out + expect(sent_stanzas.filter(s => (s.tagName === 'enable')).length).toBe(1); + expect(_converse.session.get('smacks_enabled')).toBe(true); - await new Promise(resolve => _converse.api.listen.once('reconnected', resolve)); - await u.waitUntil(() => IQ_stanzas.length === 1); + await new Promise(resolve => _converse.api.listen.once('reconnected', resolve)); + await u.waitUntil(() => IQ_stanzas.length === 1); - // Test that unacked stanzas get resent out - iq = IQ_stanzas.pop(); - expect(Strophe.serialize(iq)).toBe(``); + // Test that unacked stanzas get resent out + iq = IQ_stanzas.pop(); + expect(Strophe.serialize(iq)).toBe(``); - expect(IQ_stanzas.filter(iq => sizzle('query[xmlns="jabber:iq:roster"]', iq).pop()).length).toBe(0); - done(); - })); + expect(IQ_stanzas.filter(iq => sizzle('query[xmlns="jabber:iq:roster"]', iq).pop()).length).toBe(0); + done(); + })); - it("might not resume and the session will then be reset", - mock.initConverse( - ['chatBoxesInitialized'], - { 'auto_login': false, - 'enable_smacks': true, - 'show_controlbox_by_default': true, - 'smacks_max_unacked_stanzas': 2 - }, - async function (done, _converse) { + it("might not resume and the session will then be reset", + mock.initConverse( + ['chatBoxesInitialized'], + { 'auto_login': false, + 'enable_smacks': true, + 'show_controlbox_by_default': true, + 'smacks_max_unacked_stanzas': 2 + }, + async function (done, _converse) { - await _converse.api.user.login('romeo@montague.lit/orchard', 'secret'); - const sent_stanzas = _converse.connection.sent_stanzas; - let stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'enable')).pop()); - expect(Strophe.serialize(stanza)).toEqual(''); - let result = u.toStanza(``); - _converse.connection._dataRecv(test_utils.createRequest(result)); + await _converse.api.user.login('romeo@montague.lit/orchard', 'secret'); + const sent_stanzas = _converse.connection.sent_stanzas; + let stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'enable')).pop()); + expect(Strophe.serialize(stanza)).toEqual(''); + let result = u.toStanza(``); + _converse.connection._dataRecv(mock.createRequest(result)); - await test_utils.waitForRoster(_converse, 'current', 1); + await mock.waitForRoster(_converse, 'current', 1); - // test session resumption - await _converse.api.connection.reconnect(); - stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop()); - expect(Strophe.serialize(stanza)).toEqual(''); + // test session resumption + await _converse.api.connection.reconnect(); + stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop()); + expect(Strophe.serialize(stanza)).toEqual(''); - result = u.toStanza( - ``+ - ``+ - ``); - _converse.connection._dataRecv(test_utils.createRequest(result)); + result = u.toStanza( + ``+ + ``+ + ``); + _converse.connection._dataRecv(mock.createRequest(result)); - // Session data gets reset - expect(_converse.session.get('smacks_enabled')).toBe(false); - expect(_converse.session.get('num_stanzas_handled')).toBe(0); - expect(_converse.session.get('num_stanzas_handled_by_server')).toBe(0); - expect(_converse.session.get('num_stanzas_since_last_ack')).toBe(0); - expect(_converse.session.get('unacked_stanzas').length).toBe(0); - expect(_converse.session.get('roster_cached')).toBeFalsy(); + // Session data gets reset + expect(_converse.session.get('smacks_enabled')).toBe(false); + expect(_converse.session.get('num_stanzas_handled')).toBe(0); + expect(_converse.session.get('num_stanzas_handled_by_server')).toBe(0); + expect(_converse.session.get('num_stanzas_since_last_ack')).toBe(0); + expect(_converse.session.get('unacked_stanzas').length).toBe(0); + expect(_converse.session.get('roster_cached')).toBeFalsy(); - await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'enable')).length === 2); - stanza = sent_stanzas.filter(s => (s.tagName === 'enable')).pop(); - expect(Strophe.serialize(stanza)).toEqual(''); + await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'enable')).length === 2); + stanza = sent_stanzas.filter(s => (s.tagName === 'enable')).pop(); + expect(Strophe.serialize(stanza)).toEqual(''); - result = u.toStanza(``); - _converse.connection._dataRecv(test_utils.createRequest(result)); - expect(_converse.session.get('smacks_enabled')).toBe(true); + result = u.toStanza(``); + _converse.connection._dataRecv(mock.createRequest(result)); + expect(_converse.session.get('smacks_enabled')).toBe(true); - // Check that the roster gets fetched - await test_utils.waitForRoster(_converse, 'current', 1); - await new Promise(resolve => _converse.api.listen.once('reconnected', resolve)); - done(); - })); + // Check that the roster gets fetched + await mock.waitForRoster(_converse, 'current', 1); + await new Promise(resolve => _converse.api.listen.once('reconnected', resolve)); + done(); + })); - it("can cause MUC messages to be received before chatboxes are initialized", - mock.initConverse( - ['chatBoxesInitialized'], - { 'auto_login': false, - 'blacklisted_plugins': 'converse-mam', - 'enable_smacks': true, - 'muc_fetch_members': false, - 'show_controlbox_by_default': true, - 'smacks_max_unacked_stanzas': 2 - }, - async function (done, _converse) { + it("can cause MUC messages to be received before chatboxes are initialized", + mock.initConverse( + ['chatBoxesInitialized'], + { 'auto_login': false, + 'blacklisted_plugins': 'converse-mam', + 'enable_smacks': true, + 'muc_fetch_members': false, + 'show_controlbox_by_default': true, + 'smacks_max_unacked_stanzas': 2 + }, + async function (done, _converse) { - const key = "converse-test-session/converse.session-romeo@montague.lit-converse.session-romeo@montague.lit"; - sessionStorage.setItem( - key, - JSON.stringify({ - "id": "converse.session-romeo@montague.lit", - "jid": "romeo@montague.lit/converse.js-100020907", - "bare_jid": "romeo@montague.lit", - "resource": "converse.js-100020907", - "domain": "montague.lit", - "active": false, - "smacks_enabled": true, - "num_stanzas_handled": 580, - "num_stanzas_handled_by_server": 525, - "num_stanzas_since_last_ack": 0, - "unacked_stanzas": [], - "smacks_stream_id": "some-long-sm-id", - "push_enabled": ["romeo@montague.lit"], - "carbons_enabled": true, - "roster_cached": true - }) - ); + const key = "converse-test-session/converse.session-romeo@montague.lit-converse.session-romeo@montague.lit"; + sessionStorage.setItem( + key, + JSON.stringify({ + "id": "converse.session-romeo@montague.lit", + "jid": "romeo@montague.lit/converse.js-100020907", + "bare_jid": "romeo@montague.lit", + "resource": "converse.js-100020907", + "domain": "montague.lit", + "active": false, + "smacks_enabled": true, + "num_stanzas_handled": 580, + "num_stanzas_handled_by_server": 525, + "num_stanzas_since_last_ack": 0, + "unacked_stanzas": [], + "smacks_stream_id": "some-long-sm-id", + "push_enabled": ["romeo@montague.lit"], + "carbons_enabled": true, + "roster_cached": true + }) + ); - const muc_jid = 'lounge@montague.lit'; - const chatkey = `converse.chatboxes-romeo@montague.lit-${muc_jid}`; - sessionStorage.setItem('converse.chatboxes-romeo@montague.lit', JSON.stringify([chatkey])); - sessionStorage.setItem(chatkey, - JSON.stringify({ - hidden: false, - message_type: "groupchat", - name: "lounge", - num_unread: 0, - type: "chatroom", - jid: muc_jid, - id: muc_jid, - box_id: "box-YXJnQGNvbmZlcmVuY2UuY2hhdC5leGFtcGxlLm9yZw==", - nick: "romeo" - }) - ); + const muc_jid = 'lounge@montague.lit'; + const chatkey = `converse.chatboxes-romeo@montague.lit-${muc_jid}`; + sessionStorage.setItem('converse.chatboxes-romeo@montague.lit', JSON.stringify([chatkey])); + sessionStorage.setItem(chatkey, + JSON.stringify({ + hidden: false, + message_type: "groupchat", + name: "lounge", + num_unread: 0, + type: "chatroom", + jid: muc_jid, + id: muc_jid, + box_id: "box-YXJnQGNvbmZlcmVuY2UuY2hhdC5leGFtcGxlLm9yZw==", + nick: "romeo" + }) + ); - _converse.no_connection_on_bind = true; // XXX Don't trigger CONNECTED in tests/mock.js - await _converse.api.user.login('romeo@montague.lit', 'secret'); - delete _converse.no_connection_on_bind; + _converse.no_connection_on_bind = true; // XXX Don't trigger CONNECTED in tests/mock.js + await _converse.api.user.login('romeo@montague.lit', 'secret'); + delete _converse.no_connection_on_bind; - const sent_stanzas = _converse.connection.sent_stanzas; - const stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop()); - expect(Strophe.serialize(stanza)).toEqual(''); + const sent_stanzas = _converse.connection.sent_stanzas; + const stanza = await u.waitUntil(() => sent_stanzas.filter(s => (s.tagName === 'resume')).pop()); + expect(Strophe.serialize(stanza)).toEqual(''); - const result = u.toStanza(``); - _converse.connection._dataRecv(test_utils.createRequest(result)); - expect(_converse.session.get('smacks_enabled')).toBe(true); + const result = u.toStanza(``); + _converse.connection._dataRecv(mock.createRequest(result)); + expect(_converse.session.get('smacks_enabled')).toBe(true); - const nick = 'romeo'; - const func = _converse.chatboxes.onChatBoxesFetched; - spyOn(_converse.chatboxes, 'onChatBoxesFetched').and.callFake(collection => { - const muc = new _converse.ChatRoom({'jid': muc_jid, 'id': muc_jid, nick}, {'collection': _converse.chatboxes}); - _converse.chatboxes.add(muc); - func.call(_converse.chatboxes, collection); - }); + const nick = 'romeo'; + const func = _converse.chatboxes.onChatBoxesFetched; + spyOn(_converse.chatboxes, 'onChatBoxesFetched').and.callFake(collection => { + const muc = new _converse.ChatRoom({'jid': muc_jid, 'id': muc_jid, nick}, {'collection': _converse.chatboxes}); + _converse.chatboxes.add(muc); + func.call(_converse.chatboxes, collection); + }); - // A MUC message gets received - const msg = $msg({ - from: `${muc_jid}/juliet`, - id: u.getUniqueId(), - to: 'romeo@montague.lit', - type: 'groupchat' - }).c('body').t('First message').tree(); + // A MUC message gets received + const msg = $msg({ + from: `${muc_jid}/juliet`, + id: u.getUniqueId(), + to: 'romeo@montague.lit', + type: 'groupchat' + }).c('body').t('First message').tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); + _converse.connection._dataRecv(mock.createRequest(msg)); - await _converse.api.waitUntil('chatBoxesFetched'); - const muc = _converse.chatboxes.get(muc_jid); - await u.waitUntil(() => muc.message_queue.length === 1); + await _converse.api.waitUntil('chatBoxesFetched'); + const muc = _converse.chatboxes.get(muc_jid); + await u.waitUntil(() => muc.message_queue.length === 1); - const view = _converse.chatboxviews.get(muc_jid); - await test_utils.getRoomFeatures(_converse, muc_jid); - await test_utils.receiveOwnMUCPresence(_converse, muc_jid, nick); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); - await view.model.messages.fetched; + const view = _converse.chatboxviews.get(muc_jid); + await mock.getRoomFeatures(_converse, muc_jid); + await mock.receiveOwnMUCPresence(_converse, muc_jid, nick); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); + await view.model.messages.fetched; - await u.waitUntil(() => muc.messages.length); - expect(muc.messages.at(0).get('message')).toBe('First message') - done(); - })); - }); + await u.waitUntil(() => muc.messages.length); + expect(muc.messages.at(0).get('message')).toBe('First message') + done(); + })); }); diff --git a/spec/spoilers.js b/spec/spoilers.js index 36ee6d78d..eedbdaae7 100644 --- a/spec/spoilers.js +++ b/spec/spoilers.js @@ -1,237 +1,239 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const _ = converse.env._; - const Strophe = converse.env.Strophe; - const $msg = converse.env.$msg; - const $pres = converse.env.$pres; - const u = converse.env.utils; +/* global mock */ - describe("A spoiler message", function () { +describe("A spoiler message", function () { - it("can be received with a hint", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async (done, _converse) => { + it("can be received with a hint", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async (done, _converse) => { - await test_utils.waitForRoster(_converse, 'current'); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.waitForRoster(_converse, 'current'); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - /* - * And at the end of the story, both of them die! It is so tragic! - * Love story end - * - */ - const spoiler_hint = "Love story end" - const spoiler = "And at the end of the story, both of them die! It is so tragic!"; - const msg = $msg({ - 'xmlns': 'jabber:client', - 'to': _converse.bare_jid, - 'from': sender_jid, - 'type': 'chat' - }).c('body').t(spoiler).up() - .c('spoiler', { - 'xmlns': 'urn:xmpp:spoiler:0', - }).t(spoiler_hint) - .tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve)); - const view = _converse.chatboxviews.get(sender_jid); - await new Promise(resolve => view.once('messageInserted', resolve)); - await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio') - expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Mercutio'); - const message_content = view.el.querySelector('.chat-msg__text'); - expect(message_content.textContent).toBe(spoiler); - const spoiler_hint_el = view.el.querySelector('.spoiler-hint'); - expect(spoiler_hint_el.textContent).toBe(spoiler_hint); - done(); - })); + /* + * And at the end of the story, both of them die! It is so tragic! + * Love story end + * + */ + const spoiler_hint = "Love story end" + const spoiler = "And at the end of the story, both of them die! It is so tragic!"; + const $msg = converse.env.$msg; + const u = converse.env.utils; + const msg = $msg({ + 'xmlns': 'jabber:client', + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat' + }).c('body').t(spoiler).up() + .c('spoiler', { + 'xmlns': 'urn:xmpp:spoiler:0', + }).t(spoiler_hint) + .tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); + await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve)); + const view = _converse.chatboxviews.get(sender_jid); + await new Promise(resolve => view.once('messageInserted', resolve)); + await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio') + expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Mercutio'); + const message_content = view.el.querySelector('.chat-msg__text'); + expect(message_content.textContent).toBe(spoiler); + const spoiler_hint_el = view.el.querySelector('.spoiler-hint'); + expect(spoiler_hint_el.textContent).toBe(spoiler_hint); + done(); + })); - it("can be received without a hint", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async (done, _converse) => { + it("can be received without a hint", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async (done, _converse) => { - await test_utils.waitForRoster(_converse, 'current'); - const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - /* - * And at the end of the story, both of them die! It is so tragic! - * Love story end - * - */ - const spoiler = "And at the end of the story, both of them die! It is so tragic!"; - const msg = $msg({ - 'xmlns': 'jabber:client', - 'to': _converse.bare_jid, - 'from': sender_jid, - 'type': 'chat' - }).c('body').t(spoiler).up() - .c('spoiler', { - 'xmlns': 'urn:xmpp:spoiler:0', - }).tree(); - _converse.connection._dataRecv(test_utils.createRequest(msg)); - await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve)); - const view = _converse.chatboxviews.get(sender_jid); - await new Promise(resolve => view.model.messages.once('rendered', resolve)); - await u.waitUntil(() => u.isVisible(view.el)); - await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio') - expect(view.el.querySelector('.chat-msg__author').textContent.includes('Mercutio')).toBeTruthy(); - const message_content = view.el.querySelector('.chat-msg__text'); - expect(message_content.textContent).toBe(spoiler); - const spoiler_hint_el = view.el.querySelector('.spoiler-hint'); - expect(spoiler_hint_el.textContent).toBe(''); - done(); - })); + await mock.waitForRoster(_converse, 'current'); + const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + /* + * And at the end of the story, both of them die! It is so tragic! + * Love story end + * + */ + const $msg = converse.env.$msg; + const u = converse.env.utils; + const spoiler = "And at the end of the story, both of them die! It is so tragic!"; + const msg = $msg({ + 'xmlns': 'jabber:client', + 'to': _converse.bare_jid, + 'from': sender_jid, + 'type': 'chat' + }).c('body').t(spoiler).up() + .c('spoiler', { + 'xmlns': 'urn:xmpp:spoiler:0', + }).tree(); + _converse.connection._dataRecv(mock.createRequest(msg)); + await new Promise(resolve => _converse.api.listen.once('chatBoxViewInitialized', resolve)); + const view = _converse.chatboxviews.get(sender_jid); + await new Promise(resolve => view.model.messages.once('rendered', resolve)); + await u.waitUntil(() => u.isVisible(view.el)); + await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio') + expect(view.el.querySelector('.chat-msg__author').textContent.includes('Mercutio')).toBeTruthy(); + const message_content = view.el.querySelector('.chat-msg__text'); + expect(message_content.textContent).toBe(spoiler); + const spoiler_hint_el = view.el.querySelector('.spoiler-hint'); + expect(spoiler_hint_el.textContent).toBe(''); + done(); + })); - it("can be sent without a hint", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async (done, _converse) => { + it("can be sent without a hint", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async (done, _converse) => { - await test_utils.waitForRoster(_converse, 'current', 1); - test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.waitForRoster(_converse, 'current', 1); + mock.openControlBox(_converse); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - // XXX: We need to send a presence from the contact, so that we - // have a resource, that resource is then queried to see - // whether Strophe.NS.SPOILER is supported, in which case - // the spoiler button will appear. - const presence = $pres({ - 'from': contact_jid+'/phone', - 'to': 'romeo@montague.lit' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await test_utils.openChatBoxFor(_converse, contact_jid); - await test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]); - const view = _converse.api.chatviews.get(contact_jid); - spyOn(_converse.connection, 'send'); + const { $pres, Strophe} = converse.env; + const u = converse.env.utils; - await u.waitUntil(() => view.el.querySelector('.toggle-compose-spoiler')); - let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler'); - spoiler_toggle.click(); + // XXX: We need to send a presence from the contact, so that we + // have a resource, that resource is then queried to see + // whether Strophe.NS.SPOILER is supported, in which case + // the spoiler button will appear. + const presence = $pres({ + 'from': contact_jid+'/phone', + 'to': 'romeo@montague.lit' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await mock.openChatBoxFor(_converse, contact_jid); + await mock.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]); + const view = _converse.api.chatviews.get(contact_jid); + spyOn(_converse.connection, 'send'); - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = 'This is the spoiler'; - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - await new Promise(resolve => view.once('messageInserted', resolve)); + await u.waitUntil(() => view.el.querySelector('.toggle-compose-spoiler')); + let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler'); + spoiler_toggle.click(); - /* Test the XML stanza - * - * - * This is the spoiler - * - * - * " - */ - const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); - const spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]'); - expect(spoiler_el === null).toBeFalsy(); - expect(spoiler_el.textContent).toBe(''); + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = 'This is the spoiler'; + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + await new Promise(resolve => view.once('messageInserted', resolve)); - const body_el = stanza.querySelector('body'); - expect(body_el.textContent).toBe('This is the spoiler'); + /* Test the XML stanza + * + * + * This is the spoiler + * + * + * " + */ + const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); + const spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]'); + expect(spoiler_el === null).toBeFalsy(); + expect(spoiler_el.textContent).toBe(''); - /* Test the HTML spoiler message */ - expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague'); + const body_el = stanza.querySelector('body'); + expect(body_el.textContent).toBe('This is the spoiler'); - const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler'); - expect(spoiler_msg_el.textContent).toBe('This is the spoiler'); - expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy(); + /* Test the HTML spoiler message */ + expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague'); - spoiler_toggle = view.el.querySelector('.spoiler-toggle'); - expect(spoiler_toggle.textContent).toBe('Show more'); - spoiler_toggle.click(); - expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy(); - expect(spoiler_toggle.textContent).toBe('Show less'); - spoiler_toggle.click(); - expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy(); - done(); - })); + const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler'); + expect(spoiler_msg_el.textContent).toBe('This is the spoiler'); + expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeTruthy(); - it("can be sent with a hint", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async (done, _converse) => { + spoiler_toggle = view.el.querySelector('.spoiler-toggle'); + expect(spoiler_toggle.textContent).toBe('Show more'); + spoiler_toggle.click(); + expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeFalsy(); + expect(spoiler_toggle.textContent).toBe('Show less'); + spoiler_toggle.click(); + expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeTruthy(); + done(); + })); - await test_utils.waitForRoster(_converse, 'current', 1); - test_utils.openControlBox(_converse); - const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + it("can be sent with a hint", + mock.initConverse( + ['rosterGroupsFetched', 'chatBoxesFetched'], {}, + async (done, _converse) => { - // XXX: We need to send a presence from the contact, so that we - // have a resource, that resource is then queried to see - // whether Strophe.NS.SPOILER is supported, in which case - // the spoiler button will appear. - const presence = $pres({ - 'from': contact_jid+'/phone', - 'to': 'romeo@montague.lit' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - await test_utils.openChatBoxFor(_converse, contact_jid); - await test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]); - const view = _converse.api.chatviews.get(contact_jid); + await mock.waitForRoster(_converse, 'current', 1); + mock.openControlBox(_converse); + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; - await u.waitUntil(() => view.el.querySelector('.toggle-compose-spoiler')); - let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler'); - spoiler_toggle.click(); + const { $pres, Strophe} = converse.env; + const u = converse.env.utils; - spyOn(_converse.connection, 'send'); + // XXX: We need to send a presence from the contact, so that we + // have a resource, that resource is then queried to see + // whether Strophe.NS.SPOILER is supported, in which case + // the spoiler button will appear. + const presence = $pres({ + 'from': contact_jid+'/phone', + 'to': 'romeo@montague.lit' + }); + _converse.connection._dataRecv(mock.createRequest(presence)); + await mock.openChatBoxFor(_converse, contact_jid); + await mock.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]); + const view = _converse.api.chatviews.get(contact_jid); - const textarea = view.el.querySelector('.chat-textarea'); - textarea.value = 'This is the spoiler'; - const hint_input = view.el.querySelector('.spoiler-hint'); - hint_input.value = 'This is the hint'; + await u.waitUntil(() => view.el.querySelector('.toggle-compose-spoiler')); + let spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler'); + spoiler_toggle.click(); - view.onKeyDown({ - target: textarea, - preventDefault: function preventDefault () {}, - keyCode: 13 - }); - await new Promise(resolve => view.once('messageInserted', resolve)); + spyOn(_converse.connection, 'send'); - /* Test the XML stanza - * - * - * This is the spoiler - * - * This is the hint - * " - */ - const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); - const spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]'); + const textarea = view.el.querySelector('.chat-textarea'); + textarea.value = 'This is the spoiler'; + const hint_input = view.el.querySelector('.spoiler-hint'); + hint_input.value = 'This is the hint'; - expect(spoiler_el === null).toBeFalsy(); - expect(spoiler_el.textContent).toBe('This is the hint'); + view.onKeyDown({ + target: textarea, + preventDefault: function preventDefault () {}, + keyCode: 13 + }); + await new Promise(resolve => view.once('messageInserted', resolve)); - const body_el = stanza.querySelector('body'); - expect(body_el.textContent).toBe('This is the spoiler'); + /* Test the XML stanza + * + * + * This is the spoiler + * + * This is the hint + * " + */ + const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); + const spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]'); - /* Test the HTML spoiler message */ - expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague'); + expect(spoiler_el === null).toBeFalsy(); + expect(spoiler_el.textContent).toBe('This is the hint'); - const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler'); - expect(spoiler_msg_el.textContent).toBe('This is the spoiler'); - expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy(); + const body_el = stanza.querySelector('body'); + expect(body_el.textContent).toBe('This is the spoiler'); - spoiler_toggle = view.el.querySelector('.spoiler-toggle'); - expect(spoiler_toggle.textContent).toBe('Show more'); - spoiler_toggle.click(); - expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy(); - expect(spoiler_toggle.textContent).toBe('Show less'); - spoiler_toggle.click(); - expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy(); - done(); - })); - }); + /* Test the HTML spoiler message */ + expect(view.el.querySelector('.chat-msg__author').textContent.trim()).toBe('Romeo Montague'); + + const spoiler_msg_el = view.el.querySelector('.chat-msg__text.spoiler'); + expect(spoiler_msg_el.textContent).toBe('This is the spoiler'); + expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeTruthy(); + + spoiler_toggle = view.el.querySelector('.spoiler-toggle'); + expect(spoiler_toggle.textContent).toBe('Show more'); + spoiler_toggle.click(); + expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeFalsy(); + expect(spoiler_toggle.textContent).toBe('Show less'); + spoiler_toggle.click(); + expect(Array.from(spoiler_msg_el.classList).includes('collapsed')).toBeTruthy(); + done(); + })); }); diff --git a/spec/transcripts.js b/spec/transcripts.js deleted file mode 100644 index ee114d084..000000000 --- a/spec/transcripts.js +++ /dev/null @@ -1,77 +0,0 @@ -(function (root, factory) { - define([ - "jasmine", - "mock", - "test-utils", - "utils", - "transcripts" - ], factory - ); -} (this, function (jasmine, mock, test_utils, utils, transcripts) { - var Strophe = converse.env.Strophe; - var _ = converse.env._; - var IGNORED_TAGS = [ - 'stream:features', - 'auth', - 'challenge', - 'success', - 'stream:features', - 'response' - ]; - - function traverseElement (el, _stanza) { - if (typeof _stanza !== 'undefined') { - if (el.nodeType === 3) { - _stanza.t(el.nodeValue); - return _stanza; - } else { - _stanza = _stanza.c(el.nodeName.toLowerCase(), getAttributes(el)); - } - } else { - _stanza = new Strophe.Builder( - el.nodeName.toLowerCase(), - getAttributes(el) - ); - } - _.each(el.childNodes, _.partial(traverseElement, _, _stanza)); - return _stanza.up(); - } - - function getAttributes (el) { - var attributes = {}; - _.each(el.attributes, function (att) { - attributes[att.nodeName] = att.nodeValue; - }); - return attributes; - } - - return describe("Transcripts of chat logs", function () { - - it("can be used to replay conversations", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { - - _converse.allow_non_roster_messaging = true; - await test_utils.openAndEnterChatRoom(_converse, 'discuss@conference.conversejs.org', 'romeo'); - spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true); - _.each(transcripts, function (transcript) { - const text = transcript(); - const xml = Strophe.xmlHtmlNode(text); - _.each(xml.firstElementChild.children, function (el) { - _.each(el.children, function (el) { - if (el.nodeType === 3) { - return; // Ignore text - } - if (_.includes(IGNORED_TAGS, el.nodeName.toLowerCase())) { - return; - } - const _stanza = traverseElement(el); - _converse.connection._dataRecv(test_utils.createRequest(_stanza)); - }); - }); - }); - done(); - })); - }); -})); diff --git a/spec/user-details-modal.js b/spec/user-details-modal.js index 552e88116..3afa98cb9 100644 --- a/spec/user-details-modal.js +++ b/spec/user-details-modal.js @@ -1,77 +1,75 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const u = converse.env.utils; +/*global mock */ - return describe("The User Details Modal", function () { +const u = converse.env.utils; - it("can be used to remove a contact", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { +describe("The User Details Modal", function () { - await test_utils.waitForRoster(_converse, 'current', 1); - _converse.api.trigger('rosterContactsFetched'); + it("can be used to remove a contact", + 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); - await u.waitUntil(() => _converse.chatboxes.length > 1); - const view = _converse.chatboxviews.get(contact_jid); - let show_modal_button = view.el.querySelector('.show-user-details-modal'); - show_modal_button.click(); - const modal = view.user_details_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1000); - spyOn(window, 'confirm').and.returnValue(true); - spyOn(view.model.contact, 'removeFromRoster').and.callFake(callback => callback()); - let remove_contact_button = modal.el.querySelector('button.remove-contact'); - expect(u.isVisible(remove_contact_button)).toBeTruthy(); - remove_contact_button.click(); - await u.waitUntil(() => modal.el.getAttribute('aria-hidden'), 1000); - await u.waitUntil(() => !u.isVisible(modal.el)); - show_modal_button = view.el.querySelector('.show-user-details-modal'); - show_modal_button.click(); - remove_contact_button = modal.el.querySelector('button.remove-contact'); - expect(remove_contact_button === null).toBeTruthy(); - done(); - })); + await mock.waitForRoster(_converse, 'current', 1); + _converse.api.trigger('rosterContactsFetched'); - it("shows an alert when an error happened while removing the contact", - mock.initConverse(['rosterGroupsFetched'], {}, async function (done, _converse) { + const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit'; + await mock.openChatBoxFor(_converse, contact_jid); + await u.waitUntil(() => _converse.chatboxes.length > 1); + const view = _converse.chatboxviews.get(contact_jid); + let show_modal_button = view.el.querySelector('.show-user-details-modal'); + show_modal_button.click(); + const modal = view.user_details_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1000); + spyOn(window, 'confirm').and.returnValue(true); + spyOn(view.model.contact, 'removeFromRoster').and.callFake(callback => callback()); + let remove_contact_button = modal.el.querySelector('button.remove-contact'); + expect(u.isVisible(remove_contact_button)).toBeTruthy(); + remove_contact_button.click(); + await u.waitUntil(() => modal.el.getAttribute('aria-hidden'), 1000); + await u.waitUntil(() => !u.isVisible(modal.el)); + show_modal_button = view.el.querySelector('.show-user-details-modal'); + show_modal_button.click(); + remove_contact_button = modal.el.querySelector('button.remove-contact'); + expect(remove_contact_button === null).toBeTruthy(); + done(); + })); - await test_utils.waitForRoster(_converse, 'current', 1); - _converse.api.trigger('rosterContactsFetched'); + it("shows an alert when an error happened while removing the contact", + 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 view = _converse.chatboxviews.get(contact_jid); - let show_modal_button = view.el.querySelector('.show-user-details-modal'); - show_modal_button.click(); - const modal = view.user_details_modal; - await u.waitUntil(() => u.isVisible(modal.el), 2000); - spyOn(window, 'confirm').and.returnValue(true); + await mock.waitForRoster(_converse, 'current', 1); + _converse.api.trigger('rosterContactsFetched'); - spyOn(view.model.contact, 'removeFromRoster').and.callFake((callback, errback) => errback()); - let remove_contact_button = modal.el.querySelector('button.remove-contact'); - expect(u.isVisible(remove_contact_button)).toBeTruthy(); - remove_contact_button.click(); - await u.waitUntil(() => u.isVisible(document.querySelector('.alert-danger')), 2000); + 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 show_modal_button = view.el.querySelector('.show-user-details-modal'); + show_modal_button.click(); + const modal = view.user_details_modal; + await u.waitUntil(() => u.isVisible(modal.el), 2000); + spyOn(window, 'confirm').and.returnValue(true); - const header = document.querySelector('.alert-danger .modal-title'); - expect(header.textContent).toBe("Error"); - expect(u.ancestor(header, '.modal-content').querySelector('.modal-body p').textContent.trim()) - .toBe("Sorry, there was an error while trying to remove Mercutio as a contact."); - document.querySelector('.alert-danger button.close').click(); - show_modal_button = view.el.querySelector('.show-user-details-modal'); - show_modal_button.click(); - await u.waitUntil(() => u.isVisible(modal.el), 2000) + spyOn(view.model.contact, 'removeFromRoster').and.callFake((callback, errback) => errback()); + let remove_contact_button = modal.el.querySelector('button.remove-contact'); + expect(u.isVisible(remove_contact_button)).toBeTruthy(); + remove_contact_button.click(); + await u.waitUntil(() => u.isVisible(document.querySelector('.alert-danger')), 2000); - show_modal_button = view.el.querySelector('.show-user-details-modal'); - show_modal_button.click(); - await u.waitUntil(() => u.isVisible(modal.el), 2000) + const header = document.querySelector('.alert-danger .modal-title'); + expect(header.textContent).toBe("Error"); + expect(u.ancestor(header, '.modal-content').querySelector('.modal-body p').textContent.trim()) + .toBe("Sorry, there was an error while trying to remove Mercutio as a contact."); + document.querySelector('.alert-danger button.close').click(); + show_modal_button = view.el.querySelector('.show-user-details-modal'); + show_modal_button.click(); + await u.waitUntil(() => u.isVisible(modal.el), 2000) - remove_contact_button = modal.el.querySelector('button.remove-contact'); - expect(u.isVisible(remove_contact_button)).toBeTruthy(); - done(); - })); - }); + show_modal_button = view.el.querySelector('.show-user-details-modal'); + show_modal_button.click(); + await u.waitUntil(() => u.isVisible(modal.el), 2000) + + remove_contact_button = modal.el.querySelector('button.remove-contact'); + expect(u.isVisible(remove_contact_button)).toBeTruthy(); + done(); + })); }); diff --git a/spec/utils.js b/spec/utils.js index 3045a72f0..d6ddaf331 100644 --- a/spec/utils.js +++ b/spec/utils.js @@ -1,62 +1,58 @@ -window.addEventListener('converse-loaded', () => { - const utils = converse.env.utils; - const _ = converse.env._; +describe("Converse.js Utilities", function() { - return describe("Converse.js Utilities", function() { + it("applySiteSettings: recursively applies user settings", function () { + const context = {}; + const settings = { + show_toolbar: true, + chatview_avatar_width: 32, + chatview_avatar_height: 32, + auto_join_rooms: [], + visible_toolbar_buttons: { + 'emojis': true, + 'call': false, + 'clear': true, + 'toggle_occupants': true + } + }; + Object.assign(context, settings); - it("applySiteSettings: recursively applies user settings", function () { - var context = {}; - var settings = { - show_toolbar: true, - chatview_avatar_width: 32, - chatview_avatar_height: 32, - auto_join_rooms: [], - visible_toolbar_buttons: { - 'emojis': true, - 'call': false, - 'clear': true, - 'toggle_occupants': true - } - }; - _.extend(context, settings); + let user_settings = { + something_else: 'xxx', + show_toolbar: false, + chatview_avatar_width: 32, + chatview_avatar_height: 48, + auto_join_rooms: [ + 'anonymous@conference.nomnom.im', + ], + visible_toolbar_buttons: { + 'emojis': false, + 'call': false, + 'toggle_occupants':false, + 'invalid': false + } + }; + const utils = converse.env.utils; + utils.applySiteSettings(context, settings, user_settings); - var user_settings = { - something_else: 'xxx', - show_toolbar: false, - chatview_avatar_width: 32, - chatview_avatar_height: 48, - auto_join_rooms: [ - 'anonymous@conference.nomnom.im', - ], - visible_toolbar_buttons: { - 'emojis': false, - 'call': false, - 'toggle_occupants':false, - 'invalid': false - } - }; - utils.applySiteSettings(context, settings, user_settings); + expect(context.something_else).toBeUndefined(); + expect(context.show_toolbar).toBeFalsy(); + expect(context.chatview_avatar_width).toBe(32); + expect(context.chatview_avatar_height).toBe(48); + expect(Object.keys(context.visible_toolbar_buttons)).toEqual(Object.keys(settings.visible_toolbar_buttons)); + expect(context.visible_toolbar_buttons.emojis).toBeFalsy(); + expect(context.visible_toolbar_buttons.call).toBeFalsy(); + expect(context.visible_toolbar_buttons.toggle_occupants).toBeFalsy(); + expect(context.visible_toolbar_buttons.invalid).toBeFalsy(); + expect(context.auto_join_rooms.length).toBe(1); + expect(context.auto_join_rooms[0]).toBe('anonymous@conference.nomnom.im'); - expect(context.something_else).toBeUndefined(); - expect(context.show_toolbar).toBeFalsy(); - expect(context.chatview_avatar_width).toBe(32); - expect(context.chatview_avatar_height).toBe(48); - expect(_.keys(context.visible_toolbar_buttons)).toEqual(_.keys(settings.visible_toolbar_buttons)); - expect(context.visible_toolbar_buttons.emojis).toBeFalsy(); - expect(context.visible_toolbar_buttons.call).toBeFalsy(); - expect(context.visible_toolbar_buttons.toggle_occupants).toBeFalsy(); - expect(context.visible_toolbar_buttons.invalid).toBeFalsy(); - expect(context.auto_join_rooms.length).toBe(1); - expect(context.auto_join_rooms[0]).toBe('anonymous@conference.nomnom.im'); - - user_settings = { - visible_toolbar_buttons: { - 'toggle_occupants': true - } - }; - utils.applySiteSettings(context, settings, user_settings); - expect(_.keys(context.visible_toolbar_buttons)).toEqual(_.keys(settings.visible_toolbar_buttons)); - expect(context.visible_toolbar_buttons.toggle_occupants).toBeTruthy(); - }); + user_settings = { + visible_toolbar_buttons: { + 'toggle_occupants': true + } + }; + utils.applySiteSettings(context, settings, user_settings); + expect(Object.keys(context.visible_toolbar_buttons)).toEqual(Object.keys(settings.visible_toolbar_buttons)); + expect(context.visible_toolbar_buttons.toggle_occupants).toBeTruthy(); }); }); diff --git a/spec/xmppstatus.js b/spec/xmppstatus.js index f2ee79438..116329aa9 100644 --- a/spec/xmppstatus.js +++ b/spec/xmppstatus.js @@ -1,23 +1,22 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const u = converse.env.utils; +/*global mock */ - return describe("The XMPPStatus model", function () { +const u = converse.env.utils; - it("won't send online when setting a custom status message", - mock.initConverse(async (done, _converse) => { - _converse.xmppstatus.save({'status': 'online'}); - spyOn(_converse.connection, 'send'); - _converse.api.user.status.message.set("I'm also happy!"); - await u.waitUntil(() => _converse.connection.send.calls.count()); - const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); - expect(stanza.childNodes.length).toBe(3); - expect(stanza.querySelectorAll('status').length).toBe(1); - expect(stanza.querySelector('status').textContent).toBe("I'm also happy!"); - expect(stanza.querySelectorAll('show').length).toBe(0); - expect(stanza.querySelectorAll('priority').length).toBe(1); - expect(stanza.querySelector('priority').textContent).toBe('0'); - done(); - })); - }); +describe("The XMPPStatus model", function () { + + it("won't send online when setting a custom status message", + mock.initConverse(async (done, _converse) => { + _converse.xmppstatus.save({'status': 'online'}); + spyOn(_converse.connection, 'send'); + _converse.api.user.status.message.set("I'm also happy!"); + await u.waitUntil(() => _converse.connection.send.calls.count()); + const stanza = _converse.connection.send.calls.argsFor(0)[0].tree(); + expect(stanza.childNodes.length).toBe(3); + expect(stanza.querySelectorAll('status').length).toBe(1); + expect(stanza.querySelector('status').textContent).toBe("I'm also happy!"); + expect(stanza.querySelectorAll('show').length).toBe(0); + expect(stanza.querySelectorAll('priority').length).toBe(1); + expect(stanza.querySelector('priority').textContent).toBe('0'); + done(); + })); }); diff --git a/spec/xss.js b/spec/xss.js index 38bbab3d5..5302c5f39 100644 --- a/spec/xss.js +++ b/spec/xss.js @@ -1,244 +1,242 @@ -window.addEventListener('converse-loaded', () => { - const mock = window.mock; - const test_utils = window.test_utils; - const $pres = converse.env.$pres; - const sizzle = converse.env.sizzle; - const u = converse.env.utils; +/*global mock */ - describe("XSS", function () { - describe("A Chat Message", function () { +const $pres = converse.env.$pres; +const sizzle = converse.env.sizzle; +const u = converse.env.utils; - it("will escape IMG payload XSS attempts", - mock.initConverse( - ['rosterGroupsFetched', 'chatBoxesFetched'], {}, - async function (done, _converse) { +describe("XSS", function () { + describe("A Chat Message", function () { - spyOn(window, 'alert').and.callThrough(); - await test_utils.waitForRoster(_converse, 'current'); - await test_utils.openControlBox(_converse); + it("will escape IMG payload XSS attempts", + 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.api.chatviews.get(contact_jid); + spyOn(window, 'alert').and.callThrough(); + await mock.waitForRoster(_converse, 'current'); + await mock.openControlBox(_converse); - let message = ""; - await test_utils.sendMessage(view, message); - let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); - expect(msg.textContent).toEqual(message); - expect(msg.innerHTML).toEqual("<img src=x onerror=alert('XSS');>"); - expect(window.alert).not.toHaveBeenCalled(); + 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); - message = "xss>");http://www.opkode.com/\'onmouseover=\'alert(1)\'whatever'); + let message = "http://www.opkode.com/'onmouseover='alert(1)'whatever"; + await mock.sendMessage(view, message); - message = 'http://www.opkode.com/"onmouseover="alert(1)"whatever'; - await test_utils.sendMessage(view, message); + let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); + expect(msg.textContent).toEqual(message); + expect(msg.innerHTML) + .toEqual('http://www.opkode.com/\'onmouseover=\'alert(1)\'whatever'); - msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); - expect(msg.textContent).toEqual(message); - expect(msg.innerHTML).toEqual('http://www.opkode.com/"onmouseover="alert(1)"whatever'); + message = 'http://www.opkode.com/"onmouseover="alert(1)"whatever'; + await mock.sendMessage(view, message); - message = "https://en.wikipedia.org/wiki/Ender's_Game"; - await test_utils.sendMessage(view, message); + msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); + expect(msg.textContent).toEqual(message); + expect(msg.innerHTML).toEqual('http://www.opkode.com/"onmouseover="alert(1)"whatever'); - msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); - expect(msg.textContent).toEqual(message); - expect(msg.innerHTML).toEqual(''+message+''); + message = "https://en.wikipedia.org/wiki/Ender's_Game"; + await mock.sendMessage(view, message); - message = ""; - await test_utils.sendMessage(view, message); + msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); + expect(msg.textContent).toEqual(message); + expect(msg.innerHTML).toEqual(''+message+''); - msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); - expect(msg.textContent).toEqual(message); - expect(msg.innerHTML).toEqual( - `<https://bugs.documentfoundation.org/show_bug.cgi?id=123737>`); + message = ""; + await mock.sendMessage(view, message); - message = ''; - await test_utils.sendMessage(view, message); + msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); + expect(msg.textContent).toEqual(message); + expect(msg.innerHTML).toEqual( + `<https://bugs.documentfoundation.org/show_bug.cgi?id=123737>`); - msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); - expect(msg.textContent).toEqual(message); - expect(msg.innerHTML).toEqual( - '<http://www.opkode.com/"onmouseover="alert(1)"whatever>'); + message = ''; + await mock.sendMessage(view, message); - message = `https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=!3m6!1e1!3m4!1sQ7SdHo_bPLPlLlU8GSGWaQ!2e0!7i13312!8i6656!4m5!3m4!1s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08!8m2!3d52.3773668!4d4.5489388!5m1!1e2` - await test_utils.sendMessage(view, message); + msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); + expect(msg.textContent).toEqual(message); + expect(msg.innerHTML).toEqual( + '<http://www.opkode.com/"onmouseover="alert(1)"whatever>'); - msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); - expect(msg.textContent).toEqual(message); - expect(msg.innerHTML).toEqual( - `https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=!3m6!1e1!3m4!1sQ7SdHo_bPLPlLlU8GSGWaQ!2e0!7i13312!8i6656!4m5!3m4!1s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08!8m2!3d52.3773668!4d4.5489388!5m1!1e2`); - done(); - })); - }); + message = `https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=!3m6!1e1!3m4!1sQ7SdHo_bPLPlLlU8GSGWaQ!2e0!7i13312!8i6656!4m5!3m4!1s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08!8m2!3d52.3773668!4d4.5489388!5m1!1e2` + await mock.sendMessage(view, message); - describe("A Groupchat", function () { + msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view.el).pop(); + expect(msg.textContent).toEqual(message); + expect(msg.innerHTML).toEqual( + `https://www.google.com/maps/place/Kochstraat+6,+2041+CE+Zandvoort/@52.3775999,4.548971,3a,15y,170.85h,88.39t/data=!3m6!1e1!3m4!1sQ7SdHo_bPLPlLlU8GSGWaQ!2e0!7i13312!8i6656!4m5!3m4!1s0x47c5ec1e56f845ad:0x1de0bc4a5771fb08!8m2!3d52.3773668!4d4.5489388!5m1!1e2`); + done(); + })); + }); - it("escapes occupant nicknames when rendering them, to avoid JS-injection attacks", - mock.initConverse(['rosterGroupsFetched'], {}, - async function (done, _converse) { + describe("A Groupchat", function () { - await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); - /* - * - * - * - * - * " - */ - const presence = $pres({ - to:'romeo@montague.lit/pda', - from:"lounge@montague.lit/<img src="x" onerror="alert(123)"/>" - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - jid: 'someone@montague.lit', - role: 'moderator', - }).up() - .c('status').attrs({code:'110'}).nodeTree; + it("escapes occupant nicknames when rendering them, to avoid JS-injection attacks", + mock.initConverse(['rosterGroupsFetched'], {}, + async function (done, _converse) { - _converse.connection._dataRecv(test_utils.createRequest(presence)); - const view = _converse.chatboxviews.get('lounge@montague.lit'); - await u.waitUntil(() => view.el.querySelectorAll('li .occupant-nick').length, 500); - const occupants = view.el.querySelector('.occupant-list').querySelectorAll('li .occupant-nick'); - expect(occupants.length).toBe(2); - expect(occupants[0].textContent.trim()).toBe("<img src="x" onerror="alert(123)"/>"); - done(); - })); + await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo'); + /* + * + * + * + * + * " + */ + const presence = $pres({ + to:'romeo@montague.lit/pda', + from:"lounge@montague.lit/<img src="x" onerror="alert(123)"/>" + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + jid: 'someone@montague.lit', + role: 'moderator', + }).up() + .c('status').attrs({code:'110'}).nodeTree; - it("escapes the subject before rendering it, to avoid JS-injection attacks", - mock.initConverse( - ['rosterGroupsFetched'], {}, - async function (done, _converse) { + _converse.connection._dataRecv(mock.createRequest(presence)); + const view = _converse.chatboxviews.get('lounge@montague.lit'); + await u.waitUntil(() => view.el.querySelectorAll('li .occupant-nick').length, 500); + const occupants = view.el.querySelector('.occupant-list').querySelectorAll('li .occupant-nick'); + expect(occupants.length).toBe(2); + expect(occupants[0].textContent.trim()).toBe("<img src="x" onerror="alert(123)"/>"); + done(); + })); - await test_utils.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc'); - spyOn(window, 'alert'); - const subject = ''; - const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); - view.model.set({'subject': { - 'text': subject, - 'author': 'ralphm' - }}); - const text = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')?.textContent.trim()); - expect(text).toBe(subject); - done(); - })); - }); + it("escapes the subject before rendering it, to avoid JS-injection attacks", + mock.initConverse( + ['rosterGroupsFetched'], {}, + async function (done, _converse) { + + await mock.openAndEnterChatRoom(_converse, 'jdev@conference.jabber.org', 'jc'); + spyOn(window, 'alert'); + const subject = ''; + const view = _converse.chatboxviews.get('jdev@conference.jabber.org'); + view.model.set({'subject': { + 'text': subject, + 'author': 'ralphm' + }}); + const text = await u.waitUntil(() => view.el.querySelector('.chat-head__desc')?.textContent.trim()); + expect(text).toBe(subject); + done(); + })); }); }); diff --git a/tests.html b/tests.html deleted file mode 100644 index 898f3b0eb..000000000 --- a/tests.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - Converse Tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
-

- - - converse.js - - -

-

Tests

-
-
-
-
- - - diff --git a/tests/index.html b/tests/index.html deleted file mode 100644 index 5dfd9b1eb..000000000 --- a/tests/index.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - Converse Tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
-

- - - converse.js - - -

-

Tests

-
-
-
-
- - - diff --git a/tests/mock.js b/tests/mock.js index f6395dbc4..cf30a436a 100644 --- a/tests/mock.js +++ b/tests/mock.js @@ -2,6 +2,8 @@ const mock = {}; window.mock = mock; let _converse, initConverse; +const converseLoaded = new Promise(resolve => window.addEventListener('converse-loaded', resolve)); + mock.initConverse = function (promise_names=[], settings=null, func) { if (typeof promise_names === "function") { func = promise_names; @@ -19,6 +21,7 @@ mock.initConverse = function (promise_names=[], settings=null, func) { } document.title = "Converse Tests"; + await converseLoaded; await initConverse(settings); await Promise.all((promise_names || []).map(_converse.api.waitUntil)); try { @@ -32,12 +35,432 @@ mock.initConverse = function (promise_names=[], settings=null, func) { }; window.addEventListener('converse-loaded', () => { - const _ = converse.env._; - const u = converse.env.utils; - const Promise = converse.env.Promise; - const Strophe = converse.env.Strophe; - const dayjs = converse.env.dayjs; - const $iq = converse.env.$iq; + const { _, u, sizzle, Strophe, dayjs, $iq, $msg, $pres } = converse.env; + + mock.waitUntilDiscoConfirmed = async function (_converse, entity_jid, identities, features=[], items=[], type='info') { + const iq = await u.waitUntil(() => { + return _.filter( + _converse.connection.IQ_stanzas, + (iq) => sizzle(`iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#${type}"]`, iq).length + ).pop(); + }, 300); + const stanza = $iq({ + 'type': 'result', + 'from': entity_jid, + 'to': 'romeo@montague.lit/orchard', + 'id': iq.getAttribute('id'), + }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#'+type}); + + _.forEach(identities, function (identity) { + stanza.c('identity', {'category': identity.category, 'type': identity.type}).up() + }); + _.forEach(features, function (feature) { + stanza.c('feature', {'var': feature}).up(); + }); + _.forEach(items, function (item) { + stanza.c('item', {'jid': item}).up(); + }); + _converse.connection._dataRecv(mock.createRequest(stanza)); + } + + mock.createRequest = function (iq) { + iq = typeof iq.tree == "function" ? iq.tree() : iq; + var req = new Strophe.Request(iq, function() {}); + req.getResponse = function () { + var env = new Strophe.Builder('env', {type: 'mock'}).tree(); + env.appendChild(iq); + return env; + }; + return req; + }; + + mock.closeAllChatBoxes = function (_converse) { + return Promise.all(_converse.chatboxviews.map(view => view.close())); + }; + + mock.openControlBox = async function (_converse) { + const model = await _converse.api.controlbox.open(); + await u.waitUntil(() => model.get('connected')); + var toggle = document.querySelector(".toggle-controlbox"); + if (!u.isVisible(document.querySelector("#controlbox"))) { + if (!u.isVisible(toggle)) { + u.removeClass('hidden', toggle); + } + toggle.click(); + } + return this; + }; + + mock.closeControlBox = function () { + const controlbox = document.querySelector("#controlbox"); + if (u.isVisible(controlbox)) { + const button = controlbox.querySelector(".close-chatbox-button"); + if (!_.isNull(button)) { + button.click(); + } + } + return this; + }; + + mock.waitUntilBookmarksReturned = async function (_converse, bookmarks=[]) { + 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 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'}); + bookmarks.forEach(bookmark => { + stanza.c('conference', { + 'name': bookmark.name, + 'autojoin': bookmark.autojoin, + 'jid': bookmark.jid + }).c('nick').t(bookmark.nick).up().up() + }); + _converse.connection._dataRecv(mock.createRequest(stanza)); + await _converse.api.waitUntil('bookmarksInitialized'); + }; + + mock.openChatBoxes = function (converse, amount) { + const views = []; + for (let i=0; i _converse.chatboxviews.get(jid), 1000); + }; + + mock.openChatRoomViaModal = async function (_converse, jid, nick='') { + // Opens a new chatroom + const model = await _converse.api.controlbox.open('controlbox'); + await u.waitUntil(() => model.get('connected')); + await mock.openControlBox(_converse); + const view = await _converse.chatboxviews.get('controlbox'); + const roomspanel = view.roomspanel; + roomspanel.el.querySelector('.show-add-muc-modal').click(); + mock.closeControlBox(_converse); + const modal = roomspanel.add_room_modal; + await u.waitUntil(() => u.isVisible(modal.el), 1500) + modal.el.querySelector('input[name="chatroom"]').value = jid; + if (nick) { + modal.el.querySelector('input[name="nickname"]').value = nick; + } + modal.el.querySelector('form input[type="submit"]').click(); + await u.waitUntil(() => _converse.chatboxviews.get(jid), 1000); + return _converse.chatboxviews.get(jid); + }; + + mock.openChatRoom = function (_converse, room, server) { + return _converse.api.rooms.open(`${room}@${server}`); + }; + + mock.getRoomFeatures = async function (_converse, muc_jid, features=[]) { + const room = Strophe.getNodeFromJid(muc_jid); + muc_jid = muc_jid.toLowerCase(); + const stanzas = _converse.connection.IQ_stanzas; + const stanza = await u.waitUntil(() => stanzas.filter( + iq => iq.querySelector( + `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop() + ); + const features_stanza = $iq({ + 'from': muc_jid, + 'id': stanza.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', { + 'category': 'conference', + 'name': room[0].toUpperCase() + room.slice(1), + 'type': 'text' + }).up(); + + features = features.length ? features : mock.default_muc_features; + features.forEach(f => features_stanza.c('feature', {'var': f}).up()); + features_stanza.c('x', { 'xmlns':'jabber:x:data', 'type':'result'}) + .c('field', {'var':'FORM_TYPE', 'type':'hidden'}) + .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up() + .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'}) + .c('value').t('This is the description').up().up() + .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'}) + .c('value').t(0); + _converse.connection._dataRecv(mock.createRequest(features_stanza)); + }; + + + mock.waitForReservedNick = async function (_converse, muc_jid, nick) { + const stanzas = _converse.connection.IQ_stanzas; + const selector = `iq[to="${muc_jid.toLowerCase()}"] query[node="x-roomuser-item"]`; + const iq = await u.waitUntil(() => stanzas.filter(s => sizzle(selector, s).length).pop()); + + // We remove the stanza, otherwise we might get stale stanzas returned in our filter above. + stanzas.splice(stanzas.indexOf(iq), 1) + + // The XMPP server returns the reserved nick for this user. + const IQ_id = iq.getAttribute('id'); + const stanza = $iq({ + 'type': 'result', + 'id': IQ_id, + 'from': muc_jid, + 'to': _converse.connection.jid + }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info', 'node': 'x-roomuser-item'}); + if (nick) { + stanza.c('identity', {'category': 'conference', 'name': nick, 'type': 'text'}); + } + _converse.connection._dataRecv(mock.createRequest(stanza)); + if (nick) { + return u.waitUntil(() => nick); + } + }; + + + mock.returnMemberLists = async function (_converse, muc_jid, members=[], affiliations=['member', 'owner', 'admin']) { + if (affiliations.length === 0) { + return; + } + const stanzas = _converse.connection.IQ_stanzas; + + if (affiliations.includes('member')) { + const member_IQ = await u.waitUntil(() => _.filter( + stanzas, + s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="member"]`, s).length + ).pop()); + const member_list_stanza = $iq({ + 'from': 'coven@chat.shakespeare.lit', + 'id': member_IQ.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}); + members.filter(m => m.affiliation === 'member').forEach(m => { + member_list_stanza.c('item', { + 'affiliation': m.affiliation, + 'jid': m.jid, + 'nick': m.nick + }); + }); + _converse.connection._dataRecv(mock.createRequest(member_list_stanza)); + } + + if (affiliations.includes('admin')) { + const admin_IQ = await u.waitUntil(() => _.filter( + stanzas, + s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="admin"]`, s).length + ).pop()); + const admin_list_stanza = $iq({ + 'from': 'coven@chat.shakespeare.lit', + 'id': admin_IQ.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}); + members.filter(m => m.affiliation === 'admin').forEach(m => { + admin_list_stanza.c('item', { + 'affiliation': m.affiliation, + 'jid': m.jid, + 'nick': m.nick + }); + }); + _converse.connection._dataRecv(mock.createRequest(admin_list_stanza)); + } + + if (affiliations.includes('owner')) { + const owner_IQ = await u.waitUntil(() => _.filter( + stanzas, + s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="owner"]`, s).length + ).pop()); + const owner_list_stanza = $iq({ + 'from': 'coven@chat.shakespeare.lit', + 'id': owner_IQ.getAttribute('id'), + 'to': 'romeo@montague.lit/orchard', + 'type': 'result' + }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}); + members.filter(m => m.affiliation === 'owner').forEach(m => { + owner_list_stanza.c('item', { + 'affiliation': m.affiliation, + 'jid': m.jid, + 'nick': m.nick + }); + }); + _converse.connection._dataRecv(mock.createRequest(owner_list_stanza)); + } + return new Promise(resolve => _converse.api.listen.on('membersFetched', resolve)); + }; + + mock.receiveOwnMUCPresence = async function (_converse, muc_jid, nick) { + const sent_stanzas = _converse.connection.sent_stanzas; + await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop()); + const presence = $pres({ + to: _converse.connection.jid, + from: `${muc_jid}/${nick}`, + id: u.getUniqueId() + }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) + .c('item').attrs({ + affiliation: 'owner', + jid: _converse.bare_jid, + role: 'moderator' + }).up() + .c('status').attrs({code:'110'}); + _converse.connection._dataRecv(mock.createRequest(presence)); + }; + + + mock.openAndEnterChatRoom = async function (_converse, muc_jid, nick, features=[], members=[]) { + muc_jid = muc_jid.toLowerCase(); + const room_creation_promise = _converse.api.rooms.open(muc_jid); + await mock.getRoomFeatures(_converse, muc_jid, features); + await mock.waitForReservedNick(_converse, muc_jid, nick); + // 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 + await mock.receiveOwnMUCPresence(_converse, muc_jid, nick); + + await room_creation_promise; + const view = _converse.chatboxviews.get(muc_jid); + await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); + + const affs = _converse.muc_fetch_members; + const all_affiliations = Array.isArray(affs) ? affs : (affs ? ['member', 'admin', 'owner'] : []); + await mock.returnMemberLists(_converse, muc_jid, members, all_affiliations); + await view.model.messages.fetched; + }; + + mock.clearChatBoxMessages = function (converse, jid) { + const view = converse.chatboxviews.get(jid); + view.msgs_container.innerHTML = ''; + return view.model.messages.clearStore(); + }; + + mock.createContact = async function (_converse, name, ask, requesting, subscription) { + const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; + if (_converse.roster.get(jid)) { + return Promise.resolve(); + } + const contact = await new Promise((success, error) => { + _converse.roster.create({ + 'ask': ask, + 'fullname': name, + 'jid': jid, + 'requesting': requesting, + 'subscription': subscription + }, {success, error}); + }); + return contact; + }; + + mock.createContacts = async function (_converse, type, length) { + /* Create current (as opposed to requesting or pending) contacts + * for the user's roster. + * + * These contacts are not grouped. See below. + */ + await _converse.api.waitUntil('rosterContactsFetched'); + let names, subscription, requesting, ask; + if (type === 'requesting') { + names = mock.req_names; + subscription = 'none'; + requesting = true; + ask = null; + } else if (type === 'pending') { + names = mock.pend_names; + subscription = 'none'; + requesting = false; + ask = 'subscribe'; + } else if (type === 'current') { + names = mock.cur_names; + subscription = 'both'; + requesting = false; + ask = null; + } else if (type === 'all') { + await this.createContacts(_converse, 'current'); + await this.createContacts(_converse, 'requesting') + await this.createContacts(_converse, 'pending'); + return this; + } else { + throw Error("Need to specify the type of contact to create"); + } + const promises = names.slice(0, length).map(n => this.createContact(_converse, n, ask, requesting, subscription)); + await Promise.all(promises); + }; + + mock.waitForRoster = async function (_converse, type='current', length=-1, include_nick=true, grouped=true) { + const s = `iq[type="get"] query[xmlns="${Strophe.NS.ROSTER}"]`; + const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(iq => sizzle(s, iq).length).pop()); + + const result = $iq({ + 'to': _converse.connection.jid, + 'type': 'result', + 'id': iq.getAttribute('id') + }).c('query', { + 'xmlns': 'jabber:iq:roster' + }); + if (type === 'pending' || type === 'all') { + const pend_names = (length > -1) ? mock.pend_names.slice(0, length) : mock.pend_names; + pend_names.map(name => + result.c('item', { + jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', + name: include_nick ? name : undefined, + subscription: 'none', + ask: 'subscribe' + }).up() + ); + } + if (type === 'current' || type === 'all') { + const cur_names = Object.keys(mock.current_contacts_map); + const names = (length > -1) ? cur_names.slice(0, length) : cur_names; + names.forEach(name => { + result.c('item', { + jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', + name: include_nick ? name : undefined, + subscription: 'both', + ask: null + }); + if (grouped) { + mock.current_contacts_map[name].forEach(g => result.c('group').t(g).up()); + } + result.up(); + }); + } + _converse.connection._dataRecv(mock.createRequest(result)); + await _converse.api.waitUntil('rosterContactsFetched'); + }; + + mock.createChatMessage = function (_converse, sender_jid, message) { + return $msg({ + from: sender_jid, + to: _converse.connection.jid, + type: 'chat', + id: (new Date()).getTime() + }) + .c('body').t(message).up() + .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree(); + } + + mock.sendMessage = function (view, message) { + const promise = new Promise(resolve => view.once('messageInserted', resolve)); + view.el.querySelector('.chat-textarea').value = message; + view.onKeyDown({ + target: view.el.querySelector('textarea.chat-textarea'), + preventDefault: _.noop, + keyCode: 13 + }); + return promise; + }; + window.libsignal = { 'SignalProtocolAddress': function (name, device_id) { @@ -335,3 +758,5 @@ window.addEventListener('converse-loaded', () => { return _converse; } }); + +converse.load(); diff --git a/tests/transpiled.html b/tests/transpiled.html deleted file mode 100644 index f2afdbdb7..000000000 --- a/tests/transpiled.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - Converse.js Tests - - - - - - - - - - - - - - - -
-
-

- Converse.js

-

Tests

-
-
- - diff --git a/tests/utils.js b/tests/utils.js deleted file mode 100644 index 5cbd6ad96..000000000 --- a/tests/utils.js +++ /dev/null @@ -1,437 +0,0 @@ -window.addEventListener('converse-loaded', () => { - const _ = converse.env._; - const $msg = converse.env.$msg; - const $pres = converse.env.$pres; - const $iq = converse.env.$iq; - const Strophe = converse.env.Strophe; - const sizzle = converse.env.sizzle; - const u = converse.env.utils; - const mock = window.mock; - const utils = {}; - - window.test_utils = utils; - - utils.waitUntilDiscoConfirmed = async function (_converse, entity_jid, identities, features=[], items=[], type='info') { - const iq = await u.waitUntil(() => { - return _.filter( - _converse.connection.IQ_stanzas, - (iq) => sizzle(`iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#${type}"]`, iq).length - ).pop(); - }, 300); - const stanza = $iq({ - 'type': 'result', - 'from': entity_jid, - 'to': 'romeo@montague.lit/orchard', - 'id': iq.getAttribute('id'), - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#'+type}); - - _.forEach(identities, function (identity) { - stanza.c('identity', {'category': identity.category, 'type': identity.type}).up() - }); - _.forEach(features, function (feature) { - stanza.c('feature', {'var': feature}).up(); - }); - _.forEach(items, function (item) { - stanza.c('item', {'jid': item}).up(); - }); - _converse.connection._dataRecv(utils.createRequest(stanza)); - } - - utils.createRequest = function (iq) { - iq = typeof iq.tree == "function" ? iq.tree() : iq; - var req = new Strophe.Request(iq, function() {}); - req.getResponse = function () { - var env = new Strophe.Builder('env', {type: 'mock'}).tree(); - env.appendChild(iq); - return env; - }; - return req; - }; - - utils.closeAllChatBoxes = function (_converse) { - return Promise.all(_converse.chatboxviews.map(view => view.close())); - }; - - utils.openControlBox = async function (_converse) { - const model = await _converse.api.controlbox.open(); - await u.waitUntil(() => model.get('connected')); - var toggle = document.querySelector(".toggle-controlbox"); - if (!u.isVisible(document.querySelector("#controlbox"))) { - if (!u.isVisible(toggle)) { - u.removeClass('hidden', toggle); - } - toggle.click(); - } - return this; - }; - - utils.closeControlBox = function () { - const controlbox = document.querySelector("#controlbox"); - if (u.isVisible(controlbox)) { - const button = controlbox.querySelector(".close-chatbox-button"); - if (!_.isNull(button)) { - button.click(); - } - } - return this; - }; - - utils.waitUntilBookmarksReturned = async function (_converse, bookmarks=[]) { - await 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('items[node="storage:bookmarks"]', s).length).pop() - ); - 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'}); - bookmarks.forEach(bookmark => { - stanza.c('conference', { - 'name': bookmark.name, - 'autojoin': bookmark.autojoin, - 'jid': bookmark.jid - }).c('nick').t(bookmark.nick).up().up() - }); - _converse.connection._dataRecv(utils.createRequest(stanza)); - await _converse.api.waitUntil('bookmarksInitialized'); - }; - - utils.openChatBoxes = function (converse, amount) { - const views = []; - for (let i=0; i _converse.chatboxviews.get(jid), 1000); - }; - - utils.openChatRoomViaModal = async function (_converse, jid, nick='') { - // Opens a new chatroom - const model = await _converse.api.controlbox.open('controlbox'); - await u.waitUntil(() => model.get('connected')); - await utils.openControlBox(_converse); - const view = await _converse.chatboxviews.get('controlbox'); - const roomspanel = view.roomspanel; - roomspanel.el.querySelector('.show-add-muc-modal').click(); - utils.closeControlBox(_converse); - const modal = roomspanel.add_room_modal; - await u.waitUntil(() => u.isVisible(modal.el), 1500) - modal.el.querySelector('input[name="chatroom"]').value = jid; - if (nick) { - modal.el.querySelector('input[name="nickname"]').value = nick; - } - modal.el.querySelector('form input[type="submit"]').click(); - await u.waitUntil(() => _converse.chatboxviews.get(jid), 1000); - return _converse.chatboxviews.get(jid); - }; - - utils.openChatRoom = function (_converse, room, server) { - return _converse.api.rooms.open(`${room}@${server}`); - }; - - utils.getRoomFeatures = async function (_converse, muc_jid, features=[]) { - const room = Strophe.getNodeFromJid(muc_jid); - muc_jid = muc_jid.toLowerCase(); - const stanzas = _converse.connection.IQ_stanzas; - const stanza = await u.waitUntil(() => stanzas.filter( - iq => iq.querySelector( - `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` - )).pop() - ); - const features_stanza = $iq({ - 'from': muc_jid, - 'id': stanza.getAttribute('id'), - 'to': 'romeo@montague.lit/desktop', - 'type': 'result' - }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) - .c('identity', { - 'category': 'conference', - 'name': room[0].toUpperCase() + room.slice(1), - 'type': 'text' - }).up(); - - features = features.length ? features : mock.default_muc_features; - features.forEach(f => features_stanza.c('feature', {'var': f}).up()); - features_stanza.c('x', { 'xmlns':'jabber:x:data', 'type':'result'}) - .c('field', {'var':'FORM_TYPE', 'type':'hidden'}) - .c('value').t('http://jabber.org/protocol/muc#roominfo').up().up() - .c('field', {'type':'text-single', 'var':'muc#roominfo_description', 'label':'Description'}) - .c('value').t('This is the description').up().up() - .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'}) - .c('value').t(0); - _converse.connection._dataRecv(utils.createRequest(features_stanza)); - }; - - - utils.waitForReservedNick = async function (_converse, muc_jid, nick) { - const stanzas = _converse.connection.IQ_stanzas; - const selector = `iq[to="${muc_jid.toLowerCase()}"] query[node="x-roomuser-item"]`; - const iq = await u.waitUntil(() => stanzas.filter(s => sizzle(selector, s).length).pop()); - - // We remove the stanza, otherwise we might get stale stanzas returned in our filter above. - stanzas.splice(stanzas.indexOf(iq), 1) - - // The XMPP server returns the reserved nick for this user. - const IQ_id = iq.getAttribute('id'); - const stanza = $iq({ - 'type': 'result', - 'id': IQ_id, - 'from': muc_jid, - 'to': _converse.connection.jid - }).c('query', {'xmlns': 'http://jabber.org/protocol/disco#info', 'node': 'x-roomuser-item'}); - if (nick) { - stanza.c('identity', {'category': 'conference', 'name': nick, 'type': 'text'}); - } - _converse.connection._dataRecv(utils.createRequest(stanza)); - if (nick) { - return u.waitUntil(() => nick); - } - }; - - - utils.returnMemberLists = async function (_converse, muc_jid, members=[], affiliations=['member', 'owner', 'admin']) { - if (affiliations.length === 0) { - return; - } - const stanzas = _converse.connection.IQ_stanzas; - - if (affiliations.includes('member')) { - const member_IQ = await u.waitUntil(() => _.filter( - stanzas, - s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="member"]`, s).length - ).pop()); - const member_list_stanza = $iq({ - 'from': 'coven@chat.shakespeare.lit', - 'id': member_IQ.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}); - members.filter(m => m.affiliation === 'member').forEach(m => { - member_list_stanza.c('item', { - 'affiliation': m.affiliation, - 'jid': m.jid, - 'nick': m.nick - }); - }); - _converse.connection._dataRecv(utils.createRequest(member_list_stanza)); - } - - if (affiliations.includes('admin')) { - const admin_IQ = await u.waitUntil(() => _.filter( - stanzas, - s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="admin"]`, s).length - ).pop()); - const admin_list_stanza = $iq({ - 'from': 'coven@chat.shakespeare.lit', - 'id': admin_IQ.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}); - members.filter(m => m.affiliation === 'admin').forEach(m => { - admin_list_stanza.c('item', { - 'affiliation': m.affiliation, - 'jid': m.jid, - 'nick': m.nick - }); - }); - _converse.connection._dataRecv(utils.createRequest(admin_list_stanza)); - } - - if (affiliations.includes('owner')) { - const owner_IQ = await u.waitUntil(() => _.filter( - stanzas, - s => sizzle(`iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_ADMIN}"] item[affiliation="owner"]`, s).length - ).pop()); - const owner_list_stanza = $iq({ - 'from': 'coven@chat.shakespeare.lit', - 'id': owner_IQ.getAttribute('id'), - 'to': 'romeo@montague.lit/orchard', - 'type': 'result' - }).c('query', {'xmlns': Strophe.NS.MUC_ADMIN}); - members.filter(m => m.affiliation === 'owner').forEach(m => { - owner_list_stanza.c('item', { - 'affiliation': m.affiliation, - 'jid': m.jid, - 'nick': m.nick - }); - }); - _converse.connection._dataRecv(utils.createRequest(owner_list_stanza)); - } - return new Promise(resolve => _converse.api.listen.on('membersFetched', resolve)); - }; - - utils.receiveOwnMUCPresence = async function (_converse, muc_jid, nick) { - const sent_stanzas = _converse.connection.sent_stanzas; - await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop()); - const presence = $pres({ - to: _converse.connection.jid, - from: `${muc_jid}/${nick}`, - id: u.getUniqueId() - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - affiliation: 'owner', - jid: _converse.bare_jid, - role: 'moderator' - }).up() - .c('status').attrs({code:'110'}); - _converse.connection._dataRecv(utils.createRequest(presence)); - }; - - - utils.openAndEnterChatRoom = async function (_converse, muc_jid, nick, features=[], members=[]) { - muc_jid = muc_jid.toLowerCase(); - const room_creation_promise = _converse.api.rooms.open(muc_jid); - await utils.getRoomFeatures(_converse, muc_jid, features); - await utils.waitForReservedNick(_converse, muc_jid, nick); - // 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 - await utils.receiveOwnMUCPresence(_converse, muc_jid, nick); - - await room_creation_promise; - const view = _converse.chatboxviews.get(muc_jid); - await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); - - const affs = _converse.muc_fetch_members; - const all_affiliations = Array.isArray(affs) ? affs : (affs ? ['member', 'admin', 'owner'] : []); - await utils.returnMemberLists(_converse, muc_jid, members, all_affiliations); - await view.model.messages.fetched; - }; - - utils.clearChatBoxMessages = function (converse, jid) { - const view = converse.chatboxviews.get(jid); - view.msgs_container.innerHTML = ''; - return view.model.messages.clearStore(); - }; - - utils.createContact = async function (_converse, name, ask, requesting, subscription) { - const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit'; - if (_converse.roster.get(jid)) { - return Promise.resolve(); - } - const contact = await new Promise((success, error) => { - _converse.roster.create({ - 'ask': ask, - 'fullname': name, - 'jid': jid, - 'requesting': requesting, - 'subscription': subscription - }, {success, error}); - }); - return contact; - }; - - utils.createContacts = async function (_converse, type, length) { - /* Create current (as opposed to requesting or pending) contacts - * for the user's roster. - * - * These contacts are not grouped. See below. - */ - await _converse.api.waitUntil('rosterContactsFetched'); - let names, subscription, requesting, ask; - if (type === 'requesting') { - names = mock.req_names; - subscription = 'none'; - requesting = true; - ask = null; - } else if (type === 'pending') { - names = mock.pend_names; - subscription = 'none'; - requesting = false; - ask = 'subscribe'; - } else if (type === 'current') { - names = mock.cur_names; - subscription = 'both'; - requesting = false; - ask = null; - } else if (type === 'all') { - await this.createContacts(_converse, 'current'); - await this.createContacts(_converse, 'requesting') - await this.createContacts(_converse, 'pending'); - return this; - } else { - throw Error("Need to specify the type of contact to create"); - } - const promises = names.slice(0, length).map(n => this.createContact(_converse, n, ask, requesting, subscription)); - await Promise.all(promises); - }; - - utils.waitForRoster = async function (_converse, type='current', length=-1, include_nick=true, grouped=true) { - const s = `iq[type="get"] query[xmlns="${Strophe.NS.ROSTER}"]`; - const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(iq => sizzle(s, iq).length).pop()); - - const result = $iq({ - 'to': _converse.connection.jid, - 'type': 'result', - 'id': iq.getAttribute('id') - }).c('query', { - 'xmlns': 'jabber:iq:roster' - }); - if (type === 'pending' || type === 'all') { - const pend_names = (length > -1) ? mock.pend_names.slice(0, length) : mock.pend_names; - pend_names.map(name => - result.c('item', { - jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', - name: include_nick ? name : undefined, - subscription: 'none', - ask: 'subscribe' - }).up() - ); - } - if (type === 'current' || type === 'all') { - const cur_names = Object.keys(mock.current_contacts_map); - const names = (length > -1) ? cur_names.slice(0, length) : cur_names; - names.forEach(name => { - result.c('item', { - jid: name.replace(/ /g,'.').toLowerCase() + '@montague.lit', - name: include_nick ? name : undefined, - subscription: 'both', - ask: null - }); - if (grouped) { - mock.current_contacts_map[name].forEach(g => result.c('group').t(g).up()); - } - result.up(); - }); - } - _converse.connection._dataRecv(utils.createRequest(result)); - await _converse.api.waitUntil('rosterContactsFetched'); - }; - - utils.createChatMessage = function (_converse, sender_jid, message) { - return $msg({ - from: sender_jid, - to: _converse.connection.jid, - type: 'chat', - id: (new Date()).getTime() - }) - .c('body').t(message).up() - .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - } - - utils.sendMessage = function (view, message) { - const promise = new Promise(resolve => view.once('messageInserted', resolve)); - view.el.querySelector('.chat-textarea').value = message; - view.onKeyDown({ - target: view.el.querySelector('textarea.chat-textarea'), - preventDefault: _.noop, - keyCode: 13 - }); - return promise; - }; -});