| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403 |
- import { vi, expect, describe, beforeEach, afterEach, it } from 'vitest'
- import sinon from 'sinon'
- const modulePath = '../../../app/js/RoomManager.js'
- describe('RoomManager', function () {
- beforeEach(async function (ctx) {
- ctx.project_id = 'project-id-123'
- ctx.doc_id = 'doc-id-456'
- ctx.other_doc_id = 'doc-id-789'
- ctx.client = { namespace: { name: '' }, id: 'first-client' }
- vi.doMock('@overleaf/settings', () => ({
- default: (ctx.settings = {}),
- }))
- vi.doMock('@overleaf/metrics', () => ({
- default: (ctx.metrics = { gauge: sinon.stub() }),
- }))
- ctx.RoomManager = (await import(modulePath)).default
- ctx.RoomManager._clientsInRoom = sinon.stub()
- ctx.RoomManager._clientAlreadyInRoom = sinon.stub()
- ctx.RoomEvents = ctx.RoomManager.eventSource()
- sinon.spy(ctx.RoomEvents, 'emit')
- sinon.spy(ctx.RoomEvents, 'once')
- })
- describe('emitOnCompletion', function () {
- describe('when a subscribe errors', function () {
- afterEach(function (ctx) {
- process.removeListener('unhandledRejection', ctx.onUnhandled)
- })
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.onUnhandled = error => {
- ctx.unhandledError = error
- reject(new Error(`unhandledRejection: ${error.message}`))
- }
- process.on('unhandledRejection', ctx.onUnhandled)
- let rejectSubscribePromise
- const subscribePromise = new Promise(
- // eslint-disable-next-line promise/param-names
- (_, r) => (rejectSubscribePromise = r)
- )
- const promises = [subscribePromise]
- const eventName = 'project-subscribed-123'
- ctx.RoomEvents.once(eventName, () => setTimeout(resolve, 100))
- ctx.RoomManager.emitOnCompletion(promises, eventName)
- setTimeout(() =>
- rejectSubscribePromise(new Error('subscribe failed'))
- )
- })
- })
- it('should keep going', function (ctx) {
- expect(ctx.unhandledError).to.not.exist
- })
- })
- })
- describe('joinProject', function () {
- describe('when the project room is empty', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.project_id)
- .onFirstCall()
- .returns(0)
- ctx.client.join = sinon.stub()
- ctx.callback = sinon.stub()
- ctx.RoomEvents.on('project-active', id => {
- setTimeout(() => {
- ctx.RoomEvents.emit(`project-subscribed-${id}`)
- }, 100)
- })
- ctx.RoomManager.joinProject(ctx.client, ctx.project_id, err => {
- ctx.callback(err)
- resolve()
- })
- })
- })
- it("should emit a 'project-active' event with the id", function (ctx) {
- ctx.RoomEvents.emit
- .calledWithExactly('project-active', ctx.project_id)
- .should.equal(true)
- })
- it("should listen for the 'project-subscribed-id' event", function (ctx) {
- ctx.RoomEvents.once
- .calledWith(`project-subscribed-${ctx.project_id}`)
- .should.equal(true)
- })
- it('should join the room using the id', function (ctx) {
- ctx.client.join.calledWithExactly(ctx.project_id).should.equal(true)
- })
- })
- describe('when there are other clients in the project room', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.project_id)
- .onFirstCall()
- .returns(123)
- .onSecondCall()
- .returns(124)
- ctx.client.join = sinon.stub()
- ctx.RoomManager.joinProject(ctx.client, ctx.project_id, err => {
- if (err) return reject(err)
- resolve()
- })
- })
- })
- it('should join the room using the id', function (ctx) {
- ctx.client.join.called.should.equal(true)
- })
- it('should not emit any events', function (ctx) {
- ctx.RoomEvents.emit.called.should.equal(false)
- })
- })
- })
- describe('joinDoc', function () {
- describe('when the doc room is empty', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .onFirstCall()
- .returns(0)
- ctx.client.join = sinon.stub()
- ctx.callback = sinon.stub()
- ctx.RoomEvents.on('doc-active', id => {
- setTimeout(() => {
- ctx.RoomEvents.emit(`doc-subscribed-${id}`)
- }, 100)
- })
- ctx.RoomManager.joinDoc(ctx.client, ctx.doc_id, err => {
- ctx.callback(err)
- resolve()
- })
- })
- })
- it("should emit a 'doc-active' event with the id", function (ctx) {
- ctx.RoomEvents.emit
- .calledWithExactly('doc-active', ctx.doc_id)
- .should.equal(true)
- })
- it("should listen for the 'doc-subscribed-id' event", function (ctx) {
- ctx.RoomEvents.once
- .calledWith(`doc-subscribed-${ctx.doc_id}`)
- .should.equal(true)
- })
- it('should join the room using the id', function (ctx) {
- ctx.client.join.calledWithExactly(ctx.doc_id).should.equal(true)
- })
- })
- describe('when there are other clients in the doc room', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .onFirstCall()
- .returns(123)
- .onSecondCall()
- .returns(124)
- ctx.client.join = sinon.stub()
- ctx.RoomManager.joinDoc(ctx.client, ctx.doc_id, err => {
- if (err) return reject(err)
- resolve()
- })
- })
- })
- it('should join the room using the id', function (ctx) {
- ctx.client.join.called.should.equal(true)
- })
- it('should not emit any events', function (ctx) {
- ctx.RoomEvents.emit.called.should.equal(false)
- })
- })
- })
- describe('leaveDoc', function () {
- describe('when doc room will be empty after this client has left', function () {
- beforeEach(function (ctx) {
- ctx.RoomManager._clientAlreadyInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .returns(true)
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .onCall(0)
- .returns(0)
- ctx.client.leave = sinon.stub()
- ctx.RoomManager.leaveDoc(ctx.client, ctx.doc_id)
- })
- it('should leave the room using the id', function (ctx) {
- ctx.client.leave.calledWithExactly(ctx.doc_id).should.equal(true)
- })
- it("should emit a 'doc-empty' event with the id", function (ctx) {
- ctx.RoomEvents.emit
- .calledWithExactly('doc-empty', ctx.doc_id)
- .should.equal(true)
- })
- })
- describe('when there are other clients in the doc room', function () {
- beforeEach(function (ctx) {
- ctx.RoomManager._clientAlreadyInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .returns(true)
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .onCall(0)
- .returns(123)
- ctx.client.leave = sinon.stub()
- ctx.RoomManager.leaveDoc(ctx.client, ctx.doc_id)
- })
- it('should leave the room using the id', function (ctx) {
- ctx.client.leave.calledWithExactly(ctx.doc_id).should.equal(true)
- })
- it('should not emit any events', function (ctx) {
- ctx.RoomEvents.emit.called.should.equal(false)
- })
- })
- describe('when the client is not in the doc room', function () {
- beforeEach(function (ctx) {
- ctx.RoomManager._clientAlreadyInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .returns(false)
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .onCall(0)
- .returns(0)
- ctx.client.leave = sinon.stub()
- ctx.RoomManager.leaveDoc(ctx.client, ctx.doc_id)
- })
- it('should not leave the room', function (ctx) {
- ctx.client.leave.called.should.equal(false)
- })
- it('should not emit any events', function (ctx) {
- ctx.RoomEvents.emit.called.should.equal(false)
- })
- })
- })
- describe('leaveProjectAndDocs', function () {
- describe('when the client is connected to the project and multiple docs', function () {
- beforeEach(function (ctx) {
- ctx.RoomManager._roomsClientIsIn = sinon
- .stub()
- .returns([ctx.project_id, ctx.doc_id, ctx.other_doc_id])
- ctx.client.join = sinon.stub()
- ctx.client.leave = sinon.stub()
- })
- describe('when this is the only client connected', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- // first call is for the join,
- // second for the leave
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .onCall(0)
- .returns(0)
- .onCall(1)
- .returns(0)
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.other_doc_id)
- .onCall(0)
- .returns(0)
- .onCall(1)
- .returns(0)
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.project_id)
- .onCall(0)
- .returns(0)
- .onCall(1)
- .returns(0)
- ctx.RoomManager._clientAlreadyInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .returns(true)
- .withArgs(ctx.client, ctx.other_doc_id)
- .returns(true)
- .withArgs(ctx.client, ctx.project_id)
- .returns(true)
- ctx.RoomEvents.on('project-active', id => {
- setTimeout(() => {
- ctx.RoomEvents.emit(`project-subscribed-${id}`)
- }, 100)
- })
- ctx.RoomEvents.on('doc-active', id => {
- setTimeout(() => {
- ctx.RoomEvents.emit(`doc-subscribed-${id}`)
- }, 100)
- })
- // put the client in the rooms
- ctx.RoomManager.joinProject(ctx.client, ctx.project_id, () => {
- ctx.RoomManager.joinDoc(ctx.client, ctx.doc_id, () => {
- ctx.RoomManager.joinDoc(ctx.client, ctx.other_doc_id, () => {
- // now leave the project
- ctx.RoomManager.leaveProjectAndDocs(ctx.client)
- resolve()
- })
- })
- })
- })
- })
- it('should leave all the docs', function (ctx) {
- ctx.client.leave.calledWithExactly(ctx.doc_id).should.equal(true)
- ctx.client.leave
- .calledWithExactly(ctx.other_doc_id)
- .should.equal(true)
- })
- it('should leave the project', function (ctx) {
- ctx.client.leave.calledWithExactly(ctx.project_id).should.equal(true)
- })
- it("should emit a 'doc-empty' event with the id for each doc", function (ctx) {
- ctx.RoomEvents.emit
- .calledWithExactly('doc-empty', ctx.doc_id)
- .should.equal(true)
- ctx.RoomEvents.emit
- .calledWithExactly('doc-empty', ctx.other_doc_id)
- .should.equal(true)
- })
- it("should emit a 'project-empty' event with the id for the project", function (ctx) {
- ctx.RoomEvents.emit
- .calledWithExactly('project-empty', ctx.project_id)
- .should.equal(true)
- })
- })
- describe('when other clients are still connected', function () {
- beforeEach(function (ctx) {
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .onFirstCall()
- .returns(123)
- .onSecondCall()
- .returns(122)
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.other_doc_id)
- .onFirstCall()
- .returns(123)
- .onSecondCall()
- .returns(122)
- ctx.RoomManager._clientsInRoom
- .withArgs(ctx.client, ctx.project_id)
- .onFirstCall()
- .returns(123)
- .onSecondCall()
- .returns(122)
- ctx.RoomManager._clientAlreadyInRoom
- .withArgs(ctx.client, ctx.doc_id)
- .returns(true)
- .withArgs(ctx.client, ctx.other_doc_id)
- .returns(true)
- .withArgs(ctx.client, ctx.project_id)
- .returns(true)
- ctx.RoomManager.leaveProjectAndDocs(ctx.client)
- })
- it('should leave all the docs', function (ctx) {
- ctx.client.leave.calledWithExactly(ctx.doc_id).should.equal(true)
- ctx.client.leave
- .calledWithExactly(ctx.other_doc_id)
- .should.equal(true)
- })
- it('should leave the project', function (ctx) {
- ctx.client.leave.calledWithExactly(ctx.project_id).should.equal(true)
- })
- it('should not emit any events', function (ctx) {
- ctx.RoomEvents.emit.called.should.equal(false)
- })
- })
- })
- })
- })
|