diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index 2dbf0fe0fe..269f16beb7 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -214,12 +214,11 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_SpaceRoomView_info {
display: inline-block;
- margin: 0;
+ margin: 0 auto 0 0;
}
.mx_FacePile {
display: inline-block;
- margin-left: auto;
margin-right: 12px;
.mx_FacePile_faces {
diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
index 80ad4d6c0e..247df52b4a 100644
--- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
+++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
@@ -148,12 +148,14 @@ limitations under the License.
font-size: $font-15px;
line-height: 30px;
flex-grow: 1;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ margin-right: 12px;
}
- .mx_FormButton {
- min-width: 92px;
- font-weight: normal;
- box-sizing: border-box;
+ .mx_Checkbox {
+ align-items: center;
}
}
}
@@ -192,8 +194,4 @@ limitations under the License.
padding: 0;
}
}
-
- .mx_FormButton {
- padding: 8px 22px;
- }
}
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 1dc342fac5..6b2568d68c 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -130,11 +130,14 @@ export function sanitizedHtmlNode(insaneHtml: string) {
return
;
}
-export function sanitizedHtmlNodeInnerText(insaneHtml: string) {
- const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
- const contentDiv = document.createElement("div");
- contentDiv.innerHTML = saneHtml;
- return contentDiv.innerText;
+export function getHtmlText(insaneHtml: string) {
+ return sanitizeHtml(insaneHtml, {
+ allowedTags: [],
+ allowedAttributes: {},
+ selfClosing: [],
+ allowedSchemes: [],
+ disallowedTagsMode: 'discard',
+ })
}
/**
diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts
index deed7dcf2c..b900afc13f 100644
--- a/src/stores/room-list/previews/MessageEventPreview.ts
+++ b/src/stores/room-list/previews/MessageEventPreview.ts
@@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import ReplyThread from "../../../components/views/elements/ReplyThread";
-import { sanitizedHtmlNodeInnerText } from "../../../HtmlUtils";
+import { getHtmlText } from "../../../HtmlUtils";
export class MessageEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
@@ -55,7 +55,7 @@ export class MessageEventPreview implements IPreview {
}
if (hasHtml) {
- body = sanitizedHtmlNodeInnerText(body);
+ body = getHtmlText(body);
}
if (msgtype === 'm.emote') {
diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts
index 8ab66dfb29..cea377bfe9 100644
--- a/src/utils/arrays.ts
+++ b/src/utils/arrays.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,23 +15,47 @@ limitations under the License.
*/
/**
- * Quickly resample an array to have less data points. This isn't a perfect representation,
- * though this does work best if given a large array to downsample to a much smaller array.
- * @param {number[]} input The input array to downsample.
+ * Quickly resample an array to have less/more data points. If an input which is larger
+ * than the desired size is provided, it will be downsampled. Similarly, if the input
+ * is smaller than the desired size then it will be upsampled.
+ * @param {number[]} input The input array to resample.
* @param {number} points The number of samples to end up with.
- * @returns {number[]} The downsampled array.
+ * @returns {number[]} The resampled array.
*/
export function arrayFastResample(input: number[], points: number): number[] {
- // Heavily inpired by matrix-media-repo (used with permission)
+ if (input.length === points) return input; // short-circuit a complicated call
+
+ // Heavily inspired by matrix-media-repo (used with permission)
// https://github.com/turt2live/matrix-media-repo/blob/abe72c87d2e29/util/util_audio/fastsample.go#L10
- const everyNth = Math.round(input.length / points);
- const samples: number[] = [];
- for (let i = 0; i < input.length; i += everyNth) {
- samples.push(input[i]);
+ let samples: number[] = [];
+ if (input.length > points) {
+ // Danger: this loop can cause out of memory conditions if the input is too small.
+ const everyNth = Math.round(input.length / points);
+ for (let i = 0; i < input.length; i += everyNth) {
+ samples.push(input[i]);
+ }
+ } else {
+ // Smaller inputs mean we have to spread the values over the desired length. We
+ // end up overshooting the target length in doing this, so we'll resample down
+ // before returning. This recursion is risky, but mathematically should not go
+ // further than 1 level deep.
+ const spreadFactor = Math.ceil(points / input.length);
+ for (const val of input) {
+ samples.push(...arraySeed(val, spreadFactor));
+ }
+ samples = arrayFastResample(samples, points);
}
+
+ // Sanity fill, just in case
while (samples.length < points) {
samples.push(input[input.length - 1]);
}
+
+ // Sanity trim, just in case
+ if (samples.length > points) {
+ samples = samples.slice(0, points);
+ }
+
return samples;
}
@@ -178,6 +202,13 @@ export class GroupedArray {
constructor(private val: Map) {
}
+ /**
+ * The value of this group, after all applicable alterations.
+ */
+ public get value(): Map {
+ return this.val;
+ }
+
/**
* Orders the grouping into an array using the provided key order.
* @param keyOrder The key order.
diff --git a/src/utils/enums.ts b/src/utils/enums.ts
index f7f4787896..d3ca318c28 100644
--- a/src/utils/enums.ts
+++ b/src/utils/enums.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,11 +19,23 @@ limitations under the License.
* @param e The enum.
* @returns The enum values.
*/
-export function getEnumValues(e: any): T[] {
+export function getEnumValues(e: any): (string | number)[] {
+ // String-based enums will simply be objects ({Key: "value"}), but number-based
+ // enums will instead map themselves twice: in one direction for {Key: 12} and
+ // the reverse for easy lookup, presumably ({12: Key}). In the reverse mapping,
+ // the key is a string, not a number.
+ //
+ // For this reason, we try to determine what kind of enum we're dealing with.
+
const keys = Object.keys(e);
- return keys
- .filter(k => ['string', 'number'].includes(typeof(e[k])))
- .map(k => e[k]);
+ const values: (string | number)[] = [];
+ for (const key of keys) {
+ const value = e[key];
+ if (Number.isFinite(value) || e[value.toString()] !== Number(key)) {
+ values.push(value);
+ }
+ }
+ return values;
}
/**
diff --git a/src/utils/objects.ts b/src/utils/objects.ts
index e7f4f0f907..2c9361beba 100644
--- a/src/utils/objects.ts
+++ b/src/utils/objects.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -141,3 +141,21 @@ export function objectKeyChanges(a: O, b: O): (keyof O)[] {
export function objectClone(obj: O): O {
return JSON.parse(JSON.stringify(obj));
}
+
+/**
+ * Converts a series of entries to an object.
+ * @param entries The entries to convert.
+ * @returns The converted object.
+ */
+// NOTE: Deprecated once we have Object.fromEntries() support.
+// @ts-ignore - return type is complaining about non-string keys, but we know better
+export function objectFromEntries(entries: Iterable<[K, V]>): {[k: K]: V} {
+ const obj: {
+ // @ts-ignore - same as return type
+ [k: K]: V} = {};
+ for (const e of entries) {
+ // @ts-ignore - same as return type
+ obj[e[0]] = e[1];
+ }
+ return obj;
+}
diff --git a/test/Singleflight-test.ts b/test/utils/Singleflight-test.ts
similarity index 98%
rename from test/Singleflight-test.ts
rename to test/utils/Singleflight-test.ts
index 4f0c6e0da3..80258701bb 100644
--- a/test/Singleflight-test.ts
+++ b/test/utils/Singleflight-test.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {Singleflight} from "../src/utils/Singleflight";
+import {Singleflight} from "../../src/utils/Singleflight";
describe('Singleflight', () => {
afterEach(() => {
diff --git a/test/utils/arrays-test.ts b/test/utils/arrays-test.ts
new file mode 100644
index 0000000000..ececd274b2
--- /dev/null
+++ b/test/utils/arrays-test.ts
@@ -0,0 +1,294 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {
+ arrayDiff,
+ arrayFastClone,
+ arrayFastResample,
+ arrayHasDiff,
+ arrayHasOrderChange,
+ arrayMerge,
+ arraySeed,
+ arrayUnion,
+ ArrayUtil,
+ GroupedArray,
+} from "../../src/utils/arrays";
+import {objectFromEntries} from "../../src/utils/objects";
+
+function expectSample(i: number, input: number[], expected: number[]) {
+ console.log(`Resample case index: ${i}`); // for debugging test failures
+ const result = arrayFastResample(input, expected.length);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(expected.length);
+ expect(result).toEqual(expected);
+}
+
+describe('arrays', () => {
+ describe('arrayFastResample', () => {
+ it('should downsample', () => {
+ [
+ {input: [1, 2, 3, 4, 5], output: [1, 4]}, // Odd -> Even
+ {input: [1, 2, 3, 4, 5], output: [1, 3, 5]}, // Odd -> Odd
+ {input: [1, 2, 3, 4], output: [1, 2, 3]}, // Even -> Odd
+ {input: [1, 2, 3, 4], output: [1, 3]}, // Even -> Even
+ ].forEach((c, i) => expectSample(i, c.input, c.output));
+ });
+
+ it('should upsample', () => {
+ [
+ {input: [1, 2, 3], output: [1, 1, 2, 2, 3, 3]}, // Odd -> Even
+ {input: [1, 2, 3], output: [1, 1, 2, 2, 3]}, // Odd -> Odd
+ {input: [1, 2], output: [1, 1, 1, 2, 2]}, // Even -> Odd
+ {input: [1, 2], output: [1, 1, 1, 2, 2, 2]}, // Even -> Even
+ ].forEach((c, i) => expectSample(i, c.input, c.output));
+ });
+
+ it('should maintain sample', () => {
+ [
+ {input: [1, 2, 3], output: [1, 2, 3]}, // Odd
+ {input: [1, 2], output: [1, 2]}, // Even
+ ].forEach((c, i) => expectSample(i, c.input, c.output));
+ });
+ });
+
+ describe('arraySeed', () => {
+ it('should create an array of given length', () => {
+ const val = 1;
+ const output = [val, val, val];
+ const result = arraySeed(val, output.length);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(output.length);
+ expect(result).toEqual(output);
+ });
+ it('should maintain pointers', () => {
+ const val = {}; // this works because `{} !== {}`, which is what toEqual checks
+ const output = [val, val, val];
+ const result = arraySeed(val, output.length);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(output.length);
+ expect(result).toEqual(output);
+ });
+ });
+
+ describe('arrayFastClone', () => {
+ it('should break pointer reference on source array', () => {
+ const val = {}; // we'll test to make sure the values maintain pointers too
+ const input = [val, val, val];
+ const result = arrayFastClone(input);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(input.length);
+ expect(result).toEqual(input); // we want the array contents to match...
+ expect(result).not.toBe(input); // ... but be a different reference
+ });
+ });
+
+ describe('arrayHasOrderChange', () => {
+ it('should flag true on B ordering difference', () => {
+ const a = [1, 2, 3];
+ const b = [3, 2, 1];
+ const result = arrayHasOrderChange(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag false on no ordering difference', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3];
+ const result = arrayHasOrderChange(a, b);
+ expect(result).toBe(false);
+ });
+
+ it('should flag true on A length > B length', () => {
+ const a = [1, 2, 3, 4];
+ const b = [1, 2, 3];
+ const result = arrayHasOrderChange(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag true on A length < B length', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3, 4];
+ const result = arrayHasOrderChange(a, b);
+ expect(result).toBe(true);
+ });
+ });
+
+ describe('arrayHasDiff', () => {
+ it('should flag true on A length > B length', () => {
+ const a = [1, 2, 3, 4];
+ const b = [1, 2, 3];
+ const result = arrayHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag true on A length < B length', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3, 4];
+ const result = arrayHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag true on element differences', () => {
+ const a = [1, 2, 3];
+ const b = [4, 5, 6];
+ const result = arrayHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag false if same but order different', () => {
+ const a = [1, 2, 3];
+ const b = [3, 1, 2];
+ const result = arrayHasDiff(a, b);
+ expect(result).toBe(false);
+ });
+
+ it('should flag false if same', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3];
+ const result = arrayHasDiff(a, b);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('arrayDiff', () => {
+ it('should see added from A->B', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3, 4];
+ const result = arrayDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(0);
+ expect(result.added).toEqual([4]);
+ });
+
+ it('should see removed from A->B', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2];
+ const result = arrayDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(1);
+ expect(result.removed).toEqual([3]);
+ });
+
+ it('should see added and removed in the same set', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 4]; // note diff
+ const result = arrayDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(1);
+ expect(result.added).toEqual([4]);
+ expect(result.removed).toEqual([3]);
+ });
+ });
+
+ describe('arrayUnion', () => {
+ it('should return a union', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 4]; // note diff
+ const result = arrayUnion(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(2);
+ expect(result).toEqual([1, 2]);
+ });
+
+ it('should return an empty array on no matches', () => {
+ const a = [1, 2, 3];
+ const b = [4, 5, 6];
+ const result = arrayUnion(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+ });
+
+ describe('arrayMerge', () => {
+ it('should merge 3 arrays with deduplication', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 4, 5]; // note missing 3
+ const c = [6, 7, 8, 9];
+ const result = arrayMerge(a, b, c);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(9);
+ expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ });
+
+ it('should deduplicate a single array', () => {
+ // dev note: this is technically an edge case, but it is described behaviour if the
+ // function is only provided one function (it'll merge the array against itself)
+ const a = [1, 1, 2, 2, 3, 3];
+ const result = arrayMerge(a);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(3);
+ expect(result).toEqual([1, 2, 3]);
+ });
+ });
+
+ describe('ArrayUtil', () => {
+ it('should maintain the pointer to the given array', () => {
+ const input = [1, 2, 3];
+ const result = new ArrayUtil(input);
+ expect(result.value).toBe(input);
+ });
+
+ it('should group appropriately', () => {
+ const input = [['a', 1], ['b', 2], ['c', 3], ['a', 4], ['a', 5], ['b', 6]];
+ const output = {
+ 'a': [['a', 1], ['a', 4], ['a', 5]],
+ 'b': [['b', 2], ['b', 6]],
+ 'c': [['c', 3]],
+ };
+ const result = new ArrayUtil(input).groupBy(p => p[0]);
+ expect(result).toBeDefined();
+ expect(result.value).toBeDefined();
+
+ const asObject = objectFromEntries(result.value.entries());
+ expect(asObject).toMatchObject(output);
+ });
+ });
+
+ describe('GroupedArray', () => {
+ it('should maintain the pointer to the given map', () => {
+ const input = new Map([
+ ['a', [1, 2, 3]],
+ ['b', [7, 8, 9]],
+ ['c', [4, 5, 6]],
+ ]);
+ const result = new GroupedArray(input);
+ expect(result.value).toBe(input);
+ });
+
+ it('should ordering by the provided key order', () => {
+ const input = new Map([
+ ['a', [1, 2, 3]],
+ ['b', [7, 8, 9]], // note counting diff
+ ['c', [4, 5, 6]],
+ ]);
+ const output = [4, 5, 6, 1, 2, 3, 7, 8, 9];
+ const keyOrder = ['c', 'a', 'b']; // note weird order to cause the `output` to be strange
+ const result = new GroupedArray(input).orderBy(keyOrder);
+ expect(result).toBeDefined();
+ expect(result.value).toBeDefined();
+ expect(result.value).toEqual(output);
+ });
+ });
+});
+
diff --git a/test/utils/enums-test.ts b/test/utils/enums-test.ts
new file mode 100644
index 0000000000..423b135f77
--- /dev/null
+++ b/test/utils/enums-test.ts
@@ -0,0 +1,67 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {getEnumValues, isEnumValue} from "../../src/utils/enums";
+
+enum TestStringEnum {
+ First = "__first__",
+ Second = "__second__",
+}
+
+enum TestNumberEnum {
+ FirstKey = 10,
+ SecondKey = 20,
+}
+
+describe('enums', () => {
+ describe('getEnumValues', () => {
+ it('should work on string enums', () => {
+ const result = getEnumValues(TestStringEnum);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(2);
+ expect(result).toEqual(['__first__', '__second__']);
+ });
+
+ it('should work on number enums', () => {
+ const result = getEnumValues(TestNumberEnum);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(2);
+ expect(result).toEqual([10, 20]);
+ });
+ });
+
+ describe('isEnumValue', () => {
+ it('should return true on values in a string enum', () => {
+ const result = isEnumValue(TestStringEnum, '__first__');
+ expect(result).toBe(true);
+ });
+
+ it('should return false on values not in a string enum', () => {
+ const result = isEnumValue(TestStringEnum, 'not a value');
+ expect(result).toBe(false);
+ });
+
+ it('should return true on values in a number enum', () => {
+ const result = isEnumValue(TestNumberEnum, 10);
+ expect(result).toBe(true);
+ });
+
+ it('should return false on values not in a number enum', () => {
+ const result = isEnumValue(TestStringEnum, 99);
+ expect(result).toBe(false);
+ });
+ });
+});
diff --git a/test/utils/iterables-test.ts b/test/utils/iterables-test.ts
new file mode 100644
index 0000000000..9b30b6241c
--- /dev/null
+++ b/test/utils/iterables-test.ts
@@ -0,0 +1,77 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {iterableDiff, iterableUnion} from "../../src/utils/iterables";
+
+describe('iterables', () => {
+ describe('iterableUnion', () => {
+ it('should return a union', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 4]; // note diff
+ const result = iterableUnion(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(2);
+ expect(result).toEqual([1, 2]);
+ });
+
+ it('should return an empty array on no matches', () => {
+ const a = [1, 2, 3];
+ const b = [4, 5, 6];
+ const result = iterableUnion(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+ });
+
+ describe('iterableDiff', () => {
+ it('should see added from A->B', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 3, 4];
+ const result = iterableDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(0);
+ expect(result.added).toEqual([4]);
+ });
+
+ it('should see removed from A->B', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2];
+ const result = iterableDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(1);
+ expect(result.removed).toEqual([3]);
+ });
+
+ it('should see added and removed in the same set', () => {
+ const a = [1, 2, 3];
+ const b = [1, 2, 4]; // note diff
+ const result = iterableDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(1);
+ expect(result.added).toEqual([4]);
+ expect(result.removed).toEqual([3]);
+ });
+ });
+});
diff --git a/test/utils/maps-test.ts b/test/utils/maps-test.ts
new file mode 100644
index 0000000000..8764a8f2cf
--- /dev/null
+++ b/test/utils/maps-test.ts
@@ -0,0 +1,245 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {EnhancedMap, mapDiff, mapKeyChanges} from "../../src/utils/maps";
+
+describe('maps', () => {
+ describe('mapDiff', () => {
+ it('should indicate no differences when the pointers are the same', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const result = mapDiff(a, a);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toHaveLength(0);
+ });
+
+ it('should indicate no differences when there are none', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3]]);
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toHaveLength(0);
+ });
+
+ it('should indicate added properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toHaveLength(0);
+ expect(result.added).toEqual([4]);
+ });
+
+ it('should indicate removed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2]]);
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(1);
+ expect(result.changed).toHaveLength(0);
+ expect(result.removed).toEqual([3]);
+ });
+
+ it('should indicate changed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2], [3, 4]]); // note change
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toHaveLength(1);
+ expect(result.changed).toEqual([3]);
+ });
+
+ it('should indicate changed, added, and removed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 8], [4, 4]]); // note change
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(1);
+ expect(result.changed).toHaveLength(1);
+ expect(result.added).toEqual([4]);
+ expect(result.removed).toEqual([3]);
+ expect(result.changed).toEqual([2]);
+ });
+
+ it('should indicate changes for difference in pointers', () => {
+ const a = new Map([[1, {}]]); // {} always creates a new object
+ const b = new Map([[1, {}]]);
+ const result = mapDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toHaveLength(1);
+ expect(result.changed).toEqual([1]);
+ });
+ });
+
+ describe('mapKeyChanges', () => {
+ it('should indicate no changes for unchanged pointers', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const result = mapKeyChanges(a, a);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+
+ it('should indicate no changes for unchanged maps with different pointers', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3]]);
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+
+ it('should indicate changes for added properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(1);
+ expect(result).toEqual([4]);
+ });
+
+ it('should indicate changes for removed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3]]);
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(1);
+ expect(result).toEqual([4]);
+ });
+
+ it('should indicate changes for changed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
+ const b = new Map([[1, 1], [2, 2], [3, 3], [4, 55]]);
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(1);
+ expect(result).toEqual([4]);
+ });
+
+ it('should indicate changes for properties with different pointers', () => {
+ const a = new Map([[1, {}]]); // {} always creates a new object
+ const b = new Map([[1, {}]]);
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(1);
+ expect(result).toEqual([1]);
+ });
+
+ it('should indicate changes for changed, added, and removed properties', () => {
+ const a = new Map([[1, 1], [2, 2], [3, 3]]);
+ const b = new Map([[1, 1], [2, 8], [4, 4]]); // note change
+ const result = mapKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(3);
+ expect(result).toEqual([3, 4, 2]); // order irrelevant, but the test cares
+ });
+ });
+
+ describe('EnhancedMap', () => {
+ // Most of these tests will make sure it implements the Map class
+
+ it('should be empty by default', () => {
+ const result = new EnhancedMap();
+ expect(result.size).toBe(0);
+ });
+
+ it('should use the provided entries', () => {
+ const obj = {a: 1, b: 2};
+ const result = new EnhancedMap(Object.entries(obj));
+ expect(result.size).toBe(2);
+ expect(result.get('a')).toBe(1);
+ expect(result.get('b')).toBe(2);
+ });
+
+ it('should create keys if they do not exist', () => {
+ const key = 'a';
+ const val = {}; // we'll check pointers
+
+ const result = new EnhancedMap();
+ expect(result.size).toBe(0);
+
+ let get = result.getOrCreate(key, val);
+ expect(get).toBeDefined();
+ expect(get).toBe(val);
+ expect(result.size).toBe(1);
+
+ get = result.getOrCreate(key, 44); // specifically change `val`
+ expect(get).toBeDefined();
+ expect(get).toBe(val);
+ expect(result.size).toBe(1);
+
+ get = result.get(key); // use the base class function
+ expect(get).toBeDefined();
+ expect(get).toBe(val);
+ expect(result.size).toBe(1);
+ });
+
+ it('should proxy remove to delete and return it', () => {
+ const val = {};
+ const result = new EnhancedMap();
+ result.set('a', val);
+
+ expect(result.size).toBe(1);
+
+ const removed = result.remove('a');
+ expect(result.size).toBe(0);
+ expect(removed).toBeDefined();
+ expect(removed).toBe(val);
+ });
+
+ it('should support removing unknown keys', () => {
+ const val = {};
+ const result = new EnhancedMap();
+ result.set('a', val);
+
+ expect(result.size).toBe(1);
+
+ const removed = result.remove('not-a');
+ expect(result.size).toBe(1);
+ expect(removed).not.toBeDefined();
+ });
+ });
+});
diff --git a/test/utils/numbers-test.ts b/test/utils/numbers-test.ts
new file mode 100644
index 0000000000..36e7d4f7e7
--- /dev/null
+++ b/test/utils/numbers-test.ts
@@ -0,0 +1,163 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {clamp, defaultNumber, percentageOf, percentageWithin, sum} from "../../src/utils/numbers";
+
+describe('numbers', () => {
+ describe('defaultNumber', () => {
+ it('should use the default when the input is not a number', () => {
+ const def = 42;
+
+ let result = defaultNumber(null, def);
+ expect(result).toBe(def);
+
+ result = defaultNumber(undefined, def);
+ expect(result).toBe(def);
+
+ result = defaultNumber(Number.NaN, def);
+ expect(result).toBe(def);
+ });
+
+ it('should use the number when it is a number', () => {
+ const input = 24;
+ const def = 42;
+ const result = defaultNumber(input, def);
+ expect(result).toBe(input);
+ });
+ });
+
+ describe('clamp', () => {
+ it('should clamp high numbers', () => {
+ const input = 101;
+ const min = 0;
+ const max = 100;
+ const result = clamp(input, min, max);
+ expect(result).toBe(max);
+ });
+
+ it('should clamp low numbers', () => {
+ const input = -1;
+ const min = 0;
+ const max = 100;
+ const result = clamp(input, min, max);
+ expect(result).toBe(min);
+ });
+
+ it('should not clamp numbers in range', () => {
+ const input = 50;
+ const min = 0;
+ const max = 100;
+ const result = clamp(input, min, max);
+ expect(result).toBe(input);
+ });
+
+ it('should clamp floats', () => {
+ const min = -0.10;
+ const max = +0.10;
+
+ let result = clamp(-1.2, min, max);
+ expect(result).toBe(min);
+
+ result = clamp(1.2, min, max);
+ expect(result).toBe(max);
+
+ result = clamp(0.02, min, max);
+ expect(result).toBe(0.02);
+ });
+ });
+
+ describe('sum', () => {
+ it('should sum', () => { // duh
+ const result = sum(1, 2, 1, 4);
+ expect(result).toBe(8);
+ });
+ });
+
+ describe('percentageWithin', () => {
+ it('should work within 0-100', () => {
+ const result = percentageWithin(0.4, 0, 100);
+ expect(result).toBe(40);
+ });
+
+ it('should work within 0-100 when pct > 1', () => {
+ const result = percentageWithin(1.4, 0, 100);
+ expect(result).toBe(140);
+ });
+
+ it('should work within 0-100 when pct < 0', () => {
+ const result = percentageWithin(-1.4, 0, 100);
+ expect(result).toBe(-140);
+ });
+
+ it('should work with ranges other than 0-100', () => {
+ const result = percentageWithin(0.4, 10, 20);
+ expect(result).toBe(14);
+ });
+
+ it('should work with ranges other than 0-100 when pct > 1', () => {
+ const result = percentageWithin(1.4, 10, 20);
+ expect(result).toBe(24);
+ });
+
+ it('should work with ranges other than 0-100 when pct < 0', () => {
+ const result = percentageWithin(-1.4, 10, 20);
+ expect(result).toBe(-4);
+ });
+
+ it('should work with floats', () => {
+ const result = percentageWithin(0.4, 10.2, 20.4);
+ expect(result).toBe(14.28);
+ });
+ });
+
+ // These are the inverse of percentageWithin
+ describe('percentageOf', () => {
+ it('should work within 0-100', () => {
+ const result = percentageOf(40, 0, 100);
+ expect(result).toBe(0.4);
+ });
+
+ it('should work within 0-100 when val > 100', () => {
+ const result = percentageOf(140, 0, 100);
+ expect(result).toBe(1.40);
+ });
+
+ it('should work within 0-100 when val < 0', () => {
+ const result = percentageOf(-140, 0, 100);
+ expect(result).toBe(-1.40);
+ });
+
+ it('should work with ranges other than 0-100', () => {
+ const result = percentageOf(14, 10, 20);
+ expect(result).toBe(0.4);
+ });
+
+ it('should work with ranges other than 0-100 when val > 100', () => {
+ const result = percentageOf(24, 10, 20);
+ expect(result).toBe(1.4);
+ });
+
+ it('should work with ranges other than 0-100 when val < 0', () => {
+ const result = percentageOf(-4, 10, 20);
+ expect(result).toBe(-1.4);
+ });
+
+ it('should work with floats', () => {
+ const result = percentageOf(14.28, 10.2, 20.4);
+ expect(result).toBe(0.4);
+ });
+ });
+});
diff --git a/test/utils/objects-test.ts b/test/utils/objects-test.ts
new file mode 100644
index 0000000000..b7a80e6761
--- /dev/null
+++ b/test/utils/objects-test.ts
@@ -0,0 +1,262 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {
+ objectClone,
+ objectDiff,
+ objectExcluding,
+ objectFromEntries,
+ objectHasDiff,
+ objectKeyChanges,
+ objectShallowClone,
+ objectWithOnly,
+} from "../../src/utils/objects";
+
+describe('objects', () => {
+ describe('objectExcluding', () => {
+ it('should exclude the given properties', () => {
+ const input = {hello: "world", test: true};
+ const output = {hello: "world"};
+ const props = ["test", "doesnotexist"]; // we also make sure it doesn't explode on missing props
+ const result = objectExcluding(input, props); // any is to test the missing prop
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(output);
+ });
+ });
+
+ describe('objectWithOnly', () => {
+ it('should exclusively use the given properties', () => {
+ const input = {hello: "world", test: true};
+ const output = {hello: "world"};
+ const props = ["hello", "doesnotexist"]; // we also make sure it doesn't explode on missing props
+ const result = objectWithOnly(input, props); // any is to test the missing prop
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(output);
+ });
+ });
+
+ describe('objectShallowClone', () => {
+ it('should create a new object', () => {
+ const input = {test: 1};
+ const result = objectShallowClone(input);
+ expect(result).toBeDefined();
+ expect(result).not.toBe(input);
+ expect(result).toMatchObject(input);
+ });
+
+ it('should only clone the top level properties', () => {
+ const input = {a: 1, b: {c: 2}};
+ const result = objectShallowClone(input);
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(input);
+ expect(result.b).toBe(input.b);
+ });
+
+ it('should support custom clone functions', () => {
+ const input = {a: 1, b: 2};
+ const output = {a: 4, b: 8};
+ const result = objectShallowClone(input, (k, v) => {
+ // XXX: inverted expectation for ease of assertion
+ expect(Object.keys(input)).toContain(k);
+
+ return v * 4;
+ });
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(output);
+ });
+ });
+
+ describe('objectHasDiff', () => {
+ it('should return false for the same pointer', () => {
+ const a = {};
+ const result = objectHasDiff(a, a);
+ expect(result).toBe(false);
+ });
+
+ it('should return true if keys for A > keys for B', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1};
+ const result = objectHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should return true if keys for A < keys for B', () => {
+ const a = {a: 1};
+ const b = {a: 1, b: 2};
+ const result = objectHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should return false if the objects are the same but different pointers', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1, b: 2};
+ const result = objectHasDiff(a, b);
+ expect(result).toBe(false);
+ });
+
+ it('should consider pointers when testing values', () => {
+ const a = {a: {}, b: 2}; // `{}` is shorthand for `new Object()`
+ const b = {a: {}, b: 2};
+ const result = objectHasDiff(a, b);
+ expect(result).toBe(true); // even though the keys are the same, the value pointers vary
+ });
+ });
+
+ describe('objectDiff', () => {
+ it('should return empty sets for the same object', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1, b: 2};
+ const result = objectDiff(a, b);
+ expect(result).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(0);
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ });
+
+ it('should return empty sets for the same object pointer', () => {
+ const a = {a: 1, b: 2};
+ const result = objectDiff(a, a);
+ expect(result).toBeDefined();
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(0);
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ });
+
+ it('should indicate when property changes are made', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 11, b: 2};
+ const result = objectDiff(a, b);
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(1);
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(0);
+ expect(result.changed).toEqual(['a']);
+ });
+
+ it('should indicate when properties are added', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1, b: 2, c: 3};
+ const result = objectDiff(a, b);
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(0);
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(0);
+ expect(result.added).toEqual(['c']);
+ });
+
+ it('should indicate when properties are removed', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1};
+ const result = objectDiff(a, b);
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(0);
+ expect(result.added).toHaveLength(0);
+ expect(result.removed).toHaveLength(1);
+ expect(result.removed).toEqual(['b']);
+ });
+
+ it('should indicate when multiple aspects change', () => {
+ const a = {a: 1, b: 2, c: 3};
+ const b: (typeof a | {d: number}) = {a: 1, b: 22, d: 4};
+ const result = objectDiff(a, b);
+ expect(result.changed).toBeDefined();
+ expect(result.added).toBeDefined();
+ expect(result.removed).toBeDefined();
+ expect(result.changed).toHaveLength(1);
+ expect(result.added).toHaveLength(1);
+ expect(result.removed).toHaveLength(1);
+ expect(result.changed).toEqual(['b']);
+ expect(result.removed).toEqual(['c']);
+ expect(result.added).toEqual(['d']);
+ });
+ });
+
+ describe('objectKeyChanges', () => {
+ it('should return an empty set if no properties changed', () => {
+ const a = {a: 1, b: 2};
+ const b = {a: 1, b: 2};
+ const result = objectKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+
+ it('should return an empty set if no properties changed for the same pointer', () => {
+ const a = {a: 1, b: 2};
+ const result = objectKeyChanges(a, a);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(0);
+ });
+
+ it('should return properties which were changed, added, or removed', () => {
+ const a = {a: 1, b: 2, c: 3};
+ const b: (typeof a | {d: number}) = {a: 1, b: 22, d: 4};
+ const result = objectKeyChanges(a, b);
+ expect(result).toBeDefined();
+ expect(result).toHaveLength(3);
+ expect(result).toEqual(['c', 'd', 'b']); // order isn't important, but the test cares
+ });
+ });
+
+ describe('objectClone', () => {
+ it('should deep clone an object', () => {
+ const a = {
+ hello: "world",
+ test: {
+ another: "property",
+ test: 42,
+ third: {
+ prop: true,
+ },
+ },
+ };
+ const result = objectClone(a);
+ expect(result).toBeDefined();
+ expect(result).not.toBe(a);
+ expect(result).toMatchObject(a);
+ expect(result.test).not.toBe(a.test);
+ expect(result.test.third).not.toBe(a.test.third);
+ });
+ });
+
+ describe('objectFromEntries', () => {
+ it('should create an object from an array of entries', () => {
+ const output = {a: 1, b: 2, c: 3};
+ const result = objectFromEntries(Object.entries(output));
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(output);
+ });
+
+ it('should maintain pointers in values', () => {
+ const output = {a: {}, b: 2, c: 3};
+ const result = objectFromEntries(Object.entries(output));
+ expect(result).toBeDefined();
+ expect(result).toMatchObject(output);
+ expect(result['a']).toBe(output.a);
+ });
+ });
+});
diff --git a/test/utils/sets-test.ts b/test/utils/sets-test.ts
new file mode 100644
index 0000000000..98dc218309
--- /dev/null
+++ b/test/utils/sets-test.ts
@@ -0,0 +1,56 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {setHasDiff} from "../../src/utils/sets";
+
+describe('sets', () => {
+ describe('setHasDiff', () => {
+ it('should flag true on A length > B length', () => {
+ const a = new Set([1, 2, 3, 4]);
+ const b = new Set([1, 2, 3]);
+ const result = setHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag true on A length < B length', () => {
+ const a = new Set([1, 2, 3]);
+ const b = new Set([1, 2, 3, 4]);
+ const result = setHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag true on element differences', () => {
+ const a = new Set([1, 2, 3]);
+ const b = new Set([4, 5, 6]);
+ const result = setHasDiff(a, b);
+ expect(result).toBe(true);
+ });
+
+ it('should flag false if same but order different', () => {
+ const a = new Set([1, 2, 3]);
+ const b = new Set([3, 1, 2]);
+ const result = setHasDiff(a, b);
+ expect(result).toBe(false);
+ });
+
+ it('should flag false if same', () => {
+ const a = new Set([1, 2, 3]);
+ const b = new Set([1, 2, 3]);
+ const result = setHasDiff(a, b);
+ expect(result).toBe(false);
+ });
+ });
+});