Merge branch 'develop' into travis/remove-skinning
This commit is contained in:
commit
97efdf7094
54 changed files with 1559 additions and 431 deletions
|
@ -14,7 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room, Beacon, BeaconEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
Room,
|
||||
Beacon,
|
||||
BeaconEvent,
|
||||
MatrixEvent,
|
||||
RoomStateEvent,
|
||||
RoomMember,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
|
||||
import { M_BEACON, M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
@ -23,6 +30,7 @@ import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../src/stores/OwnBeaconS
|
|||
import {
|
||||
advanceDateAndTime,
|
||||
flushPromisesWithFakeTimers,
|
||||
makeMembershipEvent,
|
||||
resetAsyncStoreWithClient,
|
||||
setupAsyncStoreWithClient,
|
||||
} from "../test-utils";
|
||||
|
@ -158,7 +166,7 @@ describe('OwnBeaconStore', () => {
|
|||
geolocation = mockGeolocation();
|
||||
mockClient.getVisibleRooms.mockReturnValue([]);
|
||||
mockClient.unstable_setLiveBeacon.mockClear().mockResolvedValue({ event_id: '1' });
|
||||
mockClient.sendEvent.mockClear().mockResolvedValue({ event_id: '1' });
|
||||
mockClient.sendEvent.mockReset().mockResolvedValue({ event_id: '1' });
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||
jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore();
|
||||
jest.spyOn(logger, 'error').mockRestore();
|
||||
|
@ -243,6 +251,7 @@ describe('OwnBeaconStore', () => {
|
|||
|
||||
expect(removeSpy.mock.calls[0]).toEqual(expect.arrayContaining([BeaconEvent.LivenessChange]));
|
||||
expect(removeSpy.mock.calls[1]).toEqual(expect.arrayContaining([BeaconEvent.New]));
|
||||
expect(removeSpy.mock.calls[2]).toEqual(expect.arrayContaining([RoomStateEvent.Members]));
|
||||
});
|
||||
|
||||
it('destroys beacons', async () => {
|
||||
|
@ -509,6 +518,112 @@ describe('OwnBeaconStore', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('on room membership changes', () => {
|
||||
it('ignores events for rooms without beacons', async () => {
|
||||
const membershipEvent = makeMembershipEvent(room2Id, aliceId);
|
||||
// no beacons for room2
|
||||
const [, room2] = makeRoomsWithStateEvents([
|
||||
alicesRoom1BeaconInfo,
|
||||
]);
|
||||
const store = await makeOwnBeaconStore();
|
||||
const emitSpy = jest.spyOn(store, 'emit');
|
||||
const oldLiveBeaconIds = store.getLiveBeaconIds();
|
||||
|
||||
mockClient.emit(
|
||||
RoomStateEvent.Members,
|
||||
membershipEvent,
|
||||
room2.currentState,
|
||||
new RoomMember(room2Id, aliceId),
|
||||
);
|
||||
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
// strictly equal
|
||||
expect(store.getLiveBeaconIds()).toBe(oldLiveBeaconIds);
|
||||
});
|
||||
|
||||
it('ignores events for membership changes that are not current user', async () => {
|
||||
// bob joins room1
|
||||
const membershipEvent = makeMembershipEvent(room1Id, bobId);
|
||||
const member = new RoomMember(room1Id, bobId);
|
||||
member.setMembershipEvent(membershipEvent);
|
||||
|
||||
const [room1] = makeRoomsWithStateEvents([
|
||||
alicesRoom1BeaconInfo,
|
||||
]);
|
||||
const store = await makeOwnBeaconStore();
|
||||
const emitSpy = jest.spyOn(store, 'emit');
|
||||
const oldLiveBeaconIds = store.getLiveBeaconIds();
|
||||
|
||||
mockClient.emit(
|
||||
RoomStateEvent.Members,
|
||||
membershipEvent,
|
||||
room1.currentState,
|
||||
member,
|
||||
);
|
||||
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
// strictly equal
|
||||
expect(store.getLiveBeaconIds()).toBe(oldLiveBeaconIds);
|
||||
});
|
||||
|
||||
it('ignores events for membership changes that are not leave/ban', async () => {
|
||||
// alice joins room1
|
||||
const membershipEvent = makeMembershipEvent(room1Id, aliceId);
|
||||
const member = new RoomMember(room1Id, aliceId);
|
||||
member.setMembershipEvent(membershipEvent);
|
||||
|
||||
const [room1] = makeRoomsWithStateEvents([
|
||||
alicesRoom1BeaconInfo,
|
||||
alicesRoom2BeaconInfo,
|
||||
]);
|
||||
const store = await makeOwnBeaconStore();
|
||||
const emitSpy = jest.spyOn(store, 'emit');
|
||||
const oldLiveBeaconIds = store.getLiveBeaconIds();
|
||||
|
||||
mockClient.emit(
|
||||
RoomStateEvent.Members,
|
||||
membershipEvent,
|
||||
room1.currentState,
|
||||
member,
|
||||
);
|
||||
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
// strictly equal
|
||||
expect(store.getLiveBeaconIds()).toBe(oldLiveBeaconIds);
|
||||
});
|
||||
|
||||
it('destroys and removes beacons when current user leaves room', async () => {
|
||||
// alice leaves room1
|
||||
const membershipEvent = makeMembershipEvent(room1Id, aliceId, 'leave');
|
||||
const member = new RoomMember(room1Id, aliceId);
|
||||
member.setMembershipEvent(membershipEvent);
|
||||
|
||||
const [room1] = makeRoomsWithStateEvents([
|
||||
alicesRoom1BeaconInfo,
|
||||
alicesRoom2BeaconInfo,
|
||||
]);
|
||||
const store = await makeOwnBeaconStore();
|
||||
const room1BeaconInstance = store.beacons.get(alicesRoom1BeaconInfo.getType());
|
||||
const beaconDestroySpy = jest.spyOn(room1BeaconInstance, 'destroy');
|
||||
const emitSpy = jest.spyOn(store, 'emit');
|
||||
|
||||
mockClient.emit(
|
||||
RoomStateEvent.Members,
|
||||
membershipEvent,
|
||||
room1.currentState,
|
||||
member,
|
||||
);
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith(
|
||||
OwnBeaconStoreEvent.LivenessChange,
|
||||
// other rooms beacons still live
|
||||
[alicesRoom2BeaconInfo.getType()],
|
||||
);
|
||||
expect(beaconDestroySpy).toHaveBeenCalledTimes(1);
|
||||
expect(store.getLiveBeaconIds(room1Id)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopBeacon()', () => {
|
||||
beforeEach(() => {
|
||||
makeRoomsWithStateEvents([
|
||||
|
@ -581,7 +696,7 @@ describe('OwnBeaconStore', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('sending positions', () => {
|
||||
describe('publishing positions', () => {
|
||||
it('stops watching position when user has no more live beacons', async () => {
|
||||
// geolocation is only going to emit 1 position
|
||||
geolocation.watchPosition.mockImplementation(
|
||||
|
@ -710,6 +825,141 @@ describe('OwnBeaconStore', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when publishing position fails', () => {
|
||||
beforeEach(() => {
|
||||
geolocation.watchPosition.mockImplementation(
|
||||
watchPositionMockImplementation([0, 1000, 3000, 3000, 3000]),
|
||||
);
|
||||
|
||||
// eat expected console error logs
|
||||
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
||||
});
|
||||
|
||||
// we need to advance time and then flush promises
|
||||
// individually for each call to sendEvent
|
||||
// otherwise the sendEvent doesn't reject/resolve and update state
|
||||
// before the next call
|
||||
// advance and flush every 1000ms
|
||||
// until given ms is 'elapsed'
|
||||
const advanceAndFlushPromises = async (timeMs: number) => {
|
||||
while (timeMs > 0) {
|
||||
jest.advanceTimersByTime(1000);
|
||||
await flushPromisesWithFakeTimers();
|
||||
timeMs -= 1000;
|
||||
}
|
||||
};
|
||||
|
||||
it('continues publishing positions after one publish error', async () => {
|
||||
// fail to send first event, then succeed
|
||||
mockClient.sendEvent.mockRejectedValueOnce(new Error('oups')).mockResolvedValue({ event_id: '1' });
|
||||
makeRoomsWithStateEvents([
|
||||
alicesRoom1BeaconInfo,
|
||||
]);
|
||||
const store = await makeOwnBeaconStore();
|
||||
// wait for store to settle
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
await advanceAndFlushPromises(50000);
|
||||
|
||||
// called for each position from watchPosition
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledTimes(5);
|
||||
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(false);
|
||||
expect(store.hasWireErrors()).toBe(false);
|
||||
});
|
||||
|
||||
it('continues publishing positions when a beacon fails intermittently', async () => {
|
||||
// every second event rejects
|
||||
// meaning this beacon has more errors than the threshold
|
||||
// but they are not consecutive
|
||||
mockClient.sendEvent
|
||||
.mockRejectedValueOnce(new Error('oups'))
|
||||
.mockResolvedValueOnce({ event_id: '1' })
|
||||
.mockRejectedValueOnce(new Error('oups'))
|
||||
.mockResolvedValueOnce({ event_id: '1' })
|
||||
.mockRejectedValueOnce(new Error('oups'));
|
||||
|
||||
makeRoomsWithStateEvents([
|
||||
alicesRoom1BeaconInfo,
|
||||
]);
|
||||
const store = await makeOwnBeaconStore();
|
||||
const emitSpy = jest.spyOn(store, 'emit');
|
||||
// wait for store to settle
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
await advanceAndFlushPromises(50000);
|
||||
|
||||
// called for each position from watchPosition
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledTimes(5);
|
||||
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(false);
|
||||
expect(store.hasWireErrors()).toBe(false);
|
||||
expect(emitSpy).not.toHaveBeenCalledWith(
|
||||
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(),
|
||||
);
|
||||
});
|
||||
|
||||
it('stops publishing positions when a beacon fails consistently', async () => {
|
||||
// always fails to send events
|
||||
mockClient.sendEvent.mockRejectedValue(new Error('oups'));
|
||||
makeRoomsWithStateEvents([
|
||||
alicesRoom1BeaconInfo,
|
||||
]);
|
||||
const store = await makeOwnBeaconStore();
|
||||
const emitSpy = jest.spyOn(store, 'emit');
|
||||
// wait for store to settle
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
// 5 positions from watchPosition in this period
|
||||
await advanceAndFlushPromises(50000);
|
||||
|
||||
// only two allowed failures
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
|
||||
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(true);
|
||||
expect(store.hasWireErrors()).toBe(true);
|
||||
expect(emitSpy).toHaveBeenCalledWith(
|
||||
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(),
|
||||
);
|
||||
});
|
||||
|
||||
it('restarts publishing a beacon after resetting wire error', async () => {
|
||||
// always fails to send events
|
||||
mockClient.sendEvent.mockRejectedValue(new Error('oups'));
|
||||
makeRoomsWithStateEvents([
|
||||
alicesRoom1BeaconInfo,
|
||||
]);
|
||||
const store = await makeOwnBeaconStore();
|
||||
const emitSpy = jest.spyOn(store, 'emit');
|
||||
// wait for store to settle
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
// 3 positions from watchPosition in this period
|
||||
await advanceAndFlushPromises(4000);
|
||||
|
||||
// only two allowed failures
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
|
||||
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(true);
|
||||
expect(store.hasWireErrors()).toBe(true);
|
||||
expect(store.hasWireErrors(room1Id)).toBe(true);
|
||||
expect(emitSpy).toHaveBeenCalledWith(
|
||||
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(),
|
||||
);
|
||||
|
||||
// reset emitSpy mock counts to asser on wireError again
|
||||
emitSpy.mockClear();
|
||||
store.resetWireError(alicesRoom1BeaconInfo.getType());
|
||||
|
||||
expect(store.beaconHasWireError(alicesRoom1BeaconInfo.getType())).toBe(false);
|
||||
|
||||
// 2 more positions from watchPosition in this period
|
||||
await advanceAndFlushPromises(10000);
|
||||
|
||||
// 2 from before, 2 new ones
|
||||
expect(mockClient.sendEvent).toHaveBeenCalledTimes(4);
|
||||
expect(emitSpy).toHaveBeenCalledWith(
|
||||
OwnBeaconStoreEvent.WireError, alicesRoom1BeaconInfo.getType(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('publishes subsequent positions', async () => {
|
||||
// modern fake timers + debounce + promises are not friends
|
||||
// just testing that positions are published
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue