Browse Source

Add new test config for @converse/headless

- Set up a test runner for the headless build
- Move isEqualNode to headless utils
- We now run the headless tests with the headless build and test runner
JC Brand 1 month ago
parent
commit
cd8338aeae
94 changed files with 1259 additions and 1114 deletions
  1. 3 1
      CHANGES.md
  2. 7 2
      Makefile
  3. 23 121
      karma.conf.js
  4. 145 72
      package-lock.json
  5. 1 1
      package.json
  6. 8 11
      rspack/rspack.build.js
  7. 12 6
      rspack/rspack.headless.js
  8. 5 0
      src/headless/index.js
  9. 46 0
      src/headless/karma.conf.js
  10. 2 1
      src/headless/package.json
  11. 3 1
      src/headless/plugins/blocklist/tests/blocklist.js
  12. 5 6
      src/headless/plugins/bookmarks/tests/bookmarks.js
  13. 2 0
      src/headless/plugins/bookmarks/tests/deprecated.js
  14. 4 4
      src/headless/plugins/caps/tests/caps.js
  15. 6 7
      src/headless/plugins/chat/tests/api.js
  16. 2 2
      src/headless/plugins/chat/tests/chat.js
  17. 3 2
      src/headless/plugins/disco/tests/disco.js
  18. 3 3
      src/headless/plugins/mam/tests/api.js
  19. 4 2
      src/headless/plugins/muc/tests/affiliations.js
  20. 3 1
      src/headless/plugins/muc/tests/messages.js
  21. 6 5
      src/headless/plugins/muc/tests/muc.js
  22. 3 1
      src/headless/plugins/muc/tests/occupants.js
  23. 2 1
      src/headless/plugins/muc/tests/pruning.js
  24. 3 2
      src/headless/plugins/muc/tests/registration.js
  25. 3 3
      src/headless/plugins/ping/tests/ping.js
  26. 4 2
      src/headless/plugins/pubsub/tests/config.js
  27. 2 2
      src/headless/plugins/roster/contact.js
  28. 2 2
      src/headless/plugins/roster/tests/presence.js
  29. 14 31
      src/headless/plugins/smacks/tests/smacks.js
  30. 2 1
      src/headless/plugins/status/tests/status.js
  31. 26 22
      src/headless/shared/settings/tests/settings.js
  32. 7 0
      src/headless/tests/bundle-test.js
  33. 1 0
      src/headless/tests/converse.js
  34. 1 1
      src/headless/tests/eventemitter.js
  35. 669 0
      src/headless/tests/mock.js
  36. 1 1
      src/headless/tests/persistence.js
  37. 1 0
      src/headless/tsconfig.json
  38. 2 0
      src/headless/types/index.d.ts
  39. 1 1
      src/headless/types/plugins/roster/contact.d.ts
  40. 7 0
      src/headless/types/utils/html.d.ts
  41. 1 0
      src/headless/types/utils/index.d.ts
  42. 75 12
      src/headless/utils/html.js
  43. 1 2
      src/plugins/bookmark-views/components/bookmarks-list.js
  44. 1 2
      src/plugins/controlbox/model.js
  45. 1 1
      src/plugins/minimize/toggle.js
  46. 1 2
      src/plugins/modal/api.js
  47. 1 2
      src/plugins/modal/modal.js
  48. 1 2
      src/plugins/muc-views/modals/occupant.js
  49. 1 2
      src/plugins/muc-views/occupant.js
  50. 1 2
      src/plugins/muc-views/sidebar-occupant.js
  51. 1 2
      src/plugins/omemo/device.js
  52. 1 2
      src/plugins/omemo/devicelist.js
  53. 1 1
      src/plugins/omemo/devicelists.js
  54. 1 1
      src/plugins/omemo/devices.js
  55. 1 2
      src/plugins/omemo/store.js
  56. 2 2
      src/plugins/omemo/utils.js
  57. 1 2
      src/plugins/profile/modals/profile.js
  58. 1 2
      src/plugins/roomslist/model.js
  59. 2 5
      src/plugins/roomslist/view.js
  60. 3 2
      src/plugins/rosterview/modals/add-contact.js
  61. 1 2
      src/plugins/rosterview/rosterview.js
  62. 2 3
      src/plugins/rosterview/utils.js
  63. 1 2
      src/shared/autocomplete/autocomplete.js
  64. 1 1
      src/shared/chat/baseview.js
  65. 1 2
      src/shared/chat/utils.js
  66. 1 1
      src/shared/components/element.js
  67. 24 583
      src/shared/tests/mock.js
  68. 1 1
      src/types/plugins/bookmark-views/components/bookmarks-list.d.ts
  69. 6 6
      src/types/plugins/chatview/chat.d.ts
  70. 6 6
      src/types/plugins/controlbox/controlbox.d.ts
  71. 1 1
      src/types/plugins/controlbox/model.d.ts
  72. 6 6
      src/types/plugins/dragresize/mixin.d.ts
  73. 7 7
      src/types/plugins/headlines-view/view.d.ts
  74. 1 1
      src/types/plugins/minimize/toggle.d.ts
  75. 1 1
      src/types/plugins/modal/modal.d.ts
  76. 6 6
      src/types/plugins/muc-views/muc.d.ts
  77. 1 1
      src/types/plugins/muc-views/sidebar-occupant.d.ts
  78. 1 1
      src/types/plugins/omemo/device.d.ts
  79. 1 1
      src/types/plugins/omemo/devicelist.d.ts
  80. 1 1
      src/types/plugins/omemo/devicelists.d.ts
  81. 1 1
      src/types/plugins/omemo/devices.d.ts
  82. 1 1
      src/types/plugins/omemo/store.d.ts
  83. 4 4
      src/types/plugins/omemo/utils.d.ts
  84. 1 1
      src/types/plugins/profile/modals/profile.d.ts
  85. 1 1
      src/types/plugins/roomslist/model.d.ts
  86. 4 5
      src/types/plugins/roomslist/view.d.ts
  87. 1 1
      src/types/plugins/rosterview/modals/blocklist.d.ts
  88. 1 1
      src/types/plugins/rosterview/rosterview.d.ts
  89. 4 5
      src/types/plugins/rosterview/utils.d.ts
  90. 6 6
      src/types/shared/autocomplete/autocomplete.d.ts
  91. 2 3
      src/types/shared/chat/utils.d.ts
  92. 6 6
      src/types/shared/components/element.d.ts
  93. 14 12
      src/types/utils/index.d.ts
  94. 1 68
      src/utils/html.js

+ 3 - 1
CHANGES.md

@@ -1,6 +1,6 @@
 # Changelog
 
-## 11.0.2 (Unreleased)
+## 12.0.0 (Unreleased)
 
 - #3700: Fix exception that occurs when optional cp attribute is missing
 - #3730 QR Code is not valid
@@ -8,6 +8,8 @@
 - Some fixes regarding manually resized chats in `overlayed` view mode.
 - Replace webpack with [rspack](https://rspack.rs)
 - Registration: Use https://providers.xmpp.net instead of https://compliance.conversations.im
+- Create ESM builds of converse.js and converse-headless.js
+- Set up a test runner for @converse/headless so that the headless tests use the headless build
 
 ## 11.0.1 (2025-06-09)
 

+ 7 - 2
Makefile

@@ -242,10 +242,15 @@ eslint: node_modules
 	npm run lint
 
 .PHONY: check
-check: eslint | dist/converse.js dist/converse.css
+check: eslint | src/headless/dist/converse-headless.js dist/converse.js dist/converse.css
 	npm run types
 	make check-git-clean
-	npm run test -- $(ARGS)
+	cd src/headless && npm run test -- --single-run
+	npm run test -- --single-run
+
+.PHONY: test-headless
+test-headless:
+	cd src/headless && npm run test -- $(ARGS)
 
 .PHONY: test
 test:

+ 23 - 121
karma.conf.js

@@ -41,130 +41,32 @@ module.exports = function(config) {
         nocache: false
       },
       { pattern: "src/shared/tests/mock.js", type: 'module' },
+      { pattern: "src/headless/tests/mock.js", type: 'module' },
 
-      { pattern: "src/headless/plugins/blocklist/tests/blocklist.js", type: 'module' },
-      { pattern: "src/headless/plugins/bookmarks/tests/bookmarks.js", type: 'module' },
-      { pattern: "src/headless/plugins/bookmarks/tests/deprecated.js", type: 'module' },
-      { pattern: "src/headless/plugins/caps/tests/caps.js", type: 'module' },
-      { pattern: "src/headless/plugins/chat/tests/api.js", type: 'module' },
-      { pattern: "src/headless/plugins/chat/tests/chat.js", type: 'module' },
-      { pattern: "src/headless/plugins/disco/tests/disco.js", type: 'module' },
-      { pattern: "src/headless/plugins/mam/tests/api.js", type: 'module' },
-      { pattern: "src/headless/plugins/muc/tests/affiliations.js", type: 'module' },
-      { pattern: "src/headless/plugins/muc/tests/messages.js", type: 'module' },
-      { pattern: "src/headless/plugins/muc/tests/muc.js", type: 'module' },
-      { pattern: "src/headless/plugins/muc/tests/occupants.js", type: 'module' },
-      { pattern: "src/headless/plugins/muc/tests/pruning.js", type: 'module' },
-      { pattern: "src/headless/plugins/muc/tests/registration.js", type: 'module' },
-      { pattern: "src/headless/plugins/ping/tests/ping.js", type: 'module' },
-      { pattern: "src/headless/plugins/pubsub/tests/config.js", type: 'module' },
-      { pattern: "src/headless/plugins/roster/tests/presence.js", type: 'module' },
-      { pattern: "src/headless/plugins/smacks/tests/smacks.js", type: 'module' },
-      { pattern: "src/headless/plugins/status/tests/status.js", type: 'module' },
+      // Ideally this should go into the headless test runner
       { pattern: "src/headless/plugins/vcard/tests/update.js", type: 'module' },
-      { pattern: "src/headless/shared/settings/tests/settings.js", type: 'module' },
-      { pattern: "src/headless/tests/converse.js", type: 'module' },
-      { pattern: "src/headless/tests/eventemitter.js", type: 'module' },
-      { pattern: "src/plugins/adhoc-views/tests/adhoc.js", type: 'module' },
-      { pattern: "src/plugins/bookmark-views/tests/bookmarks-list.js", type: 'module' },
-      { pattern: "src/plugins/bookmark-views/tests/bookmarks.js", type: 'module' },
-      { pattern: "src/plugins/bookmark-views/tests/deprecated.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/actions.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/chatbox.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/corrections.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/deprecated-retractions.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/emojis.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/http-file-upload.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/markers.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/me-messages.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/message-audio.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/message-avatar.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/message-form.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/message-gifs.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/message-images.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/message-videos.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/messages.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/oob.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/receipts.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/retractions.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/spoilers.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/styling.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/unreads.js", type: 'module' },
-      { pattern: "src/plugins/chatview/tests/xss.js", type: 'module' },
-      { pattern: "src/plugins/controlbox/tests/controlbox.js", type: 'module' },
-      { pattern: "src/plugins/controlbox/tests/login.js", type: 'module' },
-      { pattern: "src/plugins/disco-views/tests/disco-browser.js", type: 'module' },
-      { pattern: "src/plugins/headlines-view/tests/headline.js", type: 'module' },
-      { pattern: "src/plugins/mam-views/tests/mam.js", type: 'module' },
-      { pattern: "src/plugins/mam-views/tests/placeholder.js", type: 'module' },
-      { pattern: "src/plugins/minimize/tests/minchats.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/actions.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/autocomplete.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/commands.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/component.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/corrections.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/csn.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/deprecated-retractions.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/disco.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/drafts.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/emojis.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/hats.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/http-file-upload.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/info-messages.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/mam.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/markers.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/me-messages.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/member-lists.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/mentions.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/mep.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/modtools.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/muc-add-modal.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/muc-api.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/muc-avatar.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/muc-list-modal.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/muc-mentions.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/muc-messages.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/muc-private-messages.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/muc-registration.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/muc.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/mute.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/nickname.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/occupants-filter.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/occupants.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/probes.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/rai.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/retractions.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/styling.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/unfurls.js", type: 'module' },
-      { pattern: "src/plugins/muc-views/tests/xss.js", type: 'module' },
-      { pattern: "src/plugins/notifications/tests/notification.js", type: 'module' },
-      { pattern: "src/plugins/omemo/tests/corrections.js", type: 'module' },
-      { pattern: "src/plugins/omemo/tests/media-sharing.js", type: 'module' },
-      { pattern: "src/plugins/omemo/tests/muc.js", type: 'module' },
-      { pattern: "src/plugins/omemo/tests/omemo.js", type: 'module' },
-      { pattern: "src/plugins/profile/tests/password-reset.js", type: 'module' },
-      { pattern: "src/plugins/profile/tests/profile.js", type: 'module' },
-      { pattern: "src/plugins/profile/tests/status.js", type: 'module' },
-      { pattern: "src/plugins/push/tests/push.js", type: 'module' },
-      { pattern: "src/plugins/register/tests/register.js", type: 'module' },
-      { pattern: "src/plugins/roomslist/tests/grouplists.js", type: 'module' },
-      { pattern: "src/plugins/roomslist/tests/roomslist.js", type: 'module' },
-      { pattern: "src/plugins/rootview/tests/root.js", type: 'module' },
-      { pattern: "src/plugins/rosterview/tests/add-contact-modal.js", type: 'module' },
-      { pattern: "src/plugins/rosterview/tests/blocklist.js", type: 'module' },
-      { pattern: "src/plugins/rosterview/tests/new-chat-modal.js", type: 'module' },
-      { pattern: "src/plugins/rosterview/tests/presence.js", type: 'module' },
-      { pattern: "src/plugins/rosterview/tests/protocol.js", type: 'module' },
-      { pattern: "src/plugins/rosterview/tests/blocklist.js", type: 'module' },
-      { pattern: "src/plugins/rosterview/tests/requesting_contacts.js", type: 'module' },
-      { pattern: "src/plugins/rosterview/tests/roster.js", type: 'module' },
-      { pattern: "src/plugins/rosterview/tests/unsaved-contacts.js", type: 'module' },
-      { pattern: "src/shared/modals/tests/user-details-modal.js", type: 'module' },
-      { pattern: "src/utils/tests/url.js", type: 'module' },
-      { pattern: "src/i18n/tests/i18n.js", type: 'module' },
 
-      // For some reason this test causes issues when its run earlier
-      { pattern: "src/headless/tests/persistence.js", type: 'module' },
+      { pattern: "src/i18n/tests/i18n.js", type: 'module' },
+      { pattern: "src/plugins/adhoc-views/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/bookmark-views/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/chatview/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/controlbox/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/disco-views/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/headlines-view/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/mam-views/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/minimize/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/muc-views/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/notifications/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/omemo/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/profile/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/push/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/register/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/roomslist/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/rootview/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/rosterview/tests/*.js", type: 'module' },
+      { pattern: "src/plugins/rosterview/tests/requesting_contacts.js", type: 'module' },
+      { pattern: "src/shared/modals/tests/*.js", type: 'module' },
+      { pattern: "src/utils/tests/*.js", type: 'module' },
     ],
 
     proxies: {

+ 145 - 72
package-lock.json

@@ -36,7 +36,7 @@
         "@eslint/eslintrc": "^3.3.1",
         "@eslint/js": "^9.24.0",
         "@rspack/cli": "^1.3.15",
-        "@rspack/core": "^1.3.15",
+        "@rspack/core": "^1.4.11",
         "@types/bootstrap": "^5.2.10",
         "@types/lodash-es": "^4.17.12",
         "@types/sizzle": "^2.3.8",
@@ -163,6 +163,40 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/@emnapi/core": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
+      "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.0.4",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
+      "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/wasi-threads": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz",
+      "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/@eslint-community/eslint-utils": {
       "version": "4.7.0",
       "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
@@ -533,62 +567,75 @@
       }
     },
     "node_modules/@module-federation/error-codes": {
-      "version": "0.14.3",
-      "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.14.3.tgz",
-      "integrity": "sha512-sBJ3XKU9g5Up31jFeXPFsD8AgORV7TLO/cCSMuRewSfgYbG/3vSKLJmfHrO6+PvjZSb9VyV2UaF02ojktW65vw==",
+      "version": "0.17.1",
+      "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.17.1.tgz",
+      "integrity": "sha512-n6Elm4qKSjwAPxLUGtwnl7qt4y1dxB8OpSgVvXBIzqI9p27a3ZXshLPLnumlpPg1Qudaj8sLnSnFtt9yGpt5yQ==",
       "dev": true,
       "license": "MIT"
     },
     "node_modules/@module-federation/runtime": {
-      "version": "0.14.3",
-      "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.14.3.tgz",
-      "integrity": "sha512-7ZHpa3teUDVhraYdxQGkfGHzPbjna4LtwbpudgzAxSLLFxLDNanaxCuSeIgSM9c+8sVUNC9kvzUgJEZB0krPJw==",
+      "version": "0.17.1",
+      "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.17.1.tgz",
+      "integrity": "sha512-vKEN32MvUbpeuB/s6UXfkHDZ9N5jFyDDJnj83UTJ8n4N1jHIJu9VZ6Yi4/Ac8cfdvU8UIK9bIbfVXWbUYZUDsw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@module-federation/error-codes": "0.14.3",
-        "@module-federation/runtime-core": "0.14.3",
-        "@module-federation/sdk": "0.14.3"
+        "@module-federation/error-codes": "0.17.1",
+        "@module-federation/runtime-core": "0.17.1",
+        "@module-federation/sdk": "0.17.1"
       }
     },
     "node_modules/@module-federation/runtime-core": {
-      "version": "0.14.3",
-      "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.14.3.tgz",
-      "integrity": "sha512-xMFQXflLVW/AJTWb4soAFP+LB4XuhE7ryiLIX8oTyUoBBgV6U2OPghnFljPjeXbud72O08NYlQ1qsHw1kN/V8Q==",
+      "version": "0.17.1",
+      "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.17.1.tgz",
+      "integrity": "sha512-LCtIFuKgWPQ3E+13OyrVpuTPOWBMI/Ggwsq1Q874YeT8Px28b8tJRCj09DjyRFyhpSPyV/uG80T6iXPAUoLIfQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@module-federation/error-codes": "0.14.3",
-        "@module-federation/sdk": "0.14.3"
+        "@module-federation/error-codes": "0.17.1",
+        "@module-federation/sdk": "0.17.1"
       }
     },
     "node_modules/@module-federation/runtime-tools": {
-      "version": "0.14.3",
-      "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.14.3.tgz",
-      "integrity": "sha512-QBETX7iMYXdSa3JtqFlYU+YkpymxETZqyIIRiqg0gW+XGpH3jgU68yjrme2NBJp7URQi/CFZG8KWtfClk0Pjgw==",
+      "version": "0.17.1",
+      "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.17.1.tgz",
+      "integrity": "sha512-4kr6zTFFwGywJx6whBtxsc84V+COAuuBpEdEbPZN//YLXhNB0iz2IGsy9r9wDl+06h84bD+3dQ05l9euRLgXzQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@module-federation/runtime": "0.14.3",
-        "@module-federation/webpack-bundler-runtime": "0.14.3"
+        "@module-federation/runtime": "0.17.1",
+        "@module-federation/webpack-bundler-runtime": "0.17.1"
       }
     },
     "node_modules/@module-federation/sdk": {
-      "version": "0.14.3",
-      "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.14.3.tgz",
-      "integrity": "sha512-THJZMfbXpqjQOLblCQ8jjcBFFXsGRJwUWE9l/Q4SmuCSKMgAwie7yLT0qSGrHmyBYrsUjAuy+xNB4nfKP0pnGw==",
+      "version": "0.17.1",
+      "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.17.1.tgz",
+      "integrity": "sha512-nlUcN6UTEi+3HWF+k8wPy7gH0yUOmCT+xNatihkIVR9REAnr7BUvHFGlPJmx7WEbLPL46+zJUbtQHvLzXwFhng==",
       "dev": true,
       "license": "MIT"
     },
     "node_modules/@module-federation/webpack-bundler-runtime": {
-      "version": "0.14.3",
-      "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.14.3.tgz",
-      "integrity": "sha512-hIyJFu34P7bY2NeMIUHAS/mYUHEY71VTAsN0A0AqEJFSVPszheopu9VdXq0VDLrP9KQfuXT8SDxeYeJXyj0mgA==",
+      "version": "0.17.1",
+      "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.17.1.tgz",
+      "integrity": "sha512-Swspdgf4PzcbvS9SNKFlBzfq8h/Qxwqjq/xRSqw1pqAZWondZQzwTTqPXhgrg0bFlz7qWjBS/6a8KuH/gRvGaQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@module-federation/runtime": "0.14.3",
-        "@module-federation/sdk": "0.14.3"
+        "@module-federation/runtime": "0.17.1",
+        "@module-federation/sdk": "0.17.1"
+      }
+    },
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz",
+      "integrity": "sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "^1.4.5",
+        "@emnapi/runtime": "^1.4.5",
+        "@tybys/wasm-util": "^0.10.0"
       }
     },
     "node_modules/@nodelib/fs.scandir": {
@@ -957,27 +1004,28 @@
       }
     },
     "node_modules/@rspack/binding": {
-      "version": "1.3.15",
-      "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.3.15.tgz",
-      "integrity": "sha512-utNPuJglLO5lW9XbwIqjB7+2ilMo6JkuVLTVdnNVKU94FW7asn9F/qV+d+MgjUVqU1QPCGm0NuGO9xhbgeJ7pg==",
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.4.11.tgz",
+      "integrity": "sha512-maGl/zRwnl0QVwkBCkgjn5PH20L9HdlRIdkYhEsfTepy5x2QZ0ti/0T49djjTJQrqb+S1i6wWQymMMMMMsxx6Q==",
       "dev": true,
       "license": "MIT",
       "optionalDependencies": {
-        "@rspack/binding-darwin-arm64": "1.3.15",
-        "@rspack/binding-darwin-x64": "1.3.15",
-        "@rspack/binding-linux-arm64-gnu": "1.3.15",
-        "@rspack/binding-linux-arm64-musl": "1.3.15",
-        "@rspack/binding-linux-x64-gnu": "1.3.15",
-        "@rspack/binding-linux-x64-musl": "1.3.15",
-        "@rspack/binding-win32-arm64-msvc": "1.3.15",
-        "@rspack/binding-win32-ia32-msvc": "1.3.15",
-        "@rspack/binding-win32-x64-msvc": "1.3.15"
+        "@rspack/binding-darwin-arm64": "1.4.11",
+        "@rspack/binding-darwin-x64": "1.4.11",
+        "@rspack/binding-linux-arm64-gnu": "1.4.11",
+        "@rspack/binding-linux-arm64-musl": "1.4.11",
+        "@rspack/binding-linux-x64-gnu": "1.4.11",
+        "@rspack/binding-linux-x64-musl": "1.4.11",
+        "@rspack/binding-wasm32-wasi": "1.4.11",
+        "@rspack/binding-win32-arm64-msvc": "1.4.11",
+        "@rspack/binding-win32-ia32-msvc": "1.4.11",
+        "@rspack/binding-win32-x64-msvc": "1.4.11"
       }
     },
     "node_modules/@rspack/binding-darwin-arm64": {
-      "version": "1.3.15",
-      "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.3.15.tgz",
-      "integrity": "sha512-f+DnVRENRdVe+ufpZeqTtWAUDSTnP48jVo7x9KWsXf8XyJHUi+eHKEPrFoy1HvL1/k5yJ3HVnFBh1Hb9cNIwSg==",
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.4.11.tgz",
+      "integrity": "sha512-PrmBVhR8MC269jo6uQ+BMy1uwIDx0HAJYLQRQur8gXiehWabUBCRg/d4U9KR7rLzdaSScRyc5JWXR52T7/4MfA==",
       "cpu": [
         "arm64"
       ],
@@ -989,9 +1037,9 @@
       ]
     },
     "node_modules/@rspack/binding-darwin-x64": {
-      "version": "1.3.15",
-      "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.3.15.tgz",
-      "integrity": "sha512-TfUvEIBqYUT2OK01BYXb2MNcZeZIhAnJy/5aj0qV0uy4KlvwW63HYcKWa1sFd4Ac7bnGShDkanvP3YEuHOFOyg==",
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.4.11.tgz",
+      "integrity": "sha512-YIV8Wzy+JY0SoSsVtN4wxFXOjzxxVPnVXNswrrfqVUTPr9jqGOFYUWCGpbt8lcCgfuBFm6zN8HpOsKm1xUNsVA==",
       "cpu": [
         "x64"
       ],
@@ -1003,9 +1051,9 @@
       ]
     },
     "node_modules/@rspack/binding-linux-arm64-gnu": {
-      "version": "1.3.15",
-      "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.3.15.tgz",
-      "integrity": "sha512-D/YjYk9snKvYm1Elotq8/GsEipB4ZJWVv/V8cZ+ohhFNOPzygENi6JfyI06TryBTQiN0/JDZqt/S9RaWBWnMqw==",
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.4.11.tgz",
+      "integrity": "sha512-ms6uwECUIcu+6e82C5HJhRMHnfsI+l33v7XQezntzRPN0+sG3EpikEoT7SGbgt4vDwaWLR7wS20suN4qd5r3GA==",
       "cpu": [
         "arm64"
       ],
@@ -1017,9 +1065,9 @@
       ]
     },
     "node_modules/@rspack/binding-linux-arm64-musl": {
-      "version": "1.3.15",
-      "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.3.15.tgz",
-      "integrity": "sha512-lJbBsPMOiR0hYPCSM42yp7QiZjfo0ALtX7ws2wURpsQp3BMfRVAmXU3Ixpo2XCRtG1zj8crHaCmAWOJTS0smsA==",
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.4.11.tgz",
+      "integrity": "sha512-9evq0DOdxMN/H8VM8ZmyY9NSuBgILNVV6ydBfVPMHPx4r1E7JZGpWeKDegZcS5Erw3sS9kVSIxyX78L5PDzzKw==",
       "cpu": [
         "arm64"
       ],
@@ -1031,9 +1079,9 @@
       ]
     },
     "node_modules/@rspack/binding-linux-x64-gnu": {
-      "version": "1.3.15",
-      "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.3.15.tgz",
-      "integrity": "sha512-qGB8ucHklrzNg6lsAS36VrBsCbOw0acgpQNqTE5cuHWrp1Pu3GFTRiFEogenxEmzoRbohMZt0Ev5grivrcgKBQ==",
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.4.11.tgz",
+      "integrity": "sha512-bHYFLxPPYBOSaHdQbEoCYGMQ1gOrEWj7Mro/DLfSHZi1a0okcQ2Q1y0i1DczReim3ZhLGNrK7k1IpFXCRbAobQ==",
       "cpu": [
         "x64"
       ],
@@ -1045,9 +1093,9 @@
       ]
     },
     "node_modules/@rspack/binding-linux-x64-musl": {
-      "version": "1.3.15",
-      "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.3.15.tgz",
-      "integrity": "sha512-qRn6e40fLQP+N2rQD8GAj/h4DakeTIho32VxTIaHRVuzw68ZD7VmKkwn55ssN370ejmey35ZdoNFNE12RBrMZA==",
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.4.11.tgz",
+      "integrity": "sha512-wrm4E7q2k4+cwT6Uhp6hIQ3eUe/YoaUttj6j5TqHYZX6YeLrNPtD9+ne6lQQ17BV8wmm6NZsmoFIJ5xIptpRhQ==",
       "cpu": [
         "x64"
       ],
@@ -1058,10 +1106,24 @@
         "linux"
       ]
     },
+    "node_modules/@rspack/binding-wasm32-wasi": {
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.4.11.tgz",
+      "integrity": "sha512-hiYxHZjaZ17wQtXyLCK0IdtOvMWreGVTiGsaHCxyeT+SldDG+r16bXNjmlqfZsjlfl1mkAqKz1dg+mMX28OTqw==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@napi-rs/wasm-runtime": "^1.0.1"
+      }
+    },
     "node_modules/@rspack/binding-win32-arm64-msvc": {
-      "version": "1.3.15",
-      "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.3.15.tgz",
-      "integrity": "sha512-7uJ7dWhO1nWXJiCss6Rslz8hoAxAhFpwpbWja3eHgRb7O4NPHg6MWw63AQSI2aFVakreenfu9yXQqYfpVWJ2dA==",
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.4.11.tgz",
+      "integrity": "sha512-+HF/mnjmTr8PC1dccRt1bkrD2tPDGeqvXC1BBLYd/Klq1VbtIcnrhfmvQM6KaXbiLcY9VWKzcZPOTmnyZ8TaHQ==",
       "cpu": [
         "arm64"
       ],
@@ -1073,9 +1135,9 @@
       ]
     },
     "node_modules/@rspack/binding-win32-ia32-msvc": {
-      "version": "1.3.15",
-      "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.3.15.tgz",
-      "integrity": "sha512-UsaWTYCjDiSCB0A0qETgZk4QvhwfG8gCrO4SJvA+QSEWOmgSai1YV70prFtLLIiyT9mDt1eU3tPWl1UWPRU/EQ==",
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.4.11.tgz",
+      "integrity": "sha512-EU2fQGwrRfwFd/tcOInlD0jy6gNQE4Q3Ayj0Is+cX77sbhPPyyOz0kZDEaQ4qaN2VU8w4Hu/rrD7c0GAKLFvCw==",
       "cpu": [
         "ia32"
       ],
@@ -1087,9 +1149,9 @@
       ]
     },
     "node_modules/@rspack/binding-win32-x64-msvc": {
-      "version": "1.3.15",
-      "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.3.15.tgz",
-      "integrity": "sha512-ZnDIc9Es8EF94MirPDN+hOMt7tkb8nMEbRJFKLMmNd0ElNPgsql+1cY5SqyGRH1hsKB87KfSUQlhFiKZvzbfIg==",
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.4.11.tgz",
+      "integrity": "sha512-1Nc5ZzWqfvE+iJc47qtHFzYYnHsC3awavXrCo74GdGip1vxtksM3G30BlvAQHHVtEmULotWqPbjZpflw/Xk9Ag==",
       "cpu": [
         "x64"
       ],
@@ -1124,14 +1186,14 @@
       }
     },
     "node_modules/@rspack/core": {
-      "version": "1.3.15",
-      "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.3.15.tgz",
-      "integrity": "sha512-QuElIC8jXSKWAp0LSx18pmbhA7NiA5HGoVYesmai90UVxz98tud0KpMxTVCg+0lrLrnKZfCWN9kwjCxM5pGnrA==",
+      "version": "1.4.11",
+      "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.4.11.tgz",
+      "integrity": "sha512-JtKnL6p7Kc/YgWQJF3Woo4OccbgKGyT/4187W4dyex8BMkdQcbqCNIdi6dFk02hwQzxpOOxRSBI4hlGRbz7oYQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@module-federation/runtime-tools": "0.14.3",
-        "@rspack/binding": "1.3.15",
+        "@module-federation/runtime-tools": "0.17.1",
+        "@rspack/binding": "1.4.11",
         "@rspack/lite-tapable": "1.0.1"
       },
       "engines": {
@@ -1183,6 +1245,17 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
+      "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/@types/body-parser": {
       "version": "1.19.6",
       "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",

+ 1 - 1
package.json

@@ -94,7 +94,7 @@
     "@eslint/eslintrc": "^3.3.1",
     "@eslint/js": "^9.24.0",
     "@rspack/cli": "^1.3.15",
-    "@rspack/core": "^1.3.15",
+    "@rspack/core": "^1.4.11",
     "@types/bootstrap": "^5.2.10",
     "@types/lodash-es": "^4.17.12",
     "@types/sizzle": "^2.3.8",

+ 8 - 11
rspack/rspack.build.js

@@ -5,8 +5,13 @@ const common = require('../rspack/rspack.common.js');
 
 const sharedConfig = {
     mode: 'production',
+    entry: {
+        'converse': path.resolve(__dirname, '../src/entry.js'),
+        'converse.min': path.resolve(__dirname, '../src/entry.js'),
+    },
     optimization: {
         minimize: true,
+        moduleIds: 'named', // Helps with debugging
         minimizer: [
             new rspack.SwcJsMinimizerRspackPlugin({
                 minimizerOptions: {
@@ -98,10 +103,6 @@ module.exports = [
     merge(common, {
         ...sharedConfig,
         plugins,
-        entry: {
-            'converse': path.resolve(__dirname, '../src/entry.js'),
-            'converse.min': path.resolve(__dirname, '../src/entry.js'),
-        },
         output: {
             filename: '[name].js',
         },
@@ -110,10 +111,6 @@ module.exports = [
     merge(common, {
         ...sharedConfig,
         plugins,
-        entry: {
-            'converse': path.resolve(__dirname, '../src/entry.js'),
-            'converse.min': path.resolve(__dirname, '../src/entry.js'),
-        },
         experiments: {
             outputModule: true,
             topLevelAwait: true,
@@ -121,8 +118,8 @@ module.exports = [
         output: {
             filename: '[name].esm.js',
             library: {
-                type: 'module'
-            }
+                type: 'module',
+            },
         },
-    })
+    }),
 ];

+ 12 - 6
rspack/rspack.headless.js

@@ -1,12 +1,22 @@
 const path = require('path');
+const { rspack } = require('@rspack/core');
 const { merge } = require('webpack-merge');
 const common = require('../rspack/rspack.common.js');
 
+const plugins = [
+    new rspack.CopyRspackPlugin({
+        patterns: [
+            { from: 'src/headless/plugins/emoji/emoji.json', to: 'emoji.json' },
+        ],
+    }),
+];
+
 const sharedConfig = {
     entry: {
-        'converse-headless': '@converse/headless',
-        'converse-headless.min': '@converse/headless',
+        'converse-headless': path.resolve(__dirname, '../src/headless/index.js'),
+        'converse-headless.min': path.resolve(__dirname, '../src/headless/index.js'),
     },
+    plugins,
     mode: 'production',
     module: {
         rules: [
@@ -35,10 +45,6 @@ module.exports = [
             filename: '[name].js',
             chunkFilename: '[name].js',
             globalObject: 'this',
-            library: {
-                name: 'converse',
-                type: 'umd',
-            },
         },
     }),
     // ESM Build

+ 5 - 0
src/headless/index.js

@@ -8,6 +8,9 @@ import { _converse, api, constants as shared_constants, i18n, parsers } from './
 import u from './utils/index.js';
 import converse from './shared/api/public.js';
 
+export { Collection, EventEmitter, Model } from '@converse/skeletor';
+export { Builder, Stanza } from 'strophe.js';
+
 import BaseMessage from './shared/message.js';
 export { BaseMessage };
 
@@ -54,4 +57,6 @@ Object.assign(_converse.constants, constants);
 import * as errors from './shared/errors.js';
 export { api, converse, _converse, i18n, log, u, constants, parsers, errors };
 
+window['converse'] = converse;
+
 export default converse;

+ 46 - 0
src/headless/karma.conf.js

@@ -0,0 +1,46 @@
+/* global module */
+module.exports = function(config) {
+  config.set({
+    basePath: '',
+    frameworks: ['jasmine'],
+    files: [
+      {
+        pattern: "dist/emoji.json",
+        watched: false,
+        included: false,
+        served: true,
+        type: 'json'
+      },
+      { pattern: 'dist/*.js.map', included: false },
+      "dist/converse-headless.js",
+      { pattern: "tests/*.js", type: 'module' },
+      { pattern: "shared/settings/tests/settings.js", type: 'module' },
+      { pattern: "plugins/blocklist/tests/*.js", type: 'module' },
+      { pattern: "plugins/caps/tests/*.js", type: 'module' },
+      { pattern: "plugins/bookmarks/tests/*.js", type: 'module' },
+      { pattern: "plugins/chat/tests/*.js", type: 'module' },
+      { pattern: "plugins/disco/tests/*.js", type: 'module' },
+      { pattern: "plugins/mam/tests/*.js", type: 'module' },
+      { pattern: "plugins/muc/tests/*.js", type: 'module' },
+      { pattern: "plugins/ping/tests/*.js", type: 'module' },
+      { pattern: "plugins/pubsub/tests/*.js", type: 'module' },
+      { pattern: "plugins/roster/tests/*.js", type: 'module' },
+      { pattern: "plugins/smacks/tests/*.js", type: 'module' },
+      { pattern: "plugins/status/tests/*.js", type: 'module' },
+    ],
+    client: {
+      jasmine: {
+        random: false
+      }
+    },
+    exclude: ['**/*.sw?'],
+    reporters: ['progress', 'kjhtml'],
+    port: 9876,
+    colors: true,
+    logLevel: config.LOG_INFO,
+    autoWatch: true,
+    browsers: ['Chrome'],
+    singleRun: false,
+    concurrency: Infinity
+  })
+}

+ 2 - 1
src/headless/package.json

@@ -53,7 +53,8 @@
     "directory": "src/headless"
   },
   "scripts": {
-    "test": "echo \"Error: run tests from root\" && exit 1"
+    "test": "karma start karma.conf.js",
+    "types": "tsc -p tsconfig.json"
   },
   "bugs": {
     "url": "https://github.com/conversejs/converse.js/issues"

+ 3 - 1
src/headless/plugins/blocklist/tests/blocklist.js

@@ -1,4 +1,6 @@
-/*global mock, converse */
+/*global converse */
+import mock from "../../../tests/mock.js";
+
 const { u, stx } = converse.env;
 
 describe('A blocklist', function () {

+ 5 - 6
src/headless/plugins/bookmarks/tests/bookmarks.js

@@ -1,6 +1,6 @@
-/* global mock, converse */
-const { Strophe, sizzle, stx, u } = converse.env;
-
+/* global converse */
+import mock from "../../../tests/mock.js";
+const { sizzle, stx, u } = converse.env;
 
 describe("A bookmark", function () {
 
@@ -216,14 +216,13 @@ describe("A bookmark", function () {
 
             await mock.waitForMUCDiscoInfo(_converse, jid);
             await mock.waitForReservedNick(_converse, jid, '');
-            await u.waitUntil(() => state.chatboxes.length === 2);
+            await u.waitUntil(() => state.chatboxes.length === 1);
 
             bookmarks.remove(model);
-            await u.waitUntil(() => state.chatboxes.length === 1);
+            await u.waitUntil(() => state.chatboxes.length === 0);
         }));
 
         it("has autojoin set to false upon leaving", mock.initConverse([], {}, async function (_converse) {
-            const { u } = converse.env;
             await mock.waitForRoster(_converse, 'current', 0);
             await mock.waitUntilBookmarksReturned(_converse);
 

+ 2 - 0
src/headless/plugins/bookmarks/tests/deprecated.js

@@ -1,3 +1,5 @@
+/* global converse */
+import mock from "../../../tests/mock.js";
 const { sizzle, stx, u } = converse.env;
 
 describe("A chat room", function () {

+ 4 - 4
src/headless/plugins/caps/tests/caps.js

@@ -1,4 +1,4 @@
-/*global mock */
+import mock from "../../../tests/mock.js";
 
 const original_timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
 
@@ -46,7 +46,7 @@ describe('A sent presence stanza', function () {
                 <status>Hello world</status>
                 <priority>0</priority>
                 <x xmlns="vcard-temp:x:update"/>
-                <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
+                <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
             </presence>`);
 
             api.settings.set('priority', 2);
@@ -57,7 +57,7 @@ describe('A sent presence stanza', function () {
                 <status>Going jogging</status>
                 <priority>2</priority>
                 <x xmlns="vcard-temp:x:update"/>
-                <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
+                <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
             </presence>`);
 
             api.settings.set('priority', undefined);
@@ -68,7 +68,7 @@ describe('A sent presence stanza', function () {
                 <status>Doing taxes</status>
                 <priority>0</priority>
                 <x xmlns="vcard-temp:x:update"/>
-                <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
+                <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
             </presence>`);
         })
     );

+ 6 - 7
src/headless/plugins/chat/tests/api.js

@@ -1,4 +1,7 @@
-/* global mock, converse */
+/* global converse */
+import mock from "../../../tests/mock.js";
+
+const { u } = converse.env;
 
 describe("The \"chats\" API", function() {
 
@@ -6,9 +9,6 @@ describe("The \"chats\" API", function() {
             ['rosterInitialized', 'chatBoxesInitialized'], {},
             async (_converse) => {
 
-        const u = converse.env.utils;
-
-        await mock.openControlBox(_converse);
         await mock.waitForRoster(_converse, 'current', 2);
 
         // Test on chat that doesn't exist.
@@ -20,7 +20,7 @@ describe("The \"chats\" API", function() {
         // Test on chat that's not open
         chat = await _converse.api.chats.get(jid);
         expect(chat === null).toBeTruthy();
-        expect(_converse.chatboxes.length).toBe(1);
+        expect(_converse.chatboxes.length).toBe(0);
 
         // Test for one JID
         chat = await _converse.api.chats.open(jid);
@@ -29,7 +29,7 @@ describe("The \"chats\" API", function() {
 
         // Test for multiple JIDs
         await mock.openChatBoxFor(_converse, jid2);
-        await u.waitUntil(() => _converse.chatboxes.length == 3);
+        await u.waitUntil(() => _converse.chatboxes.length == 2);
         const list = await _converse.api.chats.get([jid, jid2]);
         expect(Array.isArray(list)).toBeTruthy();
         expect(list[0].get('box_id')).toBe(`box-${jid}`);
@@ -39,7 +39,6 @@ describe("The \"chats\" API", function() {
     it("has a method 'open' which opens and returns a promise that resolves to a chat model", mock.initConverse(
             ['chatBoxesInitialized'], {}, async (_converse) => {
 
-        await mock.openControlBox(_converse);
         await mock.waitForRoster(_converse, 'current', 2);
 
         const jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';

+ 2 - 2
src/headless/plugins/chat/tests/chat.js

@@ -1,5 +1,5 @@
-
-/* global mock, converse */
+/* global converse */
+import mock from "../../../tests/mock.js";
 
 describe("A ChatBox", function() {
 

+ 3 - 2
src/headless/plugins/disco/tests/disco.js

@@ -1,6 +1,7 @@
-/*global mock, converse */
+/*global converse */
+import mock from "../../../tests/mock.js";
 
-const { u, $iq, stx } = converse.env;
+const { u, stx } = converse.env;
 
 describe("Service Discovery", function () {
 

+ 3 - 3
src/headless/plugins/mam/tests/api.js

@@ -1,9 +1,9 @@
-/*global mock, converse */
+/*global converse */
+import mock from "../../../tests/mock.js";
+
 const { stx } = converse.env;
 const dayjs = converse.env.dayjs;
 const Strophe = converse.env.Strophe;
-const $iq = converse.env.$iq;
-const $msg = converse.env.$msg;
 const u = converse.env.utils;
 const sizzle = converse.env.sizzle;
 

+ 4 - 2
src/headless/plugins/muc/tests/affiliations.js

@@ -1,5 +1,7 @@
-/*global mock, converse */
-const { stx, Strophe } = converse.env;
+/*global converse */
+import mock from "../../../tests/mock.js";
+
+const { stx } = converse.env;
 
 describe('The MUC Affiliations API', function () {
 

+ 3 - 1
src/headless/plugins/muc/tests/messages.js

@@ -1,4 +1,6 @@
-/*global mock, converse */
+/*global converse */
+import mock from "../../../tests/mock.js";
+
 const { Strophe, u, $msg, stx } = converse.env;
 
 describe("A MUC message", function () {

+ 6 - 5
src/headless/plugins/muc/tests/muc.js

@@ -1,4 +1,5 @@
-/*global mock, converse */
+/*global converse */
+import mock from "../../../tests/mock.js";
 
 const { Strophe, sizzle, stx, u } = converse.env;
 
@@ -58,7 +59,7 @@ describe("Groupchats", function () {
                 <presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="${muc_jid}/romeo" xmlns="jabber:client">
                     <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
                     <show>away</show>
-                    <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
+                    <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`);
 
             expect(muc.getOwnOccupant().get('show')).toBe('away');
@@ -73,7 +74,7 @@ describe("Groupchats", function () {
                     <show>xa</show>
                     <priority>0</priority>
                     <x xmlns="vcard-temp:x:update"/>
-                    <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
+                    <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`)
 
             profile.set({ show: 'dnd', status_message: 'Do not disturb' });
@@ -88,7 +89,7 @@ describe("Groupchats", function () {
                     <x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>
                     <show>dnd</show>
                     <status>Do not disturb</status>
-                    <c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>
+                    <c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>
                 </presence>`);
 
             expect(muc2.getOwnOccupant().get('show')).toBe('dnd');
@@ -144,7 +145,7 @@ describe("Groupchats", function () {
             expect(Strophe.serialize(pres)).toBe(
                 `<presence from="${_converse.jid}" id="${pres.getAttribute('id')}" to="coven@chat.shakespeare.lit/romeo" xmlns="jabber:client">`+
                     `<x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x>`+
-                    `<c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>`+
+                    `<c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>`+
                 `</presence>`);
         }));
     });

+ 3 - 1
src/headless/plugins/muc/tests/occupants.js

@@ -1,4 +1,6 @@
-/*global mock, converse */
+/*global converse */
+import mock from "../../../tests/mock.js";
+
 const { Strophe, u, stx } = converse.env;
 
 describe("A MUC occupant", function () {

+ 2 - 1
src/headless/plugins/muc/tests/pruning.js

@@ -1,4 +1,5 @@
-/*global mock, converse */
+/*global converse */
+import mock from "../../../tests/mock.js";
 
 const {  u } = converse.env;
 

+ 3 - 2
src/headless/plugins/muc/tests/registration.js

@@ -1,6 +1,7 @@
-/*global mock, converse */
+/*global converse */
+import mock from "../../../tests/mock.js";
 
-const { $iq, Strophe, sizzle, u } = converse.env;
+const { Strophe, sizzle, u } = converse.env;
 
 describe("Groupchats", function () {
     beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));

+ 3 - 3
src/headless/plugins/ping/tests/ping.js

@@ -1,7 +1,7 @@
-/*global mock, converse */
+/*global converse */
+import mock from "../../../tests/mock.js";
 
-const Strophe = converse.env.Strophe;
-const u = converse.env.utils;
+const { Strophe, u } = converse.env;
 
 
 describe("XMPP Ping", function () {

+ 4 - 2
src/headless/plugins/pubsub/tests/config.js

@@ -1,5 +1,7 @@
-/* global mock, converse */
-const { Strophe, sizzle, stx, u, errors } = converse.env;
+/* global converse */
+import mock from "../../../tests/mock.js";
+
+const { sizzle, stx, u, errors } = converse.env;
 
 describe('The pubsub API', function () {
     beforeAll(() => jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza }));

+ 2 - 2
src/headless/plugins/roster/contact.js

@@ -66,8 +66,8 @@ class RosterContact extends ModelWithVCard(ColorAwareModel(Model)) {
         return this.presence?.getStatus() || 'offline';
     }
 
-    openChat () {
-        api.chats.open(this.get('jid'), {}, true);
+    async openChat () {
+        return await api.chats.open(this.get('jid'), {}, true);
     }
 
     /**

+ 2 - 2
src/headless/plugins/roster/tests/presence.js

@@ -1,4 +1,5 @@
-/*global mock, converse */
+/* global converse */
+import mock from "../../../tests/mock.js";
 
 // See: https://xmpp.org/rfcs/rfc3921.html
 
@@ -7,7 +8,6 @@ describe("A received presence stanza", function () {
     it("has its priority taken into account",
         mock.initConverse([], {}, async (_converse) => {
 
-        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);

+ 14 - 31
src/headless/plugins/smacks/tests/smacks.js

@@ -1,11 +1,7 @@
-/*global mock, converse */
+/* global converse */
+import mock from "../../../tests/mock.js";
 
-const { stx } = converse.env;
-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;
+const { stx, $msg, Strophe, sizzle, u } = converse.env;
 
 describe("XEP-0198 Stream Management", function () {
 
@@ -42,7 +38,7 @@ describe("XEP-0198 Stream Management", function () {
         );
 
         let IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
-        await u.waitUntil(() => IQ_stanzas.length === 5);
+        await u.waitUntil(() => IQ_stanzas.length === 4);
 
         const disco_iq = IQ_stanzas[0];
         expect(disco_iq).toEqualStanza(stx`
@@ -55,26 +51,22 @@ describe("XEP-0198 Stream Management", function () {
         await mock.waitForRoster(_converse, 'current', 1);
 
         expect(IQ_stanzas[2]).toEqualStanza(stx`
-            <iq from="romeo@montague.lit" id="${IQ_stanzas[2].getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">
-            <pubsub xmlns="http://jabber.org/protocol/pubsub"><items node="eu.siacs.conversations.axolotl.devicelist"/></pubsub></iq>`);
-
-        expect(IQ_stanzas[3]).toEqualStanza(stx`
-            <iq from="romeo@montague.lit/orchard" id="${IQ_stanzas[3].getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">
+            <iq from="romeo@montague.lit/orchard" id="${IQ_stanzas[2].getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">
                 <query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
 
-        expect(IQ_stanzas[4]).toEqualStanza(stx`
-            <iq from="romeo@montague.lit/orchard" id="${IQ_stanzas[4].getAttribute('id')}" type="set" xmlns="jabber:client">
+        expect(IQ_stanzas[3]).toEqualStanza(stx`
+            <iq from="romeo@montague.lit/orchard" id="${IQ_stanzas[3].getAttribute('id')}" type="set" xmlns="jabber:client">
                 <enable xmlns="urn:xmpp:carbons:2"/></iq>`);
 
         await u.waitUntil(() => sent_stanzas.filter(s => (s.nodeName === 'presence')).length);
 
-        expect(sent_stanzas.filter(s => (s.nodeName === 'r')).length).toBe(3);
-        expect(_converse.session.get('unacked_stanzas').length).toBe(6);
+        expect(sent_stanzas.filter(s => (s.nodeName === 'r')).length).toBe(2);
+        expect(_converse.session.get('unacked_stanzas').length).toBe(5);
 
         // test handling of acks
         let ack = stx`<a xmlns="urn:xmpp:sm:3" h="2"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(ack));
-        expect(_converse.session.get('unacked_stanzas').length).toBe(4);
+        expect(_converse.session.get('unacked_stanzas').length).toBe(3);
 
         // test handling of ack requests
         let r = stx`<r xmlns="urn:xmpp:sm:3"/>`;
@@ -96,14 +88,13 @@ describe("XEP-0198 Stream Management", function () {
 
         ack = stx`<a xmlns="urn:xmpp:sm:3" h="2"/>`;
         _converse.api.connection.get()._dataRecv(mock.createRequest(ack));
-        expect(_converse.session.get('unacked_stanzas').length).toBe(4);
+        expect(_converse.session.get('unacked_stanzas').length).toBe(3);
 
         expect(_converse.session.get('unacked_stanzas')[0]).toBe(Strophe.serialize(IQ_stanzas[2]));
         expect(_converse.session.get('unacked_stanzas')[1]).toBe(Strophe.serialize(IQ_stanzas[3]));
-        expect(_converse.session.get('unacked_stanzas')[2]).toBe(Strophe.serialize(IQ_stanzas[4]));
-        expect(_converse.session.get('unacked_stanzas')[3]).toBe(
+        expect(_converse.session.get('unacked_stanzas')[2]).toBe(
             `<presence xmlns="jabber:client"><priority>0</priority><x xmlns="vcard-temp:x:update"/>`+
-                `<c hash="sha-1" node="https://conversejs.org" ver="qgxN8hmrdSa2/4/7PUoM9bPFN2s=" xmlns="http://jabber.org/protocol/caps"/>`+
+                `<c hash="sha-1" node="https://conversejs.org" ver="t7NrIuCRhg80cJKAq33v3LKogjI=" xmlns="http://jabber.org/protocol/caps"/>`+
             `</presence>`);
 
         r = stx`<r xmlns="urn:xmpp:sm:3"/>`;
@@ -129,7 +120,7 @@ describe("XEP-0198 Stream Management", function () {
         expect(_converse.session.get('smacks_enabled')).toBe(true);
 
         await new Promise(resolve => _converse.api.listen.once('reconnected', resolve));
-        await u.waitUntil(() => IQ_stanzas.length === 3);
+        await u.waitUntil(() => IQ_stanzas.length === 2);
 
         // Test that unacked stanzas get resent out
         let iq = IQ_stanzas.pop();
@@ -144,14 +135,6 @@ describe("XEP-0198 Stream Management", function () {
                 <query xmlns="http://jabber.org/protocol/disco#info"/>
             </iq>`);
 
-        iq = IQ_stanzas.pop();
-        expect(iq).toEqualStanza(stx`
-            <iq from="romeo@montague.lit" id="${iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">
-                <pubsub xmlns="http://jabber.org/protocol/pubsub">
-                    <items node="eu.siacs.conversations.axolotl.devicelist"/>
-                </pubsub>
-            </iq>`);
-
         expect(IQ_stanzas.filter(iq => sizzle('query[xmlns="jabber:iq:roster"]', iq).pop()).length).toBe(0);
     }));
 

+ 2 - 1
src/headless/plugins/status/tests/status.js

@@ -1,4 +1,5 @@
-/*global mock, converse */
+/*global converse */
+import mock from "../../../tests/mock.js";
 
 const { u, sizzle } = converse.env;
 

+ 26 - 22
src/headless/shared/settings/tests/settings.js

@@ -1,18 +1,18 @@
-/*global mock */
-
+import { initConverse } from "../../../tests/mock.js";
 
 describe("The \"settings\" API", function () {
     it("has methods 'get' and 'set' to set configuration settings",
-            mock.initConverse(null, {'play_sounds': true}, (_converse) => {
+            initConverse(null, { loglevel: 'debug' }, (_converse) => {
 
         const { api } = _converse;
 
         expect(Object.keys(api.settings)).toEqual(["extend", "get", "set", "listen"]);
-        expect(api.settings.get("play_sounds")).toBe(true);
-        api.settings.set("play_sounds", false);
-        expect(api.settings.get("play_sounds")).toBe(false);
-        api.settings.set({"play_sounds": true});
-        expect(api.settings.get("play_sounds")).toBe(true);
+        expect(api.settings.get("loglevel")).toBe('debug');
+        api.settings.set("loglevel", 'warn');
+        expect(api.settings.get("loglevel")).toBe('warn');
+        api.settings.set({"loglevel": 'error'});
+        expect(api.settings.get("loglevel")).toBe('error');
+
         // Only whitelisted settings allowed.
         expect(typeof api.settings.get("non_existing")).toBe("undefined");
         api.settings.set("non_existing", true);
@@ -20,7 +20,7 @@ describe("The \"settings\" API", function () {
     }));
 
     it("extended via settings.extend don't override settings passed in via converse.initialize",
-            mock.initConverse([], {'emoji_categories': {"travel": ":rocket:"}}, (_converse) => {
+            initConverse([], {'emoji_categories': {"travel": ":rocket:"}}, (_converse) => {
 
         expect(_converse.api.settings.get('emoji_categories')?.travel).toBe(':rocket:');
 
@@ -33,7 +33,7 @@ describe("The \"settings\" API", function () {
     }));
 
     it("only overrides the passed in properties",
-            mock.initConverse([],
+            initConverse([],
             {
                 'root': document.createElement('div').attachShadow({ 'mode': 'open' }),
                 'emoji_categories': { 'travel': ':rocket:' },
@@ -60,7 +60,7 @@ describe("Configuration settings", function () {
     describe("when set", function () {
 
         it("will trigger a change event for which listeners can be registered",
-                mock.initConverse([], {}, function (_converse) {
+                initConverse([], {}, function (_converse) {
 
             const { api } = _converse;
             let changed;
@@ -68,24 +68,28 @@ describe("Configuration settings", function () {
                 changed = o;
             }
             api.settings.listen.on('change', callback);
-            api.settings.set('allowed_image_domains', ['conversejs.org']);
-            expect(changed).toEqual({'allowed_image_domains': ['conversejs.org']});
 
-            api.settings.set('allowed_image_domains', ['conversejs.org', 'opkode.com']);
-            expect(changed).toEqual({'allowed_image_domains': ['conversejs.org', 'opkode.com']});
+            expect(api.settings.get('allow_non_roster_messaging')).toBe(true);
+
+            api.settings.set('allow_non_roster_messaging', false);
+            expect(changed).toEqual({ allow_non_roster_messaging: false });
+
+            api.settings.set('allow_non_roster_messaging', true);
+            expect(changed).toEqual({ allow_non_roster_messaging: true });
 
             api.settings.listen.not('change', callback);
 
-            api.settings.set('allowed_image_domains', ['conversejs.org', 'opkode.com', 'inverse.chat']);
-            expect(changed).toEqual({'allowed_image_domains': ['conversejs.org', 'opkode.com']});
+            api.settings.set('allow_non_roster_messaging', false );
+            expect(changed).toEqual({ allow_non_roster_messaging: true });
 
-            api.settings.listen.on('change:allowed_image_domains', callback);
+            api.settings.listen.on('change:clear_cache_on_logout', callback);
 
-            api.settings.set('allowed_video_domains', ['inverse.chat']);
-            expect(changed).toEqual({'allowed_image_domains': ['conversejs.org', 'opkode.com']});
+            expect(api.settings.get('clear_cache_on_logout')).toBe(false);
+            api.settings.set('clear_cache_on_logout', true);
+            expect(changed).toEqual(true);
 
-            api.settings.set('allowed_image_domains', ['inverse.chat']);
-            expect(changed).toEqual(['inverse.chat']);
+            api.settings.set('clear_cache_on_logout', false);
+            expect(changed).toEqual(false);
         }));
     });
 });

+ 7 - 0
src/headless/tests/bundle-test.js

@@ -0,0 +1,7 @@
+describe("The Headless Bundle", function() {
+    it("should load properly", function() {
+        expect(window.converse).toBeDefined();
+        expect(window.converse.env).toBeDefined();
+        expect(window.converse.initialize).toBeDefined();
+    });
+});

+ 1 - 0
src/headless/tests/converse.js

@@ -1,4 +1,5 @@
 /* global mock, converse */
+import mock from "../tests/mock.js";
 
 const { Strophe } = converse.env;
 

+ 1 - 1
src/headless/tests/eventemitter.js

@@ -1,4 +1,4 @@
-/*global mock */
+import mock from "../tests/mock.js";
 
 const container = {};
 

+ 669 - 0
src/headless/tests/mock.js

@@ -0,0 +1,669 @@
+const view_mode = 'overlayed';
+const theme = ['dracula', 'classic', 'cyberpunk', 'nordic'][Math.floor(Math.random() * 4)];
+
+let originalVCardGet;
+let _converse;
+
+export const chatroom_names = [
+    'Dyon van de Wege',
+    'Thomas Kalb',
+    'Dirk Theissen',
+    'Felix Hofmann',
+    'Ka Lek',
+    'Anne Ebersbacher'
+];
+
+export const default_muc_features = [
+    'http://jabber.org/protocol/muc',
+    'jabber:iq:register',
+    'urn:xmpp:mam:2',
+    'urn:xmpp:sid:0',
+    'muc_passwordprotected',
+    'muc_hidden',
+    'muc_temporary',
+    'muc_open',
+    'muc_unmoderated',
+    'muc_anonymous',
+];
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000;
+
+jasmine.toEqualStanza = function toEqualStanza() {
+    return {
+        compare(actual, expected) {
+            const { u } = window.converse.env;
+            const result = { pass: u.isEqualNode(actual, expected) };
+            if (!result.pass) {
+                result.message =
+                    `Stanzas don't match:\n` +
+                    `Actual:\n${(actual.tree?.() ?? actual).outerHTML}\n` +
+                    `Expected:\n${expected.tree().outerHTML}`;
+            }
+            return result;
+        },
+    };
+};
+
+export const domain = 'montague.lit';
+
+export const current_contacts_map = {
+    'Mercutio': ['Colleagues', 'friends & acquaintences'],
+    'Juliet Capulet': ['friends & acquaintences'],
+    'Lady Montague': ['Colleagues', 'Family'],
+    'Lord Montague': ['Family'],
+    'Friar Laurence': ['friends & acquaintences'],
+    'Tybalt': ['friends & acquaintences'],
+    'Lady Capulet': ['ænemies'],
+    'Benviolo': ['friends & acquaintences'],
+    'Balthasar': ['Colleagues'],
+    'Peter': ['Colleagues'],
+    'Abram': ['Colleagues'],
+    'Sampson': ['Colleagues'],
+    'Gregory': ['friends & acquaintences'],
+    'Potpan': [],
+    'Friar John': [],
+};
+export const req_names = ['Escalus, prince of Verona', 'The Nurse', 'Paris'];
+export const pend_names = ['Lord Capulet', 'Guard', 'Servant'];
+export const cur_names = Object.keys(current_contacts_map);
+
+export async function waitForRoster(_converse, type = 'current', length = -1, include_nick = true, grouped = true) {
+    const { u, sizzle } = window.converse.env;
+    const s = `iq[type="get"] query[xmlns="${Strophe.NS.ROSTER}"]`;
+    const iq = await u.waitUntil(() =>
+        _converse.api.connection
+            .get()
+            .IQ_stanzas.filter((iq) => sizzle(s, iq).length)
+            .pop()
+    );
+
+    const result = $iq({
+        'to': _converse.api.connection.get().jid,
+        'type': 'result',
+        'id': iq.getAttribute('id'),
+    }).c('query', {
+        'xmlns': 'jabber:iq:roster',
+    });
+    if (type === 'pending' || type === 'all') {
+        (length > -1 ? pend_names.slice(0, length) : pend_names).map((name) =>
+            result
+                .c('item', {
+                    jid: `${name.replace(/ /g, '.').toLowerCase()}@${domain}`,
+                    name: include_nick ? name : undefined,
+                    subscription: 'none',
+                    ask: 'subscribe',
+                })
+                .up()
+        );
+    }
+    if (type === 'current' || type === 'all') {
+        const cur_names = Object.keys(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()}@${domain}`,
+                name: include_nick ? name : undefined,
+                subscription: 'both',
+                ask: null,
+            });
+            if (grouped) {
+                current_contacts_map[name].forEach((g) => result.c('group').t(g).up());
+            }
+            result.up();
+        });
+    }
+    _converse.api.connection.get()._dataRecv(createRequest(result));
+    await _converse.api.waitUntil('rosterContactsFetched');
+}
+
+export async function waitUntilDiscoConfirmed(
+    _converse,
+    entity_jid,
+    identities,
+    features = [],
+    items = [],
+    type = 'info'
+) {
+    const { u, sizzle } = window.converse.env;
+    const sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#${type}"]`;
+    const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.find((iq) => sizzle(sel, iq).length));
+    const stanza = stx`
+            <iq type="result"
+                from="${entity_jid}"
+                to="${_converse.session.get('jid')}"
+                id="${iq.getAttribute('id')}"
+                xmlns="jabber:client">
+            <query xmlns="http://jabber.org/protocol/disco#${type}">
+                ${identities?.map((identity) => stx`<identity category="${identity.category}" type="${identity.type}"></identity>`)}
+                ${features?.map((feature) => stx`<feature var="${feature}"></feature>`)}
+                ${items?.map((item) => stx`<item jid="${item}"></item>`)}
+            </query>
+            </iq>`;
+    _converse.api.connection.get()._dataRecv(createRequest(stanza));
+}
+
+/**
+ * Returns an item-not-found disco info result, simulating that this was a
+ * new MUC being entered.
+ */
+export async function waitForNewMUCDiscoInfo(_converse, muc_jid) {
+    const { u } = window.converse.env;
+    const { api } = _converse;
+    const connection = api.connection.get();
+    const own_jid = connection.jid;
+    const stanzas = 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 = stx`<iq from="${muc_jid}"
+                id="${stanza.getAttribute('id')}"
+                to="${own_jid}"
+                type="error"
+                xmlns="jabber:client">
+            <error type="cancel">
+                <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
+            </error>
+        </iq>`;
+    _converse.api.connection.get()._dataRecv(createRequest(features_stanza));
+}
+
+export async function waitUntilBookmarksReturned(
+    _converse,
+    bookmarks = [],
+    features = [
+        'http://jabber.org/protocol/pubsub#publish-options',
+        'http://jabber.org/protocol/pubsub#config-node-max',
+        'urn:xmpp:bookmarks:1#compat',
+    ],
+    node = 'urn:xmpp:bookmarks:1'
+) {
+    const { u, sizzle } = window.converse.env;
+    await waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [{ 'category': 'pubsub', 'type': 'pep' }], features);
+    const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
+    const sent_stanza = await u.waitUntil(() =>
+        IQ_stanzas.filter((s) => sizzle(`items[node="${node}"]`, s).length).pop()
+    );
+
+    let stanza;
+    if (node === 'storage:bookmarks') {
+        stanza = stx`
+            <iq to="${_converse.api.connection.get().jid}"
+                type="result"
+                id="${sent_stanza.getAttribute('id')}"
+                xmlns="jabber:client">
+            <pubsub xmlns="${Strophe.NS.PUBSUB}">
+                <items node="storage:bookmarks">
+                    <item id="current">
+                        <storage xmlns="storage:bookmarks">
+                        </storage>
+                    </item>
+                    ${bookmarks.map(
+                        (b) => stx`
+                        <conference name="${b.name}" autojoin="${b.autojoin}" jid="${b.jid}">
+                            ${b.nick ? stx`<nick>${b.nick}</nick>` : ''}
+                        </conference>`
+                    )}
+                </items>
+            </pubsub>
+            </iq>`;
+    } else {
+        stanza = stx`
+            <iq type="result"
+                to="${_converse.jid}"
+                id="${sent_stanza.getAttribute('id')}"
+                xmlns="jabber:client">
+            <pubsub xmlns="${Strophe.NS.PUBSUB}">
+                <items node="urn:xmpp:bookmarks:1">
+                ${bookmarks.map(
+                    (b) => stx`
+                    <item id="${b.jid}">
+                        <conference xmlns="urn:xmpp:bookmarks:1"
+                                    name="${b.name}"
+                                    autojoin="${b.autojoin ?? false}">
+                            ${b.nick ? stx`<nick>${b.nick}</nick>` : ''}
+                            ${b.password ? stx`<password>${b.password}</password>` : ''}
+                        </conference>
+                    </item>`
+                )};
+                </items>
+            </pubsub>
+            </iq>`;
+    }
+
+    _converse.api.connection.get()._dataRecv(createRequest(stanza));
+    await _converse.api.waitUntil('bookmarksInitialized');
+}
+
+export async function receiveOwnMUCPresence(
+    _converse,
+    muc_jid,
+    nick,
+    affiliation = 'owner',
+    role = 'moderator',
+    features = []
+) {
+    const { u, sizzle } = window.converse.env;
+    const sent_stanzas = _converse.api.connection.get().sent_stanzas;
+    await u.waitUntil(() => sent_stanzas.filter((iq) => sizzle('presence history', iq).length).pop());
+
+    _converse.api.connection.get()._dataRecv(
+        createRequest(stx`
+        <presence xmlns="jabber:client"
+                to="${_converse.api.connection.get().jid}"
+                from="${muc_jid}/${nick}"
+                id="${u.getUniqueId()}">
+            <x xmlns="http://jabber.org/protocol/muc#user">
+                <item affiliation="${affiliation}" role="${role}" jid="${_converse.bare_jid}"/>
+                <status code="110"/>
+            </x>
+            ${
+                features.includes(Strophe.NS.OCCUPANTID)
+                    ? stx`<occupant-id xmlns="${Strophe.NS.OCCUPANTID}" id="${u.getUniqueId()}"/>`
+                    : ''
+            }
+            ${_converse.state.profile.get('show') ? stx`<show>${_converse.state.profile.get('show')}</show>` : ''}
+        </presence>`)
+    );
+}
+
+export async function waitForReservedNick(_converse, muc_jid, nick) {
+    const { u, sizzle } = window.converse.env;
+    const stanzas = _converse.api.connection.get().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.api.connection.get().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.api.connection.get()._dataRecv(createRequest(stanza));
+    if (nick) {
+        return u.waitUntil(() => nick);
+    }
+}
+
+export async function waitForMUCDiscoInfo(_converse, muc_jid, features = [], settings = {}) {
+    const { u, Strophe } = window.converse.env;
+
+    const room = Strophe.getNodeFromJid(muc_jid);
+    muc_jid = muc_jid.toLowerCase();
+    const stanzas = _converse.api.connection.get().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()
+    );
+    features = features.length ? features : default_muc_features;
+
+    const features_stanza = stx`
+        <iq from="${muc_jid}"
+            id="${stanza.getAttribute('id')}"
+            to="romeo@montague.lit/desktop"
+            type="result"
+            xmlns="jabber:client">
+            <query xmlns="http://jabber.org/protocol/disco#info">
+                <identity category="conference"
+                          name="${settings.name ?? `${room[0].toUpperCase()}${room.slice(1)}`}"
+                          type="text"/>
+                ${features.map((f) => stx`<feature var="${f}"></feature>`)}
+            </query>
+            <x xmlns="jabber:x:data" type="result">
+                <field var="FORM_TYPE" type="hidden"><value>http://jabber.org/protocol/muc#roominfo</value></field>
+                <field var="muc#roominfo_description" type="text-single" label="Description"><value>This is the description</value></field>
+                <field var="muc#roominfo_occupants" type="text-single" label="Number of occupants"><value>0</value></field>
+            </x>
+        </iq>`;
+    _converse.api.connection.get()._dataRecv(createRequest(features_stanza));
+}
+
+export async function returnMemberLists(_converse, muc_jid, members = [], affiliations = ['member', 'owner', 'admin']) {
+    if (affiliations.length === 0) {
+        return;
+    }
+    const { u, sizzle } = window.converse.env;
+    const stanzas = _converse.api.connection.get().IQ_stanzas;
+
+    if (affiliations.includes('member')) {
+        const member_IQ = await u.waitUntil(() =>
+            stanzas
+                .filter(
+                    (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.api.connection.get()._dataRecv(createRequest(member_list_stanza));
+    }
+
+    if (affiliations.includes('admin')) {
+        const admin_IQ = await u.waitUntil(() =>
+            stanzas
+                .filter(
+                    (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.api.connection.get()._dataRecv(createRequest(admin_list_stanza));
+    }
+
+    if (affiliations.includes('owner')) {
+        const owner_IQ = await u.waitUntil(() =>
+            stanzas
+                .filter(
+                    (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.api.connection.get()._dataRecv(createRequest(owner_list_stanza));
+    }
+    return new Promise((resolve) => _converse.api.listen.on('membersFetched', resolve));
+}
+
+export async function openAndEnterMUC(
+    _converse,
+    muc_jid,
+    nick,
+    features = [],
+    members = [],
+    force_open = true,
+    settings = {},
+    own_affiliation = 'owner',
+    own_role = 'moderator'
+) {
+    const { u } = window.converse.env;
+    const { api } = _converse;
+    muc_jid = muc_jid.toLowerCase();
+
+    const room_creation_promise = api.rooms.open(muc_jid, settings, force_open);
+    await waitForMUCDiscoInfo(_converse, muc_jid, features, settings);
+    await 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 receiveOwnMUCPresence(_converse, muc_jid, nick, own_affiliation, own_role, features);
+
+    await room_creation_promise;
+    const model = _converse.chatboxes.get(muc_jid);
+    await u.waitUntil(() => model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED);
+
+    const affs = api.settings.get('muc_fetch_members');
+    const all_affiliations = Array.isArray(affs) ? affs : affs ? ['member', 'admin', 'owner'] : [];
+
+    if (['member', 'admin', 'owner'].includes(own_affiliation)) {
+        await returnMemberLists(_converse, muc_jid, members, all_affiliations);
+    }
+    await model.messages.fetched;
+    return model;
+}
+
+async function openChatBoxFor(_converse, jid) {
+    await _converse.api.waitUntil('rosterContactsFetched');
+    return _converse.roster.get(jid).openChat();
+}
+
+export function createChatMessage(_converse, sender_jid, message, type = 'chat') {
+    return $msg({
+        from: sender_jid,
+        to: _converse.api.connection.get().jid,
+        type,
+        id: new Date().getTime(),
+    })
+        .c('body')
+        .t(message)
+        .up()
+        .c('markable', { 'xmlns': Strophe.NS.MARKERS })
+        .up()
+        .c('active', { 'xmlns': Strophe.NS.CHATSTATES })
+        .tree();
+}
+
+function getMockVcardFetcher(settings) {
+    const { dayjs } = window.converse.env;
+    return (model, force) => {
+        let jid;
+        if (typeof model === 'string' || model instanceof String) {
+            jid = model;
+        } else if (!model.get('vcard_updated') || force) {
+            jid = model.get('jid') || model.get('muc_jid');
+        }
+
+        let fullname;
+        let nickname;
+        if (!jid || jid == 'romeo@montague.lit') {
+            jid = settings?.vcard?.jid ?? 'romeo@montague.lit';
+            fullname = settings?.vcard?.display_name ?? 'Romeo Montague';
+            nickname = settings?.vcard?.nickname ?? 'Romeo';
+        } else {
+            const name = jid.split('@')[0].replace(/\./g, ' ').split(' ');
+            const last = name.length - 1;
+            name[0] = name[0].charAt(0).toUpperCase() + name[0].slice(1);
+            name[last] = name[last].charAt(0).toUpperCase() + name[last].slice(1);
+            fullname = name.join(' ');
+        }
+        const vcard = $iq().c('vCard').c('FN').t(fullname).up();
+        if (nickname) vcard.c('NICKNAME').t(nickname);
+        const vcard_el = vcard.tree();
+
+        return Promise.resolve({
+            stanza: vcard_el,
+            fullname: vcard_el.querySelector('FN')?.textContent,
+            nickname: vcard_el.querySelector('NICKNAME')?.textContent,
+            image: vcard_el.querySelector('PHOTO BINVAL')?.textContent,
+            image_type: vcard_el.querySelector('PHOTO TYPE')?.textContent,
+            url: vcard_el.querySelector('URL')?.textContent,
+            vcard_updated: dayjs().format(),
+            vcard_error: undefined,
+        });
+    };
+}
+
+function clearIndexedDB() {
+    const { u } = window.converse.env;
+    const promise = u.getOpenPromise();
+    const db_request = window.indexedDB.open('converse-test-persistent');
+    db_request.onsuccess = function () {
+        const db = db_request.result;
+        const bare_jid = 'romeo@montague.lit';
+        let store;
+        try {
+            store = db.transaction([bare_jid], 'readwrite').objectStore(bare_jid);
+        } catch (e) {
+            return promise.resolve();
+        }
+        const request = store.clear();
+        request.onsuccess = promise.resolve();
+        request.onerror = promise.resolve();
+    };
+    db_request.onerror = function (ev) {
+        return promise.reject(ev.target.error);
+    };
+    return promise;
+}
+
+function clearStores() {
+    [localStorage, sessionStorage].forEach((s) =>
+        Object.keys(s).forEach((k) => k.match(/^converse-test-/) && s.removeItem(k))
+    );
+    const cache_key = `converse.room-bookmarksromeo@montague.lit`;
+    window.sessionStorage.removeItem(cache_key + 'fetched');
+}
+
+export function createRequest(stanza) {
+    stanza = typeof stanza.tree == 'function' ? stanza.tree() : stanza;
+    const req = new Strophe.Request(stanza, () => {});
+    req.getResponse = function () {
+        var env = new Strophe.Builder('env', { type: 'mock' }).tree();
+        env.appendChild(stanza);
+        return env;
+    };
+    return req;
+}
+
+async function _initConverse(converse, settings) {
+    clearStores();
+    await clearIndexedDB();
+
+    _converse = await converse.initialize(
+        Object.assign(
+            {
+                animate: false,
+                auto_subscribe: false,
+                bosh_service_url: 'montague.lit/http-bind',
+                disable_effects: true,
+                discover_connection_methods: false,
+                embed_3rd_party_media_players: false,
+                enable_smacks: false,
+                fetch_url_headers: false,
+                i18n: 'en',
+                loglevel: window.location.pathname === '/debug.html' ? 'debug' : 'error',
+                no_trimming: true,
+                persistent_store: 'localStorage',
+                play_sounds: false,
+                theme,
+                use_emojione: false,
+                view_mode,
+            },
+            settings || {}
+        )
+    );
+
+    window._converse = _converse;
+
+    originalVCardGet = originalVCardGet || _converse.api.vcard.get;
+
+    if (!settings?.no_vcard_mocks && _converse.api.vcard) {
+        _converse.api.vcard.get = getMockVcardFetcher(settings);
+    } else {
+        _converse.api.vcard.get = originalVCardGet;
+    }
+
+    if (settings?.auto_login !== false) {
+        await _converse.api.user.login('romeo@montague.lit/orchard', 'secret');
+    }
+    return _converse;
+}
+
+export function initConverse(promise_names = [], settings = null, func) {
+    if (typeof promise_names === 'function') {
+        func = promise_names;
+        promise_names = [];
+        settings = null;
+    }
+
+    return async () => {
+        if (_converse && _converse.api.connection.connected()) {
+            await _converse.api.user.logout();
+        }
+        const el = document.querySelector('#conversejs');
+        if (el) {
+            el.parentElement.removeChild(el);
+        }
+        document.title = 'Converse Tests';
+
+        await _initConverse(window.converse, settings);
+        await Promise.all((promise_names || []).map(_converse.api.waitUntil));
+
+        // eslint-disable-next-line max-len
+        _converse.default_avatar_image =
+            'PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCI+CiA8cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgZmlsbD0iIzU1NSIvPgogPGNpcmNsZSBjeD0iNjQiIGN5PSI0MSIgcj0iMjQiIGZpbGw9IiNmZmYiLz4KIDxwYXRoIGQ9Im0yOC41IDExMiB2LTEyIGMwLTEyIDEwLTI0IDI0LTI0IGgyMyBjMTQgMCAyNCAxMiAyNCAyNCB2MTIiIGZpbGw9IiNmZmYiLz4KPC9zdmc+Cg==';
+        _converse.default_avatar_image_type = 'image/svg+xml';
+
+        try {
+            await func(_converse);
+        } catch (e) {
+            console.error(e);
+            fail(e);
+        }
+    };
+}
+
+export default {
+    chatroom_names,
+    createChatMessage,
+    createRequest,
+    cur_names,
+    default_muc_features,
+    initConverse,
+    openAndEnterMUC,
+    openChatBoxFor,
+    receiveOwnMUCPresence,
+    returnMemberLists,
+    waitForMUCDiscoInfo,
+    waitForNewMUCDiscoInfo,
+    waitForReservedNick,
+    waitForRoster,
+    waitUntilBookmarksReturned,
+    waitUntilDiscoConfirmed,
+};

+ 1 - 1
src/headless/tests/persistence.js

@@ -1,4 +1,4 @@
-/* global mock */
+import mock from "../tests/mock.js";
 
 describe("The persistent store", function() {
 

+ 1 - 0
src/headless/tsconfig.json

@@ -4,6 +4,7 @@
   ],
   "exclude": [
       "**/tests/*",
+      "karma.conf.js",
       "dist/",
       "types/"
   ],

+ 2 - 0
src/headless/types/index.d.ts

@@ -16,6 +16,8 @@ import * as errors from './shared/errors.js';
 import { constants as shared_constants } from './shared/index.js';
 import * as muc_constants from './plugins/muc/constants.js';
 export { BaseMessage, ModelWithMessages, api, converse, _converse, i18n, log, u, parsers, errors };
+export { Collection, EventEmitter, Model } from "@converse/skeletor";
+export { Builder, Stanza } from "strophe.js";
 export { Bookmark, Bookmarks } from "./plugins/bookmarks/index.js";
 export { ChatBox, Message, Messages } from "./plugins/chat/index.js";
 export { MUCMessage, MUCMessages, MUC, MUCOccupant, MUCOccupants } from "./plugins/muc/index.js";

+ 1 - 1
src/headless/types/plugins/roster/contact.d.ts

@@ -154,7 +154,7 @@ declare class RosterContact extends RosterContact_base {
     setPresence(): Promise<void>;
     presence: any;
     getStatus(): any;
-    openChat(): void;
+    openChat(): Promise<any>;
     /**
      * @param {import('./types').ContactDisplayNameOptions} [options]
      * @returns {string}

+ 7 - 0
src/headless/types/utils/html.d.ts

@@ -3,6 +3,13 @@
  * @returns {boolean}
  */
 export function isElement(el: unknown): boolean;
+/**
+ * Given two XML or HTML elements, determine if they're equal
+ * @param {Element} actual
+ * @param {Element} expected
+ * @returns {Boolean}
+ */
+export function isEqualNode(actual: Element, expected: Element): boolean;
 /**
  * @param {Element | typeof Strophe.Builder} stanza
  * @param {string} name

+ 1 - 0
src/headless/types/utils/index.d.ts

@@ -85,6 +85,7 @@ declare const _default: {
     savedLoginInfo(jid: string): Promise<Model>;
     safeSave(model: Model, attributes: any, options: any): void;
     isElement(el: unknown): boolean;
+    isEqualNode(actual: Element, expected: Element): boolean;
     isTagEqual(stanza: Element | typeof import("strophe.js").Builder, name: string): boolean;
     stringToElement(s: string): Element;
     queryChildren(el: HTMLElement, selector: string): ChildNode[];

+ 75 - 12
src/headless/utils/html.js

@@ -1,26 +1,89 @@
 import DOMPurify from 'dompurify';
-import { Strophe } from 'strophe.js';
+import { Strophe, Builder, Stanza } from 'strophe.js';
 
 /**
  * @param {unknown} el
  * @returns {boolean}
  */
-export function isElement (el) {
+export function isElement(el) {
     return el instanceof Element || el instanceof HTMLDocument;
 }
 
+const EMPTY_TEXT_REGEX = /\s*\n\s*/;
+
+/**
+ * @param {Element|Builder|Stanza} el
+ */
+function stripEmptyTextNodes(el) {
+    if (el instanceof Builder || el instanceof Stanza) {
+        el = el.tree();
+    }
+
+    let n;
+    const text_nodes = [];
+    const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, (node) => {
+        if (node.parentElement.nodeName.toLowerCase() === 'body') {
+            return NodeFilter.FILTER_REJECT;
+        }
+        return NodeFilter.FILTER_ACCEPT;
+    });
+    while ((n = walker.nextNode())) text_nodes.push(n);
+    text_nodes.forEach((n) => EMPTY_TEXT_REGEX.test(/** @type {Text} */ (n).data) && n.parentElement.removeChild(n));
+
+    return el;
+}
+
+/**
+ * Given two XML or HTML elements, determine if they're equal
+ * @param {Element} actual
+ * @param {Element} expected
+ * @returns {Boolean}
+ */
+export function isEqualNode(actual, expected) {
+    if (!isElement(actual)) throw new Error('Element being compared must be an Element!');
+
+    expected = stripEmptyTextNodes(expected);
+    actual = stripEmptyTextNodes(actual);
+
+    let isEqual = actual.isEqualNode(expected);
+
+    if (!isEqual) {
+        // XXX: This is a hack.
+        // When creating two XML elements, one via DOMParser, and one via
+        // createElementNS (or createElement), then "isEqualNode" doesn't match.
+        //
+        // For example, in the following code `isEqual` is false:
+        // ------------------------------------------------------
+        // const a = document.createElementNS('foo', 'div');
+        // a.setAttribute('xmlns', 'foo');
+        //
+        // const b = (new DOMParser()).parseFromString('<div xmlns="foo"></div>', 'text/xml').firstElementChild;
+        // const isEqual = a.isEqualNode(div); //  false
+        //
+        // The workaround here is to serialize both elements to string and then use
+        // DOMParser again for both (via xmlHtmlNode).
+        //
+        // This is not efficient, but currently this is only being used in tests.
+        //
+        const { xmlHtmlNode } = Strophe;
+        const actual_string = Strophe.serialize(actual);
+        const expected_string = Strophe.serialize(expected);
+        isEqual =
+            actual_string === expected_string || xmlHtmlNode(actual_string).isEqualNode(xmlHtmlNode(expected_string));
+    }
+    return isEqual;
+}
+
 /**
  * @param {Element | typeof Strophe.Builder} stanza
  * @param {string} name
  * @returns {boolean}
  */
-export function isTagEqual (stanza, name) {
+export function isTagEqual(stanza, name) {
     if (stanza instanceof Strophe.Builder) {
         return isTagEqual(stanza.tree(), name);
     } else if (!(stanza instanceof Element)) {
-        throw Error(
-            "isTagEqual called with value which isn't "+
-            "an element or Strophe.Builder instance");
+        throw Error("isTagEqual called with value which isn't " + 'an element or Strophe.Builder instance');
     } else {
         return Strophe.isTagEqual(stanza, name);
     }
@@ -33,7 +96,7 @@ export function isTagEqual (stanza, name) {
  * @method u#stringToElement
  * @param {string} s - The HTML string
  */
-export function stringToElement (s) {
+export function stringToElement(s) {
     var div = document.createElement('div');
     div.innerHTML = s;
     return div.firstElementChild;
@@ -45,17 +108,17 @@ export function stringToElement (s) {
  * @param {HTMLElement} el - the DOM element
  * @param {string} selector - the selector they should be matched against
  */
-export function queryChildren (el, selector) {
-    return Array.from(el.childNodes).filter(el => (el instanceof Element) && el.matches(selector));
+export function queryChildren(el, selector) {
+    return Array.from(el.childNodes).filter((el) => el instanceof Element && el.matches(selector));
 }
 
 /**
  * @param {Element} el - the DOM element
  * @return {number}
  */
-export function siblingIndex (el) {
+export function siblingIndex(el) {
     /* eslint-disable no-cond-assign */
-    for (var i = 0; el = el.previousElementSibling; i++);
+    for (var i = 0; (el = el.previousElementSibling); i++);
     return i;
 }
 
@@ -65,7 +128,7 @@ const element = document.createElement('div');
  * @param {string} str
  * @return {string}
  */
-export function decodeHTMLEntities (str) {
+export function decodeHTMLEntities(str) {
     if (str && typeof str === 'string') {
         element.innerHTML = DOMPurify.sanitize(str);
         str = element.textContent;

+ 1 - 2
src/plugins/bookmark-views/components/bookmarks-list.js

@@ -1,6 +1,5 @@
 import debounce from 'lodash-es/debounce';
-import { Model } from '@converse/skeletor';
-import { _converse, api, u } from '@converse/headless';
+import { _converse, api, u, Model } from '@converse/headless';
 import tplBookmarksList from './templates/list.js';
 import tplSpinner from 'templates/spinner.js';
 import { CustomElement } from 'shared/components/element.js';

+ 1 - 2
src/plugins/controlbox/model.js

@@ -1,5 +1,4 @@
-import { Model } from '@converse/skeletor';
-import { _converse, api, converse, constants } from '@converse/headless';
+import { _converse, api, converse, constants, Model } from '@converse/headless';
 
 const { dayjs } = converse.env;
 const { CONTROLBOX_TYPE } = constants;

+ 1 - 1
src/plugins/minimize/toggle.js

@@ -1,4 +1,4 @@
-import { Model } from '@converse/skeletor';
+import { Model } from '@converse/headless';
 
 class MinimizedChatsToggle extends Model {
     defaults () { // eslint-disable-line class-methods-use-this

+ 1 - 2
src/plugins/modal/api.js

@@ -1,7 +1,6 @@
 import './alert.js';
 import Confirm from './confirm.js';
-import { Model } from '@converse/skeletor';
-import { api } from '@converse/headless';
+import { api, Model } from '@converse/headless';
 
 let modals = [];
 let modals_map = {};

+ 1 - 2
src/plugins/modal/modal.js

@@ -1,9 +1,8 @@
 import { html } from 'lit';
 import Modal from 'bootstrap/js/src/modal.js';
 import { getOpenPromise } from '@converse/openpromise';
-import { Model } from '@converse/skeletor';
 import { CustomElement } from 'shared/components/element.js';
-import { api, u } from '@converse/headless';
+import { api, u, Model } from '@converse/headless';
 import { modal_close_button } from './templates/buttons.js';
 import tplModal from './templates/modal.js';
 

+ 1 - 2
src/plugins/muc-views/modals/occupant.js

@@ -1,5 +1,4 @@
-import { Model } from '@converse/skeletor';
-import { _converse, api, converse } from "@converse/headless";
+import { _converse, api, converse, Model } from "@converse/headless";
 import { __ } from 'i18n';
 import BaseModal from "plugins/modal/modal.js";
 import tplOccupantModal from "./templates/occupant.js";

+ 1 - 2
src/plugins/muc-views/occupant.js

@@ -1,5 +1,4 @@
-import { Model } from '@converse/skeletor';
-import { _converse, api, converse } from '@converse/headless';
+import { _converse, api, converse, Model } from '@converse/headless';
 import { CustomElement } from 'shared/components/element.js';
 import tplMUCOccupant from './templates/muc-occupant.js';
 import './occupant-bottom-panel.js';

+ 1 - 2
src/plugins/muc-views/sidebar-occupant.js

@@ -1,5 +1,4 @@
-import { Model } from "@converse/skeletor";
-import { MUC, _converse, api } from "@converse/headless";
+import { _converse, api, MUC, Model } from "@converse/headless";
 import { ObservableElement } from "shared/components/observable.js";
 import tplMUCOccupant from "./templates/occupant.js";
 import "./occupant-bottom-panel.js";

+ 1 - 2
src/plugins/omemo/device.js

@@ -1,5 +1,4 @@
-import { Model } from "@converse/skeletor";
-import { _converse, api, converse, log, u } from "@converse/headless";
+import { _converse, api, converse, log, u, Model } from "@converse/headless";
 import { IQError } from "shared/errors.js";
 import { UNDECIDED } from "./consts.js";
 import { parseBundle } from "./utils.js";

+ 1 - 2
src/plugins/omemo/devicelist.js

@@ -1,6 +1,5 @@
-import { Model } from "@converse/skeletor";
 import { getOpenPromise } from "@converse/openpromise";
-import { _converse, api, converse, errors, log, parsers, u } from "@converse/headless";
+import { _converse, api, converse, errors, log, parsers, u, Model } from "@converse/headless";
 
 const { Strophe, stx, sizzle } = converse.env;
 

+ 1 - 1
src/plugins/omemo/devicelists.js

@@ -1,5 +1,5 @@
+import { Collection } from "@converse/headless";
 import DeviceList from "./devicelist.js";
-import { Collection } from "@converse/skeletor";
 
 class DeviceLists extends Collection {
     constructor() {

+ 1 - 1
src/plugins/omemo/devices.js

@@ -1,4 +1,4 @@
-import { Collection } from "@converse/skeletor";
+import { Collection } from "@converse/headless";
 import Device from "./device.js";
 
 class Devices extends Collection {

+ 1 - 2
src/plugins/omemo/store.js

@@ -1,9 +1,8 @@
 /**
  * @typedef {module:plugins-omemo-index.WindowWithLibsignal} WindowWithLibsignal
  */
-import { Model } from "@converse/skeletor";
 import { generateDeviceID } from "./utils.js";
-import { _converse, api, converse, log } from "@converse/headless";
+import { _converse, api, converse, log, Model } from "@converse/headless";
 
 const { Strophe, stx, u } = converse.env;
 

+ 2 - 2
src/plugins/omemo/utils.js

@@ -1013,8 +1013,8 @@ function encryptKey(key_and_tag, device) {
 
 /**
  * @param {MUC|ChatBox} chat
- * @param {{ message: BaseMessage, stanza: import('strophe.js').Builder }} data
- * @return {Promise<{ message: BaseMessage, stanza: import('strophe.js').Builder }>}
+ * @param {{ message: BaseMessage, stanza: import('@converse/headless').Builder }} data
+ * @return {Promise<{ message: BaseMessage, stanza: import('@converse/headless').Builder }>}
  */
 export async function createOMEMOMessageStanza(chat, data) {
     let { stanza } = data;

+ 1 - 2
src/plugins/profile/modals/profile.js

@@ -1,6 +1,5 @@
 import { html } from 'lit';
-import { Model } from '@converse/skeletor';
-import { _converse, api, log } from '@converse/headless';
+import { _converse, api, log, Model } from '@converse/headless';
 import { __ } from 'i18n';
 import BaseModal from 'plugins/modal/modal.js';
 import { compressImage } from 'utils/file.js';

+ 1 - 2
src/plugins/roomslist/model.js

@@ -1,5 +1,4 @@
-import { Model } from '@converse/skeletor';
-import { _converse, api, converse, constants } from "@converse/headless";
+import { _converse, api, converse, constants, Model } from "@converse/headless";
 
 const { Strophe } = converse.env;
 const { OPENED } = constants;

+ 2 - 5
src/plugins/roomslist/view.js

@@ -1,6 +1,3 @@
-/**
- * @typedef {import('@converse/skeletor').Model} Model
- */
 import { _converse, api, converse, u, constants } from '@converse/headless';
 import { __ } from 'i18n';
 import 'plugins/muc-views/modals/muc-details.js';
@@ -38,12 +35,12 @@ export class RoomsList extends CustomElement {
         return tplRoomslist(this);
     }
 
-    /** @param {Model} model */
+    /** @param {import('@converse/headless').Model} model */
     renderIfChatRoom(model) {
         u.muc.isChatRoom(model) && this.requestUpdate();
     }
 
-    /** @param {Model} model */
+    /** @param {import('@converse/headless').Model} model */
     renderIfRelevantChange(model) {
         const attrs = ['bookmarked', 'hidden', 'name', 'num_unread', 'num_unread_general', 'has_activity'];
         const changed = model.changed || {};

+ 3 - 2
src/plugins/rosterview/modals/add-contact.js

@@ -1,9 +1,10 @@
-import { Strophe } from 'strophe.js';
-import { _converse, api, log } from '@converse/headless';
+import { _converse, converse, api, log } from '@converse/headless';
 import BaseModal from 'plugins/modal/modal.js';
 import tplAddContactModal from './templates/add-contact.js';
 import { __ } from 'i18n';
 
+const { Strophe } = converse.env;
+
 import './styles/add-contact.scss';
 
 export default class AddContactModal extends BaseModal {

+ 1 - 2
src/plugins/rosterview/rosterview.js

@@ -1,5 +1,4 @@
-import { Model } from '@converse/skeletor';
-import { _converse, api, u, constants } from "@converse/headless";
+import { _converse, api, u, constants, Model } from "@converse/headless";
 import { CustomElement } from 'shared/components/element.js';
 import { slideIn, slideOut } from 'utils/html.js';
 import tplRoster from "./templates/roster.js";

+ 2 - 3
src/plugins/rosterview/utils.js

@@ -1,5 +1,4 @@
 /**
- * @typedef {import('@converse/skeletor').Model} Model
  * @typedef {import('@converse/headless').RosterContact} RosterContact
  * @typedef {import('@converse/headless').RosterContacts} RosterContacts
  */
@@ -197,7 +196,7 @@ export function isContactFiltered(contact, groupname) {
 /**
  * @param {RosterContact} contact
  * @param {string} groupname
- * @param {Model} model
+ * @param {import('@converse/headless').Model} model
  * @returns {boolean}
  */
 export function shouldShowContact(contact, groupname, model) {
@@ -220,7 +219,7 @@ export function shouldShowContact(contact, groupname, model) {
 
 /**
  * @param {string} group
- * @param {Model} model
+ * @param {import('@converse/headless').Model} model
  */
 export function shouldShowGroup(group, model) {
     if (!model.get('filter_visible')) return true;

+ 1 - 2
src/shared/autocomplete/autocomplete.js

@@ -5,8 +5,7 @@
  */
 
 import { render, html } from 'lit';
-import { EventEmitter } from '@converse/skeletor';
-import { converse, u } from '@converse/headless';
+import { converse, u, EventEmitter } from '@converse/headless';
 import Suggestion from './suggestion.js';
 import { helpers, getAutoCompleteItem, FILTER_CONTAINS, SORT_BY_QUERY_POSITION } from './utils.js';
 

+ 1 - 1
src/shared/chat/baseview.js

@@ -16,7 +16,7 @@ export default class BaseChatView extends CustomElement {
     constructor() {
         super();
         this.jid = /** @type {string} */ null;
-        this.model = /** @type {import('@converse/skeletor').Model} */ null;
+        this.model = /** @type {import('@converse/headless').Model} */ null;
         this.viewportMediaQuery = window.matchMedia(`(max-width: ${MOBILE_CUTOFF}px)`);
         this.renderOnViewportChange = () => this.requestUpdate();
     }

+ 1 - 2
src/shared/chat/utils.js

@@ -2,7 +2,6 @@
  * @typedef {import('plugins/chatview/types').HeadingButtonAttributes} HeadingButtonAttributes
  * @typedef {import('@converse/headless').Message} Message
  * @typedef {import('@converse/headless').MUCMessage} MUCMessage
- * @typedef {import('@converse/skeletor').Model} Model
  * @typedef {import('lit').TemplateResult} TemplateResult
  */
 import { api, converse } from '@converse/headless';
@@ -29,7 +28,7 @@ export function getChatStyle(model) {
 }
 
 /**
- * @param {Model} model
+ * @param {import('@converse/headless').Model} model
  */
 export function getUnreadMsgsDisplay(model) {
     const num_unread = model.get('num_unread') || 0;

+ 1 - 1
src/shared/components/element.js

@@ -1,5 +1,5 @@
 import { LitElement } from 'lit';
-import { EventEmitter } from '@converse/skeletor';
+import { EventEmitter } from '@converse/headless';
 
 export class CustomElement extends EventEmitter(LitElement) {
     constructor() {

+ 24 - 583
src/shared/tests/mock.js

@@ -1,58 +1,29 @@
-let _converse;
+import {
+    chatroom_names,
+    createChatMessage,
+    createRequest,
+    cur_names,
+    current_contacts_map,
+    default_muc_features,
+    domain,
+    initConverse,
+    openAndEnterMUC,
+    pend_names,
+    receiveOwnMUCPresence,
+    req_names,
+    returnMemberLists,
+    waitForMUCDiscoInfo,
+    waitForNewMUCDiscoInfo,
+    waitForReservedNick,
+    waitForRoster,
+    waitUntilBookmarksReturned,
+    waitUntilDiscoConfirmed,
+} from '../../headless/tests/mock.js';
+
 const mock = {};
 const converse = window.converse;
 converse.load();
-const { u, sizzle, Strophe, dayjs, $iq, $msg, $pres } = converse.env;
-
-
-jasmine.DEFAULT_TIMEOUT_INTERVAL = 7000;
-
-jasmine.toEqualStanza = function toEqualStanza () {
-    return {
-        compare (actual, expected) {
-            const result = { pass: u.isEqualNode(actual, expected) };
-            if (!result.pass) {
-                result.message = `Stanzas don't match:\n`+
-                    `Actual:\n${(actual.tree?.() ?? actual).outerHTML}\n`+
-                    `Expected:\n${expected.tree().outerHTML}`;
-            }
-            return result;
-        }
-    }
-}
-
-function initConverse (promise_names=[], settings=null, func) {
-    if (typeof promise_names === "function") {
-        func = promise_names;
-        promise_names = []
-        settings = null;
-    }
-
-    return async () => {
-        if (_converse && _converse.api.connection.connected()) {
-            await _converse.api.user.logout();
-        }
-        const el = document.querySelector('#conversejs');
-        if (el) {
-            el.parentElement.removeChild(el);
-        }
-        document.title = "Converse Tests";
-
-        await _initConverse(settings);
-        await Promise.all((promise_names || []).map(_converse.api.waitUntil));
-
-        // eslint-disable-next-line max-len
-        _converse.default_avatar_image = 'PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCI+CiA8cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgZmlsbD0iIzU1NSIvPgogPGNpcmNsZSBjeD0iNjQiIGN5PSI0MSIgcj0iMjQiIGZpbGw9IiNmZmYiLz4KIDxwYXRoIGQ9Im0yOC41IDExMiB2LTEyIGMwLTEyIDEwLTI0IDI0LTI0IGgyMyBjMTQgMCAyNCAxMiAyNCAyNCB2MTIiIGZpbGw9IiNmZmYiLz4KPC9zdmc+Cg==';
-        _converse.default_avatar_image_type = 'image/svg+xml';
-
-        try {
-            await func(_converse);
-        } catch(e) {
-            console.error(e);
-            fail(e);
-        }
-    }
-}
+const { u, $iq } = converse.env;
 
 function getContactJID(index) {
     return mock.cur_names[index].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -77,35 +48,6 @@ async function checkHeaderToggling(group) {
     expect(u.hasClass('fa-caret-down', toggle.firstElementChild)).toBeTruthy();
 };
 
-async function waitUntilDiscoConfirmed (_converse, entity_jid, identities, features=[], items=[], type='info') {
-    const sel = `iq[to="${entity_jid}"] query[xmlns="http://jabber.org/protocol/disco#${type}"]`;
-    const iq = await u.waitUntil(() => _converse.api.connection.get().IQ_stanzas.find(iq => sizzle(sel, iq).length));
-    const stanza = stx`
-            <iq type="result"
-                from="${entity_jid}"
-                to="${_converse.session.get('jid')}"
-                id="${iq.getAttribute('id')}"
-                xmlns="jabber:client">
-            <query xmlns="http://jabber.org/protocol/disco#${type}">
-                ${identities?.map(identity => stx`<identity category="${identity.category}" type="${identity.type}"></identity>`)}
-                ${features?.map(feature => stx`<feature var="${feature}"></feature>`)}
-                ${items?.map(item => stx`<item jid="${item}"></item>`)}
-            </query>
-            </iq>`;
-    _converse.api.connection.get()._dataRecv(createRequest(stanza));
-}
-
-function createRequest (stanza) {
-    stanza = typeof stanza.tree == "function" ? stanza.tree() : stanza;
-    const req = new Strophe.Request(stanza, () => {});
-    req.getResponse = function () {
-        var env = new Strophe.Builder('env', {type: 'mock'}).tree();
-        env.appendChild(stanza);
-        return env;
-    };
-    return req;
-}
-
 function closeAllChatBoxes (_converse) {
     return Promise.all(_converse.chatboxviews.map(view => view.close()));
 }
@@ -136,7 +78,7 @@ async function waitUntilBlocklistInitialized (_converse, blocklist=[]) {
     window.sessionStorage.removeItem('converse.blocklist-romeo@montague.lit-fetched');
 
     const { api } = _converse;
-    await mock.waitUntilDiscoConfirmed(
+    await waitUntilDiscoConfirmed(
         _converse,
         _converse.domain,
         [{ 'category': 'server', 'type': 'IM' }],
@@ -159,73 +101,6 @@ async function waitUntilBlocklistInitialized (_converse, blocklist=[]) {
     return await api.waitUntil('blocklistInitialized');
 }
 
-async function waitUntilBookmarksReturned (
-    _converse,
-    bookmarks=[],
-    features=[
-        'http://jabber.org/protocol/pubsub#publish-options',
-        'http://jabber.org/protocol/pubsub#config-node-max',
-        'urn:xmpp:bookmarks:1#compat'
-   ],
-    node='urn:xmpp:bookmarks:1'
-) {
-    await waitUntilDiscoConfirmed(
-        _converse, _converse.bare_jid,
-        [{'category': 'pubsub', 'type': 'pep'}],
-        features,
-    );
-    const IQ_stanzas = _converse.api.connection.get().IQ_stanzas;
-    const sent_stanza = await u.waitUntil(
-        () => IQ_stanzas.filter(s => sizzle(`items[node="${node}"]`, s).length).pop()
-    );
-
-    let stanza;
-    if (node === 'storage:bookmarks') {
-        stanza = stx`
-            <iq to="${_converse.api.connection.get().jid}"
-                type="result"
-                id="${sent_stanza.getAttribute('id')}"
-                xmlns="jabber:client">
-            <pubsub xmlns="${Strophe.NS.PUBSUB}">
-                <items node="storage:bookmarks">
-                    <item id="current">
-                        <storage xmlns="storage:bookmarks">
-                        </storage>
-                    </item>
-                    ${bookmarks.map((b) => stx`
-                        <conference name="${b.name}" autojoin="${b.autojoin}" jid="${b.jid}">
-                            ${b.nick ? stx`<nick>${b.nick}</nick>` : ''}
-                        </conference>`)}
-                </items>
-            </pubsub>
-            </iq>`;
-    } else {
-        stanza = stx`
-            <iq type="result"
-                to="${_converse.jid}"
-                id="${sent_stanza.getAttribute('id')}"
-                xmlns="jabber:client">
-            <pubsub xmlns="${Strophe.NS.PUBSUB}">
-                <items node="urn:xmpp:bookmarks:1">
-                ${bookmarks.map((b) => stx`
-                    <item id="${b.jid}">
-                        <conference xmlns="urn:xmpp:bookmarks:1"
-                                    name="${b.name}"
-                                    autojoin="${b.autojoin ?? false}">
-                            ${b.nick ? stx`<nick>${b.nick}</nick>` : ''}
-                            ${b.password ? stx`<password>${b.password}</password>` : ''}
-                        </conference>
-                    </item>`)
-                };
-                </items>
-            </pubsub>
-            </iq>`;
-    }
-
-    _converse.api.connection.get()._dataRecv(createRequest(stanza));
-    await _converse.api.waitUntil('bookmarksInitialized');
-}
-
 function openChatBoxes (converse, amount) {
     for (let i=0; i<amount; i++) {
         const jid = cur_names[i].replace(/ /g,'.').toLowerCase() + '@montague.lit';
@@ -239,185 +114,6 @@ async function openChatBoxFor (_converse, jid) {
     return u.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
 }
 
-/**
- * Returns an item-not-found disco info result, simulating that this was a
- * new MUC being entered.
- */
-async function waitForNewMUCDiscoInfo(_converse, muc_jid) {
-    const { api } = _converse;
-    const connection = api.connection.get();
-    const own_jid = connection.jid;
-    const stanzas = 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 =
-        stx`<iq from="${muc_jid}"
-                id="${stanza.getAttribute('id')}"
-                to="${own_jid}"
-                type="error"
-                xmlns="jabber:client">
-            <error type="cancel">
-                <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
-            </error>
-        </iq>`;
-    _converse.api.connection.get()._dataRecv(mock.createRequest(features_stanza));
-}
-
-async function waitForMUCDiscoInfo (_converse, muc_jid, features=[], settings={}) {
-    const room = Strophe.getNodeFromJid(muc_jid);
-    muc_jid = muc_jid.toLowerCase();
-    const stanzas = _converse.api.connection.get().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': settings.name ?? `${room[0].toUpperCase()}${room.slice(1)}`,
-            'type': 'text'
-        }).up();
-
-    features = features.length ? features : 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.api.connection.get()._dataRecv(createRequest(features_stanza));
-}
-
-
-async function waitForReservedNick (_converse, muc_jid, nick) {
-    const stanzas = _converse.api.connection.get().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.api.connection.get().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.api.connection.get()._dataRecv(createRequest(stanza));
-    if (nick) {
-        return u.waitUntil(() => nick);
-    }
-}
-
-
-async function returnMemberLists (_converse, muc_jid, members=[], affiliations=['member', 'owner', 'admin']) {
-    if (affiliations.length === 0) {
-        return;
-    }
-    const stanzas = _converse.api.connection.get().IQ_stanzas;
-
-    if (affiliations.includes('member')) {
-        const member_IQ = await u.waitUntil(() =>
-            stanzas.filter(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.api.connection.get()._dataRecv(createRequest(member_list_stanza));
-    }
-
-    if (affiliations.includes('admin')) {
-        const admin_IQ = await u.waitUntil(() => stanzas.filter(
-            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.api.connection.get()._dataRecv(createRequest(admin_list_stanza));
-    }
-
-    if (affiliations.includes('owner')) {
-        const owner_IQ = await u.waitUntil(() => stanzas.filter(
-            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.api.connection.get()._dataRecv(createRequest(owner_list_stanza));
-    }
-    return new Promise(resolve => _converse.api.listen.on('membersFetched', resolve));
-}
-
-async function receiveOwnMUCPresence (_converse, muc_jid, nick, affiliation='owner', role='moderator', features=[]) {
-    const sent_stanzas = _converse.api.connection.get().sent_stanzas;
-    await u.waitUntil(() => sent_stanzas.filter(iq => sizzle('presence history', iq).length).pop());
-
-    _converse.api.connection.get()._dataRecv(createRequest(stx`
-        <presence xmlns="jabber:client"
-                to="${_converse.api.connection.get().jid}"
-                from="${muc_jid}/${nick}"
-                id="${u.getUniqueId()}">
-            <x xmlns="http://jabber.org/protocol/muc#user">
-                <item affiliation="${affiliation}" role="${role}" jid="${_converse.bare_jid}"/>
-                <status code="110"/>
-            </x>
-            ${ (features.includes(Strophe.NS.OCCUPANTID))
-                ? stx`<occupant-id xmlns="${Strophe.NS.OCCUPANTID}" id="${u.getUniqueId()}"/>`
-                : ''
-            }
-            ${ _converse.state.profile.get('show')
-                ? stx`<show>${_converse.state.profile.get('show')}</show>`
-                : ''
-            }
-        </presence>`));
-}
-
 async function openAddMUCModal (_converse) {
     await mock.openControlBox(_converse);
     const controlbox = await u.waitUntil(() => _converse.chatboxviews.get('controlbox'));
@@ -427,42 +123,6 @@ async function openAddMUCModal (_converse) {
     return modal;
 }
 
-async function openAndEnterMUC (
-        _converse,
-        muc_jid,
-        nick,
-        features=[],
-        members=[],
-        force_open=true,
-        settings={},
-        own_affiliation='owner',
-        own_role='moderator',
-    ) {
-    const { api } = _converse;
-    muc_jid = muc_jid.toLowerCase();
-
-    const room_creation_promise = api.rooms.open(muc_jid, settings, force_open);
-    await waitForMUCDiscoInfo(_converse, muc_jid, features, settings);
-    await 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 receiveOwnMUCPresence(_converse, muc_jid, nick, own_affiliation, own_role, features);
-
-    await room_creation_promise;
-    const model = _converse.chatboxes.get(muc_jid);
-    await u.waitUntil(() => (model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
-
-    const affs = api.settings.get('muc_fetch_members');
-    const all_affiliations = Array.isArray(affs) ? affs :  (affs ? ['member', 'admin', 'owner'] : []);
-
-    if (['member', 'admin', 'owner'].includes(own_affiliation)) {
-        await returnMemberLists(_converse, muc_jid, members, all_affiliations);
-    }
-    await model.messages.fetched;
-    return model;
-}
-
 async function createContact (_converse, name, ask, requesting, subscription) {
     const jid = name.replace(/ /g,'.').toLowerCase() + '@montague.lit';
     if (_converse.roster.get(jid)) {
@@ -515,58 +175,6 @@ async function createContacts (_converse, type, length) {
     await Promise.all(promises);
 }
 
-async function waitForRoster (_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.api.connection.get().IQ_stanzas.filter(iq => sizzle(s, iq).length).pop());
-
-    const result = $iq({
-        'to': _converse.api.connection.get().jid,
-        'type': 'result',
-        'id': iq.getAttribute('id')
-    }).c('query', {
-        'xmlns': 'jabber:iq:roster'
-    });
-    if (type === 'pending' || type === 'all') {
-        ((length > -1) ? pend_names.slice(0, length) : pend_names).map(name =>
-            result.c('item', {
-                jid: `${name.replace(/ /g,'.').toLowerCase()}@${domain}`,
-                name: include_nick ? name : undefined,
-                subscription: 'none',
-                ask: 'subscribe'
-            }).up()
-        );
-    }
-    if (type === 'current' || type === 'all') {
-        const cur_names = Object.keys(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()}@${domain}`,
-                name: include_nick ? name : undefined,
-                subscription: 'both',
-                ask: null
-            });
-            if (grouped) {
-                current_contacts_map[name].forEach(g => result.c('group').t(g).up());
-            }
-            result.up();
-        });
-    }
-    _converse.api.connection.get()._dataRecv(createRequest(result));
-    await _converse.api.waitUntil('rosterContactsFetched');
-}
-
-function createChatMessage (_converse, sender_jid, message, type='chat') {
-    return $msg({
-                from: sender_jid,
-                to: _converse.api.connection.get().jid,
-                type,
-                id: (new Date()).getTime()
-            })
-            .c('body').t(message).up()
-            .c('markable', {'xmlns': Strophe.NS.MARKERS}).up()
-            .c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
-}
 
 async function sendMessage (view, message) {
     const promise = new Promise(resolve => view.model.messages.once('rendered', resolve));
@@ -638,50 +246,6 @@ window.libsignal = {
     }
 }
 
-const default_muc_features = [
-    'http://jabber.org/protocol/muc',
-    'jabber:iq:register',
-    Strophe.NS.SID,
-    Strophe.NS.MAM,
-    'muc_passwordprotected',
-    'muc_hidden',
-    'muc_temporary',
-    'muc_open',
-    'muc_unmoderated',
-    'muc_anonymous'
-];
-
-const view_mode = 'overlayed';
-
-const domain = 'montague.lit';
-
-// Names from http://www.fakenamegenerator.com/
-const req_names = [
-    'Escalus, prince of Verona', 'The Nurse', 'Paris'
-];
-
-
-const pend_names = [
-    'Lord Capulet', 'Guard', 'Servant'
-];
-const current_contacts_map = {
-    'Mercutio': ['Colleagues', 'friends & acquaintences'],
-    'Juliet Capulet': ['friends & acquaintences'],
-    'Lady Montague': ['Colleagues', 'Family'],
-    'Lord Montague': ['Family'],
-    'Friar Laurence': ['friends & acquaintences'],
-    'Tybalt': ['friends & acquaintences'],
-    'Lady Capulet': ['ænemies'],
-    'Benviolo': ['friends & acquaintences'],
-    'Balthasar': ['Colleagues'],
-    'Peter': ['Colleagues'],
-    'Abram': ['Colleagues'],
-    'Sampson': ['Colleagues'],
-    'Gregory': ['friends & acquaintences'],
-    'Potpan': [],
-    'Friar John': []
-}
-
 
 const map = current_contacts_map;
 const groups_map = {};
@@ -692,7 +256,6 @@ Object.keys(map).forEach(k => {
     });
 });
 
-const cur_names = Object.keys(current_contacts_map);
 const num_contacts = req_names.length + pend_names.length + cur_names.length;
 
 const req_jids = req_names.map((name) => `${name.replace(/ /g, '.').toLowerCase()}@${domain}`);
@@ -706,14 +269,6 @@ const groups = {
     'Ungrouped': 2
 }
 
-const chatroom_names = [
-    'Dyon van de Wege',
-    'Thomas Kalb',
-    'Dirk Theissen',
-    'Felix Hofmann',
-    'Ka Lek',
-    'Anne Ebersbacher'
-];
 
 // TODO: need to also test other roles and affiliations
 const chatroom_roles = {
@@ -729,119 +284,6 @@ const event = {
     'preventDefault': function () {}
 }
 
-function clearIndexedDB () {
-    const promise = u.getOpenPromise();
-    const db_request = window.indexedDB.open("converse-test-persistent");
-    db_request.onsuccess = function () {
-        const db = db_request.result;
-        const bare_jid = "romeo@montague.lit";
-        let store;
-        try {
-            store= db.transaction([bare_jid], "readwrite").objectStore(bare_jid);
-        } catch (e) {
-            return promise.resolve();
-        }
-        const request = store.clear();
-        request.onsuccess = promise.resolve();
-        request.onerror = promise.resolve();
-    };
-    db_request.onerror = function (ev) {
-        return promise.reject(ev.target.error);
-    }
-    return promise;
-}
-
-function clearStores () {
-    [localStorage, sessionStorage].forEach(
-        s => Object.keys(s).forEach(k => k.match(/^converse-test-/) && s.removeItem(k))
-    );
-    const cache_key = `converse.room-bookmarksromeo@montague.lit`;
-    window.sessionStorage.removeItem(cache_key+'fetched');
-}
-
-function getMockVcardFetcher (settings) {
-    return (model, force) => {
-        let jid;
-        if (typeof model === 'string' || model instanceof String) {
-            jid = model;
-        } else if (!model.get('vcard_updated') || force) {
-            jid = model.get('jid') || model.get('muc_jid');
-        }
-
-        let fullname;
-        let nickname;
-        if (!jid || jid == 'romeo@montague.lit') {
-            jid = settings?.vcard?.jid ?? 'romeo@montague.lit';
-            fullname = settings?.vcard?.display_name ?? 'Romeo Montague' ;
-            nickname = settings?.vcard?.nickname ?? 'Romeo';
-        } else {
-            const name = jid.split('@')[0].replace(/\./g, ' ').split(' ');
-            const last = name.length-1;
-            name[0] =  name[0].charAt(0).toUpperCase()+name[0].slice(1);
-            name[last] = name[last].charAt(0).toUpperCase()+name[last].slice(1);
-            fullname = name.join(' ');
-        }
-        const vcard = $iq().c('vCard').c('FN').t(fullname).up();
-        if (nickname) vcard.c('NICKNAME').t(nickname);
-        const vcard_el = vcard.tree();
-
-        return Promise.resolve({
-            stanza: vcard_el,
-            fullname: vcard_el.querySelector('FN')?.textContent,
-            nickname: vcard_el.querySelector('NICKNAME')?.textContent,
-            image: vcard_el.querySelector('PHOTO BINVAL')?.textContent,
-            image_type: vcard_el.querySelector('PHOTO TYPE')?.textContent,
-            url: vcard_el.querySelector('URL')?.textContent,
-            vcard_updated: dayjs().format(),
-            vcard_error: undefined
-        });
-    }
-}
-
-const theme = ['dracula', 'classic', 'cyberpunk', 'nordic'][Math.floor(Math.random()*4)];
-let originalVCardGet;
-
-async function _initConverse (settings) {
-    clearStores();
-    await clearIndexedDB();
-
-
-    _converse = await converse.initialize(Object.assign({
-        animate: false,
-        auto_subscribe: false,
-        bosh_service_url: 'montague.lit/http-bind',
-        disable_effects: true,
-        discover_connection_methods: false,
-        embed_3rd_party_media_players: false,
-        enable_smacks: false,
-        fetch_url_headers: false,
-        i18n: 'en',
-        loglevel: window.location.pathname === '/debug.html' ? 'debug' : 'error',
-        no_trimming: true,
-        persistent_store: 'localStorage',
-        play_sounds: false,
-        theme,
-        use_emojione: false,
-        view_mode,
-    }, settings || {}));
-
-    window._converse = _converse;
-
-    originalVCardGet = originalVCardGet || _converse.api.vcard.get;
-
-    if (!settings?.no_vcard_mocks && _converse.api.vcard) {
-        _converse.api.vcard.get = getMockVcardFetcher(settings);
-    } else {
-        _converse.api.vcard.get = originalVCardGet;
-    }
-
-    if (settings?.auto_login !== false) {
-        await _converse.api.user.login('romeo@montague.lit/orchard', 'secret');
-    }
-    return _converse;
-}
-
-
 async function deviceListFetched (_converse, jid, device_ids) {
     const selector = `iq[to="${jid}"] items[node="eu.siacs.conversations.axolotl.devicelist"]`;
     const iq_stanza = await u.waitUntil(
@@ -989,7 +431,6 @@ Object.assign(mock, {
     returnMemberLists,
     sendMessage,
     toggleControlBox,
-    view_mode,
     waitForMUCDiscoInfo,
     waitForNewMUCDiscoInfo,
     waitForReservedNick,

+ 1 - 1
src/types/plugins/bookmark-views/components/bookmarks-list.d.ts

@@ -9,5 +9,5 @@ export default class BookmarksView extends CustomElement {
     clearFilter(ev: Event): void;
 }
 import { CustomElement } from 'shared/components/element.js';
-import { Model } from '@converse/skeletor';
+import { Model } from '@converse/headless';
 //# sourceMappingURL=bookmarks-list.d.ts.map

+ 6 - 6
src/types/plugins/chatview/chat.d.ts

@@ -20,16 +20,16 @@ declare const ChatView_base: {
         initialize(): any;
         connectedCallback(): any;
         disconnectedCallback(): void;
-        on(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
+        on(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
         _events: any;
         _listeners: {};
-        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
         _listeningTo: {};
         _listenId: any;
-        off(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context?: any): any;
-        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
-        once(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
-        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+        off(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
         trigger(name: string, ...args: any[]): any;
         readonly renderOptions: import("lit-html").RenderOptions;
         __childPart: any;

+ 6 - 6
src/types/plugins/controlbox/controlbox.d.ts

@@ -21,16 +21,16 @@ declare const ControlBoxView_base: {
         initialize(): any;
         connectedCallback(): any;
         disconnectedCallback(): void;
-        on(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
+        on(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
         _events: any;
         _listeners: {};
-        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
         _listeningTo: {};
         _listenId: any;
-        off(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context?: any): any;
-        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
-        once(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
-        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+        off(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
         trigger(name: string, ...args: any[]): any;
         readonly renderOptions: import("lit-html").RenderOptions;
         __childPart: any;

+ 1 - 1
src/types/plugins/controlbox/model.d.ts

@@ -24,5 +24,5 @@ declare class ControlBox extends Model {
     maybeShow(force?: boolean): any;
     onReconnection(): void;
 }
-import { Model } from '@converse/skeletor';
+import { Model } from '@converse/headless';
 //# sourceMappingURL=model.d.ts.map

+ 6 - 6
src/types/plugins/dragresize/mixin.d.ts

@@ -42,16 +42,16 @@ export default function DragResizable<T extends import("shared/components/types"
         initialize(): any;
         connectedCallback(): any;
         disconnectedCallback(): void;
-        on(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
+        on(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
         _events: any;
         _listeners: {};
-        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
         _listeningTo: {};
         _listenId: any;
-        off(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context?: any): any;
-        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
-        once(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
-        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+        off(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
         trigger(name: string, ...args: any[]): any;
         readonly renderOptions: import("lit-html").RenderOptions;
         __childPart: any;

+ 7 - 7
src/types/plugins/headlines-view/view.d.ts

@@ -21,21 +21,21 @@ declare const HeadlinesFeedView_base: {
         initialize(): any;
         connectedCallback(): any;
         disconnectedCallback(): void;
-        on(name: string, callback: (event: any, model: import("@converse/skeletor" /**
+        on(name: string, callback: (event: any, model: import("@converse/headless" /**
          * Triggered once the {@link HeadlinesFeedView} has been initialized
          * @event _converse#headlinesBoxViewInitialized
          * @type {HeadlinesFeedView}
          * @example _converse.api.listen.on('headlinesBoxViewInitialized', view => { ... });
-         */).Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
+         */).Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
         _events: any;
         _listeners: {};
-        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
         _listeningTo: {};
         _listenId: any;
-        off(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context?: any): any;
-        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
-        once(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
-        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+        off(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
         trigger(name: string, ...args: any[]): any;
         readonly renderOptions: import("lit-html").RenderOptions;
         __childPart: any;

+ 1 - 1
src/types/plugins/minimize/toggle.d.ts

@@ -4,5 +4,5 @@ declare class MinimizedChatsToggle extends Model {
         collapsed: boolean;
     };
 }
-import { Model } from '@converse/skeletor';
+import { Model } from '@converse/headless';
 //# sourceMappingURL=toggle.d.ts.map

+ 1 - 1
src/types/plugins/modal/modal.d.ts

@@ -56,6 +56,6 @@ declare class BaseModal extends CustomElement {
     #private;
 }
 import { CustomElement } from 'shared/components/element.js';
-import { Model } from '@converse/skeletor';
+import { Model } from '@converse/headless';
 import Modal from 'bootstrap/js/src/modal.js';
 //# sourceMappingURL=modal.d.ts.map

+ 6 - 6
src/types/plugins/muc-views/muc.d.ts

@@ -20,16 +20,16 @@ declare const MUCView_base: {
         initialize(): any;
         connectedCallback(): any;
         disconnectedCallback(): void;
-        on(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
+        on(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
         _events: any;
         _listeners: {};
-        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+        listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
         _listeningTo: {};
         _listenId: any;
-        off(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context?: any): any;
-        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
-        once(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
-        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+        off(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context?: any): any;
+        stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
+        once(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
+        listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
         trigger(name: string, ...args: any[]): any;
         readonly renderOptions: import("lit-html").RenderOptions;
         __childPart: any;

+ 1 - 1
src/types/plugins/muc-views/sidebar-occupant.d.ts

@@ -23,6 +23,6 @@ export default class MUCOccupantListItem extends ObservableElement {
     onOccupantClicked(ev: MouseEvent, occupant: import("@converse/headless/types/plugins/muc/occupant.js").default): void;
 }
 import { ObservableElement } from "shared/components/observable.js";
-import { Model } from "@converse/skeletor";
+import { Model } from "@converse/headless";
 import { MUC } from "@converse/headless";
 //# sourceMappingURL=sidebar-occupant.d.ts.map

+ 1 - 1
src/types/plugins/omemo/device.d.ts

@@ -23,5 +23,5 @@ declare class Device extends Model {
      */
     getBundle(): Promise<import("./types").Bundle>;
 }
-import { Model } from "@converse/skeletor";
+import { Model } from "@converse/headless";
 //# sourceMappingURL=device.d.ts.map

+ 1 - 1
src/types/plugins/omemo/devicelist.d.ts

@@ -39,5 +39,5 @@ declare class DeviceList extends Model {
      */
     removeOwnDevices(device_ids: string[]): Promise<any>;
 }
-import { Model } from "@converse/skeletor";
+import { Model } from "@converse/headless";
 //# sourceMappingURL=devicelist.d.ts.map

+ 1 - 1
src/types/plugins/omemo/devicelists.d.ts

@@ -3,6 +3,6 @@ declare class DeviceLists extends Collection {
     constructor();
     model: typeof DeviceList;
 }
-import { Collection } from "@converse/skeletor";
+import { Collection } from "@converse/headless";
 import DeviceList from "./devicelist.js";
 //# sourceMappingURL=devicelists.d.ts.map

+ 1 - 1
src/types/plugins/omemo/devices.d.ts

@@ -3,6 +3,6 @@ declare class Devices extends Collection {
     constructor();
     model: typeof Device;
 }
-import { Collection } from "@converse/skeletor";
+import { Collection } from "@converse/headless";
 import Device from "./device.js";
 //# sourceMappingURL=devices.d.ts.map

+ 1 - 1
src/types/plugins/omemo/store.d.ts

@@ -55,5 +55,5 @@ declare class OMEMOStore extends Model {
     fetchSession(): Promise<any>;
     _setup_promise: Promise<any>;
 }
-import { Model } from "@converse/skeletor";
+import { Model } from "@converse/headless";
 //# sourceMappingURL=store.d.ts.map

+ 4 - 4
src/types/plugins/omemo/utils.d.ts

@@ -103,15 +103,15 @@ export function initOMEMO(reconnecting: boolean): Promise<void>;
 export function getOMEMOToolbarButton(toolbar_el: import("shared/chat/toolbar").ChatToolbar, buttons: Array<import("lit").TemplateResult>): import("lit-html").TemplateResult<1 | 2 | 3>[];
 /**
  * @param {MUC|ChatBox} chat
- * @param {{ message: BaseMessage, stanza: import('strophe.js').Builder }} data
- * @return {Promise<{ message: BaseMessage, stanza: import('strophe.js').Builder }>}
+ * @param {{ message: BaseMessage, stanza: import('@converse/headless').Builder }} data
+ * @return {Promise<{ message: BaseMessage, stanza: import('@converse/headless').Builder }>}
  */
 export function createOMEMOMessageStanza(chat: MUC | ChatBox, data: {
     message: BaseMessage;
-    stanza: import("strophe.js").Builder;
+    stanza: import("@converse/headless").Builder;
 }): Promise<{
     message: BaseMessage;
-    stanza: import("strophe.js").Builder;
+    stanza: import("@converse/headless").Builder;
 }>;
 export namespace omemo {
     export { decryptMessage };

+ 1 - 1
src/types/plugins/profile/modals/profile.d.ts

@@ -56,5 +56,5 @@ export default class ProfileModal extends BaseModal {
     logOut(ev: MouseEvent): Promise<void>;
 }
 import BaseModal from 'plugins/modal/modal.js';
-import { Model } from '@converse/skeletor';
+import { Model } from '@converse/headless';
 //# sourceMappingURL=profile.d.ts.map

+ 1 - 1
src/types/plugins/roomslist/model.d.ts

@@ -11,5 +11,5 @@ declare class RoomsListModel extends Model {
      */
     setDomain(jid: string): void;
 }
-import { Model } from '@converse/skeletor';
+import { Model } from "@converse/headless";
 //# sourceMappingURL=model.d.ts.map

+ 4 - 5
src/types/plugins/roomslist/view.d.ts

@@ -2,10 +2,10 @@ export class RoomsList extends CustomElement {
     initialize(): void;
     model: RoomsListModel;
     render(): import("lit-html").TemplateResult<1>;
-    /** @param {Model} model */
-    renderIfChatRoom(model: Model): void;
-    /** @param {Model} model */
-    renderIfRelevantChange(model: Model): void;
+    /** @param {import('@converse/headless').Model} model */
+    renderIfChatRoom(model: import("@converse/headless").Model): void;
+    /** @param {import('@converse/headless').Model} model */
+    renderIfRelevantChange(model: import("@converse/headless").Model): void;
     /** @returns {import('@converse/headless').MUC[]} */
     getRoomsToShow(): import("@converse/headless").MUC[];
     /** @param {Event} ev */
@@ -20,7 +20,6 @@ export class RoomsList extends CustomElement {
      */
     toggleDomainList(ev: Event, domain: string): void;
 }
-export type Model = import("@converse/skeletor").Model;
 import { CustomElement } from 'shared/components/element.js';
 import RoomsListModel from './model.js';
 //# sourceMappingURL=view.d.ts.map

+ 1 - 1
src/types/plugins/rosterview/modals/blocklist.d.ts

@@ -4,7 +4,7 @@ export default class BlockListModal extends BaseModal {
             type: StringConstructor;
         };
         model: {
-            type: typeof import("@converse/skeletor").Model;
+            type: typeof import("@converse/headless").Model;
         };
     };
     constructor();

+ 1 - 1
src/types/plugins/rosterview/rosterview.d.ts

@@ -22,5 +22,5 @@ export default class RosterView extends CustomElement {
     toggleFilter(ev?: MouseEvent): void;
 }
 import { CustomElement } from 'shared/components/element.js';
-import { Model } from '@converse/skeletor';
+import { Model } from "@converse/headless";
 //# sourceMappingURL=rosterview.d.ts.map

+ 4 - 5
src/types/plugins/rosterview/utils.d.ts

@@ -36,15 +36,15 @@ export function isContactFiltered(contact: RosterContact | Profile, groupname: s
 /**
  * @param {RosterContact} contact
  * @param {string} groupname
- * @param {Model} model
+ * @param {import('@converse/headless').Model} model
  * @returns {boolean}
  */
-export function shouldShowContact(contact: RosterContact, groupname: string, model: Model): boolean;
+export function shouldShowContact(contact: RosterContact, groupname: string, model: import("@converse/headless").Model): boolean;
 /**
  * @param {string} group
- * @param {Model} model
+ * @param {import('@converse/headless').Model} model
  */
-export function shouldShowGroup(group: string, model: Model): boolean;
+export function shouldShowGroup(group: string, model: import("@converse/headless").Model): boolean;
 /**
  * Populates a contacts map with the given contact, categorizing it into appropriate groups.
  * @param {import('./types').ContactsMap} contacts_map
@@ -69,7 +69,6 @@ export function getJIDsAutoCompleteList(): any[];
  * @param {string} query
  */
 export function getNamesAutoCompleteList(query: string): Promise<any[]>;
-export type Model = import("@converse/skeletor").Model;
 export type RosterContact = import("@converse/headless").RosterContact;
 export type RosterContacts = import("@converse/headless").RosterContacts;
 import { Profile } from '@converse/headless';

+ 6 - 6
src/types/shared/autocomplete/autocomplete.d.ts

@@ -1,14 +1,14 @@
 declare const AutoComplete_base: (new (...args: any[]) => {
-    on(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
+    on(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
     _events: any;
     _listeners: {};
-    listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+    listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
     _listeningTo: {};
     _listenId: any;
-    off(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context?: any): any;
-    stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
-    once(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
-    listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+    off(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context?: any): any;
+    stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
+    once(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
+    listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
     trigger(name: string, ...args: any[]): any;
 }) & ObjectConstructor;
 export class AutoComplete extends AutoComplete_base {

+ 2 - 3
src/types/shared/chat/utils.d.ts

@@ -4,9 +4,9 @@ export function isMobileViewport(): boolean;
  */
 export function getChatStyle(model: import("@converse/headless/types/shared/chatbox").default): string;
 /**
- * @param {Model} model
+ * @param {import('@converse/headless').Model} model
  */
-export function getUnreadMsgsDisplay(model: Model): any;
+export function getUnreadMsgsDisplay(model: import("@converse/headless").Model): any;
 /**
  * @param {Promise<HeadingButtonAttributes>|HeadingButtonAttributes} promise_or_data
  * @returns {Promise<TemplateResult|''>}
@@ -90,6 +90,5 @@ export type EmojiMarkupOptions = {
 export type HeadingButtonAttributes = import("plugins/chatview/types").HeadingButtonAttributes;
 export type Message = import("@converse/headless").Message;
 export type MUCMessage = import("@converse/headless").MUCMessage;
-export type Model = import("@converse/skeletor").Model;
 export type TemplateResult = import("lit").TemplateResult;
 //# sourceMappingURL=utils.d.ts.map

+ 6 - 6
src/types/shared/components/element.d.ts

@@ -1,14 +1,14 @@
 declare const CustomElement_base: (new (...args: any[]) => {
-    on(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
+    on(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
     _events: any;
     _listeners: {};
-    listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+    listenTo(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
     _listeningTo: {};
     _listenId: any;
-    off(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context?: any): any;
-    stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
-    once(name: string, callback: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any, context: any): any;
-    listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/skeletor").Model, collection: import("@converse/skeletor").Collection, options?: Record<string, any>) => any): any;
+    off(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context?: any): any;
+    stopListening(obj?: any, name?: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
+    once(name: string, callback: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any, context: any): any;
+    listenToOnce(obj: any, name: string, callback?: (event: any, model: import("@converse/headless").Model, collection: import("@converse/headless").Collection, options?: Record<string, any>) => any): any;
     trigger(name: string, ...args: any[]): any;
 }) & typeof LitElement;
 export class CustomElement extends CustomElement_base {

+ 14 - 12
src/types/utils/index.d.ts

@@ -28,12 +28,12 @@ declare const _default: {
     isString(s: any): boolean;
     getDefaultStorageType(): import("headless/types/utils/types.js").StorageType;
     createStore(id: string, type: import("headless/types/utils/types.js").StorageType): any;
-    initStorage(model: import("@converse/skeletor").Model | import("@converse/skeletor").Collection, id: string, type?: import("headless/types/utils/types.js").StorageType): void;
+    initStorage(model: import("@converse/headless").Model | import("@converse/headless").Collection, id: string, type?: import("headless/types/utils/types.js").StorageType): void;
     isErrorStanza(stanza: Element): boolean;
     isForbiddenError(stanza: Element): boolean;
     isServiceUnavailableError(stanza: Element): boolean;
     getAttributes(stanza: Element): object;
-    toStanza: typeof import("strophe.js").Stanza.toElement;
+    toStanza: typeof import("@converse/headless").Stanza.toElement;
     isUniView(): boolean;
     isTestEnv(): boolean;
     getUnloadEvent(): "pagehide" | "beforeunload" | "unload";
@@ -49,7 +49,7 @@ declare const _default: {
     isFunction(val: unknown): boolean;
     isUndefined(x: unknown): boolean;
     isErrorObject(o: unknown): boolean;
-    isPersistableModel(model: import("@converse/skeletor").Model): boolean;
+    isPersistableModel(model: import("@converse/headless").Model): boolean;
     isValidJID(jid?: string | null): boolean;
     isValidMUCJID(jid: string): boolean;
     isSameBareJID(jid1: string, jid2: string): boolean;
@@ -65,10 +65,11 @@ declare const _default: {
     registerGlobalEventHandlers(_converse: ConversePrivateGlobal): void;
     cleanup(_converse: ConversePrivateGlobal): Promise<void>;
     attemptNonPreboundSession(credentials?: import("headless/types/utils/types.js").Credentials, automatic?: boolean): Promise<void>;
-    savedLoginInfo(jid: string): Promise<import("@converse/skeletor").Model>;
-    safeSave(model: import("@converse/skeletor").Model, attributes: any, options: any): void;
+    savedLoginInfo(jid: string): Promise<import("@converse/headless").Model>;
+    safeSave(model: import("@converse/headless").Model, attributes: any, options: any): void;
     isElement(el: unknown): boolean;
-    isTagEqual(stanza: Element | typeof import("strophe.js").Builder, name: string): boolean;
+    isEqualNode(actual: Element, expected: Element): boolean;
+    isTagEqual(stanza: Element | typeof import("@converse/headless").Builder, name: string): boolean;
     stringToElement(s: string): Element;
     queryChildren(el: HTMLElement, selector: string): ChildNode[];
     siblingIndex(el: Element): number;
@@ -139,12 +140,12 @@ declare const _default: {
         isString(s: any): boolean;
         getDefaultStorageType(): import("headless/types/utils/types.js").StorageType;
         createStore(id: string, type: import("headless/types/utils/types.js").StorageType): any;
-        initStorage(model: import("@converse/skeletor").Model | import("@converse/skeletor").Collection, id: string, type?: import("headless/types/utils/types.js").StorageType): void;
+        initStorage(model: import("@converse/headless").Model | import("@converse/headless").Collection, id: string, type?: import("headless/types/utils/types.js").StorageType): void;
         isErrorStanza(stanza: Element): boolean;
         isForbiddenError(stanza: Element): boolean;
         isServiceUnavailableError(stanza: Element): boolean;
         getAttributes(stanza: Element): object;
-        toStanza: typeof import("strophe.js").Stanza.toElement;
+        toStanza: typeof import("@converse/headless").Stanza.toElement;
         isUniView(): boolean;
         isTestEnv(): boolean;
         getUnloadEvent(): "pagehide" | "beforeunload" | "unload";
@@ -160,7 +161,7 @@ declare const _default: {
         isFunction(val: unknown): boolean;
         isUndefined(x: unknown): boolean;
         isErrorObject(o: unknown): boolean;
-        isPersistableModel(model: import("@converse/skeletor").Model): boolean;
+        isPersistableModel(model: import("@converse/headless").Model): boolean;
         isValidJID(jid?: string | null): boolean;
         isValidMUCJID(jid: string): boolean;
         isSameBareJID(jid1: string, jid2: string): boolean;
@@ -176,10 +177,11 @@ declare const _default: {
         registerGlobalEventHandlers(_converse: ConversePrivateGlobal): void;
         cleanup(_converse: ConversePrivateGlobal): Promise<void>;
         attemptNonPreboundSession(credentials?: import("headless/types/utils/types.js").Credentials, automatic?: boolean): Promise<void>;
-        savedLoginInfo(jid: string): Promise<import("@converse/skeletor").Model>;
-        safeSave(model: import("@converse/skeletor").Model, attributes: any, options: any): void;
+        savedLoginInfo(jid: string): Promise<import("@converse/headless").Model>;
+        safeSave(model: import("@converse/headless").Model, attributes: any, options: any): void;
         isElement(el: unknown): boolean;
-        isTagEqual(stanza: Element | typeof import("strophe.js").Builder, name: string): boolean;
+        isEqualNode(actual: Element, expected: Element): boolean;
+        isTagEqual(stanza: Element | typeof import("@converse/headless").Builder, name: string): boolean;
         stringToElement(s: string): Element;
         queryChildren(el: HTMLElement, selector: string): ChildNode[];
         siblingIndex(el: Element): number;

+ 1 - 68
src/utils/html.js

@@ -5,7 +5,6 @@
  * @typedef {import('lit').TemplateResult} TemplateResult
  */
 import { render } from 'lit';
-import { Builder, Stanza } from 'strophe.js';
 import { api, converse, log, u } from '@converse/headless';
 import tplDateInput from 'templates/form_date.js';
 import tplFormCaptcha from '../templates/form_captcha.js';
@@ -18,35 +17,11 @@ import tplFormUrl from '../templates/form_url.js';
 import tplFormUsername from '../templates/form_username.js';
 import tplHyperlink from 'templates/hyperlink.js';
 
-const { sizzle, Strophe, dayjs } = converse.env;
+const { sizzle, dayjs } = converse.env;
 const { isValidURL } = u;
 
 const APPROVED_URL_PROTOCOLS = ['http:', 'https:', 'xmpp:', 'mailto:'];
 
-const EMPTY_TEXT_REGEX = /\s*\n\s*/;
-
-/**
- * @param {Element|Builder|Stanza} el
- */
-function stripEmptyTextNodes (el) {
-    if (el instanceof Builder || el instanceof Stanza) {
-        el = el.tree();
-    }
-
-    let n;
-    const text_nodes = [];
-    const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, (node) => {
-        if (node.parentElement.nodeName.toLowerCase() === 'body') {
-            return NodeFilter.FILTER_REJECT;
-        }
-        return NodeFilter.FILTER_ACCEPT;
-    });
-    while (n = walker.nextNode()) text_nodes.push(n);
-    text_nodes.forEach((n) => EMPTY_TEXT_REGEX.test(/** @type {Text} */(n).data) && n.parentElement.removeChild(n))
-
-    return el;
-}
-
 /**
  * @param {string} name
  * @param {{ new_password: string }} options
@@ -58,47 +33,6 @@ function getAutoCompleteProperty (name, options) {
     }[name];
 }
 
-/**
- * Given two XML or HTML elements, determine if they're equal
- * @param {Element} actual
- * @param {Element} expected
- * @returns {Boolean}
- */
-function isEqualNode (actual, expected) {
-    if (!u.isElement(actual)) throw new Error('Element being compared must be an Element!');
-
-    actual = stripEmptyTextNodes(actual);
-    expected = stripEmptyTextNodes(expected);
-
-    let isEqual = actual.isEqualNode(expected);
-
-    if (!isEqual) {
-        // XXX: This is a hack.
-        // When creating two XML elements, one via DOMParser, and one via
-        // createElementNS (or createElement), then "isEqualNode" doesn't match.
-        //
-        // For example, in the following code `isEqual` is false:
-        // ------------------------------------------------------
-        // const a = document.createElementNS('foo', 'div');
-        // a.setAttribute('xmlns', 'foo');
-        //
-        // const b = (new DOMParser()).parseFromString('<div xmlns="foo"></div>', 'text/xml').firstElementChild;
-        // const isEqual = a.isEqualNode(div); //  false
-        //
-        // The workaround here is to serialize both elements to string and then use
-        // DOMParser again for both (via xmlHtmlNode).
-        //
-        // This is not efficient, but currently this is only being used in tests.
-        //
-        const { xmlHtmlNode } = Strophe;
-        const actual_string = Strophe.serialize(actual);
-        const expected_string = Strophe.serialize(expected);
-        isEqual =
-            actual_string === expected_string || xmlHtmlNode(actual_string).isEqualNode(xmlHtmlNode(expected_string));
-    }
-    return isEqual;
-}
-
 /**
  * Given an HTMLElement representing a form field, return it's name and value.
  * @param {HTMLInputElement|HTMLSelectElement} field
@@ -544,7 +478,6 @@ Object.assign(u, {
     getRootElement,
     hasClass,
     hideElement,
-    isEqualNode,
     isInDOM,
     isVisible,
     nextUntil,