Remove sysend dependency (#10852)

GitOrigin-RevId: c3d9601256af8720ab41264609cb5c5c810afbba
This commit is contained in:
Alf Eaton 2023-01-09 12:52:11 +00:00 committed by Copybot
parent 1ff186a738
commit cda947d1ac
18 changed files with 640 additions and 580 deletions

12
package-lock.json generated
View file

@ -32072,11 +32072,6 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true
},
"node_modules/sysend": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/sysend/-/sysend-1.10.0.tgz",
"integrity": "sha512-kQDpqW60fvgbNLnWnTRaJ2KGX3aW5fThu9St8T4h+j8XC1YIDXhWVqS8Ho+WYgasmtrP7RvcRRhs+SVCb9o7wA=="
},
"node_modules/table-layout": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.5.tgz",
@ -38406,7 +38401,6 @@
"rolling-rate-limiter": "^0.2.10",
"sanitize-html": "^1.27.1",
"scroll-into-view-if-needed": "^2.2.25",
"sysend": "^1.10.0",
"tsscmp": "^1.0.6",
"underscore": "^1.13.1",
"unzipper": "^0.10.11",
@ -49247,7 +49241,6 @@
"sinon-chai": "^3.7.0",
"sinon-mongoose": "^2.3.0",
"socket.io-mock": "^1.3.1",
"sysend": "^1.10.0",
"terser-webpack-plugin": "^5.3.1",
"timekeeper": "^2.2.0",
"to-string-loader": "^1.2.0",
@ -70904,11 +70897,6 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true
},
"sysend": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/sysend/-/sysend-1.10.0.tgz",
"integrity": "sha512-kQDpqW60fvgbNLnWnTRaJ2KGX3aW5fThu9St8T4h+j8XC1YIDXhWVqS8Ho+WYgasmtrP7RvcRRhs+SVCb9o7wA=="
},
"table-layout": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.5.tgz",

View file

@ -7,7 +7,6 @@ import {
useState,
} from 'react'
import PropTypes from 'prop-types'
import sysend from 'sysend'
import getMeta from '../../utils/meta'
import { buildUrlWithDetachRole } from '../utils/url-helper'
import useCallbackHandlers from '../hooks/use-callback-handlers'
@ -26,7 +25,12 @@ DetachContext.Provider.propTypes = {
const debugPdfDetach = getMeta('ol-debugPdfDetach')
const SYSEND_CHANNEL = `detach-${getMeta('ol-project_id')}`
const projectId = getMeta('ol-project_id')
export const detachChannelId = `detach-${projectId}`
export const detachChannel =
'BroadcastChannel' in window
? new BroadcastChannel(detachChannelId)
: undefined
export function DetachProvider({ children }) {
const [lastDetachedConnectedAt, setLastDetachedConnectedAt] = useState()
@ -45,13 +49,20 @@ export function DetachProvider({ children }) {
}, [role])
useEffect(() => {
sysend.on(SYSEND_CHANNEL, message => {
if (detachChannel) {
const listener = event => {
if (debugPdfDetach) {
console.log(`Receiving:`, message)
console.log(`Receiving:`, event.data)
}
callEventHandlers(event.data)
}
detachChannel.addEventListener('message', listener)
return () => {
detachChannel.removeEventListener('message', listener)
}
}
callEventHandlers(message)
})
return () => sysend.off(SYSEND_CHANNEL)
}, [callEventHandlers])
const broadcastEvent = useCallback(
@ -80,7 +91,8 @@ export function DetachProvider({ children }) {
if (data) {
message.data = data
}
sysend.broadcast(SYSEND_CHANNEL, message)
detachChannel?.postMessage(message)
},
[role]
)

View file

@ -232,7 +232,6 @@
"rolling-rate-limiter": "^0.2.10",
"sanitize-html": "^1.27.1",
"scroll-into-view-if-needed": "^2.2.25",
"sysend": "^1.10.0",
"tsscmp": "^1.0.6",
"underscore": "^1.13.1",
"unzipper": "^0.10.11",

View file

@ -98,6 +98,16 @@ globalThis.ResizeObserver =
window.ResizeObserver =
require('@juggle/resize-observer').ResizeObserver
// add stub for BroadcastChannel (unused in these tests)
globalThis.BroadcastChannel =
global.BroadcastChannel =
window.BroadcastChannel =
class BroadcastChannel {
addEventListener(type, listener) {}
removeEventListener(type, listener) {}
postMessage(message) {}
}
// node-fetch doesn't accept relative URL's: https://github.com/node-fetch/node-fetch/blob/master/docs/v2-LIMITS.md#known-differences
const fetch = require('node-fetch')
globalThis.fetch =

View file

@ -1,7 +1,7 @@
import sysendTestHelper from '../../helpers/sysend'
import { EditorProviders } from '../../helpers/editor-providers'
import DetachCompileButton from '../../../../frontend/js/features/pdf-preview/components/detach-compile-button'
import { mockScope } from './scope'
import { testDetachChannel } from '../../helpers/detach-channel'
describe('<DetachCompileButton/>', function () {
beforeEach(function () {
@ -11,7 +11,6 @@ describe('<DetachCompileButton/>', function () {
afterEach(function () {
window.metaAttributesCache = new Map()
sysendTestHelper.resetHistory()
})
it('detacher mode and not linked: does not show button ', function () {
@ -41,8 +40,10 @@ describe('<DetachCompileButton/>', function () {
<EditorProviders scope={scope}>
<DetachCompileButton />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detached',
event: 'connected',
})
@ -62,8 +63,10 @@ describe('<DetachCompileButton/>', function () {
<EditorProviders scope={scope}>
<DetachCompileButton />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'connected',
})

View file

@ -1,6 +1,6 @@
import sysendTestHelper from '../../helpers/sysend'
import { EditorProviders } from '../../helpers/editor-providers'
import PdfLogsEntries from '../../../../frontend/js/features/pdf-preview/components/pdf-logs-entries'
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
window.metaAttributesCache = new Map([['ol-debugPdfDetach', true]])
describe('<PdfLogsEntries/>', function () {
@ -38,7 +38,6 @@ describe('<PdfLogsEntries/>', function () {
afterEach(function () {
window.metaAttributesCache = new Map()
sysendTestHelper.resetHistory()
})
it('displays human readable hint', function () {
@ -60,17 +59,14 @@ describe('<PdfLogsEntries/>', function () {
cy.findByRole('button', {
name: 'Navigate to log position in source code: main.tex, 9',
})
.click()
.then(() => {
expect(props.fileTreeManager.findEntityByPath).to.be.calledOnce
expect(props.editorManager.openDoc).to.be.calledOnce
expect(props.editorManager.openDoc).to.be.calledWith(fakeEntity, {
}).click()
cy.get('@findEntityByPath').should('be.calledOnce')
cy.get('@openDoc').should('be.calledOnceWith', fakeEntity, {
gotoLine: 9,
gotoColumn: 8,
})
})
})
it('opens doc via detached action', function () {
cy.window().then(win => {
@ -82,7 +78,7 @@ describe('<PdfLogsEntries/>', function () {
<PdfLogsEntries entries={logEntries} />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
testDetachChannel.postMessage({
role: 'detached',
event: 'action-sync-to-entry',
data: {
@ -95,15 +91,14 @@ describe('<PdfLogsEntries/>', function () {
],
},
})
})
expect(props.fileTreeManager.findEntityByPath).to.be.calledOnce
expect(props.editorManager.openDoc).to.be.calledOnce
expect(props.editorManager.openDoc).to.be.calledWith(fakeEntity, {
cy.get('@findEntityByPath').should('be.calledOnce')
cy.get('@openDoc').should('be.calledOnceWith', fakeEntity, {
gotoLine: 7,
gotoColumn: 6,
})
})
})
it('sends open doc clicks via detached action', function () {
cy.window().then(win => {
@ -116,15 +111,15 @@ describe('<PdfLogsEntries/>', function () {
</EditorProviders>
)
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
cy.findByRole('button', {
name: 'Navigate to log position in source code: main.tex, 9',
})
.click()
.then(() => {
expect(props.fileTreeManager.findEntityByPath).not.to.be.called
expect(props.editorManager.openDoc).not.to.be.called
}).click()
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
cy.get('@findEntityByPath').should('not.be.called')
cy.get('@openDoc').should('not.be.called')
cy.get('@postDetachMessage').should('be.calledWith', {
role: 'detached',
event: 'action-sync-to-entry',
data: {
@ -139,4 +134,3 @@ describe('<PdfLogsEntries/>', function () {
})
})
})
})

View file

@ -1,10 +1,8 @@
import sysendTestHelper from '../../helpers/sysend'
import PdfPreviewDetachedRoot from '../../../../frontend/js/features/pdf-preview/components/pdf-preview-detached-root'
import { User } from '../../../../types/user'
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
// https://github.com/overleaf/internal/issues/10080
// eslint-disable-next-line mocha/no-skipped-tests
describe.skip('<PdfPreviewDetachedRoot/>', function () {
describe('<PdfPreviewDetachedRoot/>', function () {
beforeEach(function () {
window.user = { id: 'user1' } as User
@ -21,17 +19,18 @@ describe.skip('<PdfPreviewDetachedRoot/>', function () {
afterEach(function () {
window.metaAttributesCache = new Map()
sysendTestHelper.resetHistory()
})
it('syncs compiling state', function () {
cy.mount(<PdfPreviewDetachedRoot />).then(() => {
sysendTestHelper.receiveMessage({
cy.mount(<PdfPreviewDetachedRoot />)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'connected',
})
sysendTestHelper.receiveMessage({
testDetachChannel.postMessage({
role: 'detacher',
event: 'state-compiling',
data: { value: true },
@ -39,10 +38,9 @@ describe.skip('<PdfPreviewDetachedRoot/>', function () {
})
cy.findByRole('button', { name: 'Compiling…' })
cy.findByRole('button', { name: 'Recompile' })
.should('not.exist')
.then(() => {
sysendTestHelper.receiveMessage({
cy.findByRole('button', { name: 'Recompile' }).should('not.exist')
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'state-compiling',
data: { value: false },
@ -53,19 +51,23 @@ describe.skip('<PdfPreviewDetachedRoot/>', function () {
})
it('sends a clear cache request when the button is pressed', function () {
cy.mount(<PdfPreviewDetachedRoot />).then(() => {
sysendTestHelper.receiveMessage({
cy.mount(<PdfPreviewDetachedRoot />)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'state-showLogs',
data: { value: true },
})
})
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
cy.findByRole('button', { name: 'Clear cached files' })
.should('not.be.disabled')
.click()
.should(() => {
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
cy.get('@postDetachMessage').should('be.calledWith', {
role: 'detached',
event: 'action-clearCache',
data: {
@ -74,4 +76,3 @@ describe.skip('<PdfPreviewDetachedRoot/>', function () {
})
})
})
})

View file

@ -1,6 +1,6 @@
import sysendTestHelper from '../../helpers/sysend'
import { EditorProviders } from '../../helpers/editor-providers'
import PdfPreviewHybridToolbar from '../../../../frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
import { testDetachChannel } from '../../helpers/detach-channel'
describe('<PdfPreviewHybridToolbar/>', function () {
beforeEach(function () {
@ -10,7 +10,6 @@ describe('<PdfPreviewHybridToolbar/>', function () {
afterEach(function () {
window.metaAttributesCache = new Map()
sysendTestHelper.resetHistory()
})
it('shows normal mode', function () {
@ -47,8 +46,10 @@ describe('<PdfPreviewHybridToolbar/>', function () {
<EditorProviders>
<PdfPreviewHybridToolbar />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'connected',
})
@ -66,12 +67,14 @@ describe('<PdfPreviewHybridToolbar/>', function () {
<EditorProviders>
<PdfPreviewHybridToolbar />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'connected',
})
sysendTestHelper.receiveMessage({
testDetachChannel.postMessage({
role: 'detacher',
event: 'closed',
})

View file

@ -1,11 +1,11 @@
import PdfSynctexControls from '../../../../frontend/js/features/pdf-preview/components/pdf-synctex-controls'
import sysendTestHelper from '../../helpers/sysend'
import { cloneDeep } from 'lodash'
import { useDetachCompileContext as useCompileContext } from '../../../../frontend/js/shared/context/detach-compile-context'
import { useFileTreeData } from '../../../../frontend/js/shared/context/file-tree-data-context'
import { useEffect } from 'react'
import { EditorProviders } from '../../helpers/editor-providers'
import { mockScope } from './scope'
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
const mockHighlights = [
{
@ -55,7 +55,7 @@ const WithPosition = ({ mockPosition }: { mockPosition: Position }) => {
// mock PDF scroll position update
const setDetachedPosition = (mockPosition: Position) => {
sysendTestHelper.receiveMessage({
testDetachChannel.postMessage({
role: 'detacher',
event: 'state-position',
data: { value: mockPosition },
@ -127,8 +127,7 @@ const interceptSyncPdf = () => {
}).as('sync-pdf')
}
// eslint-disable-next-line mocha/no-skipped-tests
describe.skip('<PdfSynctexControls/>', function () {
describe('<PdfSynctexControls/>', function () {
beforeEach(function () {
window.metaAttributesCache = new Map()
@ -255,9 +254,7 @@ describe.skip('<PdfSynctexControls/>', function () {
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
</EditorProviders>
).then(() => {
sysendTestHelper.resetHistory()
})
)
cy.wait('@compile')
@ -270,6 +267,8 @@ describe.skip('<PdfSynctexControls/>', function () {
)
})
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
const syncing = interceptSyncCodeAsync()
cy.findByRole('button', {
@ -290,20 +289,16 @@ describe.skip('<PdfSynctexControls/>', function () {
cy.findByRole('button', {
name: 'Go to code location in PDF',
}).should(() => {
const message = sysendTestHelper.getMessageWithEvent(
'action-setHighlights'
)
}).should('not.be.disabled')
// synctex is called locally and the result are broadcast for the detached tab
// NOTE: can't use `.to.deep.include({…})` as it doesn't match the nested array
expect(message).to.deep.equal({
cy.get('@postDetachMessage').should('be.calledWith', {
role: 'detacher',
event: 'action-setHighlights',
data: { args: [mockHighlights] },
})
})
})
it('reacts to sync to code action', function () {
interceptSyncPdf()
@ -317,7 +312,7 @@ describe.skip('<PdfSynctexControls/>', function () {
<PdfSynctexControls />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
testDetachChannel.postMessage({
role: 'detached',
event: 'action-sync-to-code',
data: {
@ -362,7 +357,7 @@ describe.skip('<PdfSynctexControls/>', function () {
)
cy.wait('@compile').then(() => {
sysendTestHelper.receiveMessage({
testDetachChannel.postMessage({
role: 'detacher',
event: `state-position`,
data: { value: mockPosition },
@ -373,36 +368,28 @@ describe.skip('<PdfSynctexControls/>', function () {
name: /^Go to PDF location in code/,
})
cy.findByRole('button', { name: /^Go to PDF location in code/ })
.should('not.be.disabled')
.then(() => {
sysendTestHelper.resetHistory()
})
cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
'not.be.disabled'
)
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
cy.findByRole('button', { name: /^Go to PDF location in code/ }).click()
// the button is only disabled when the state is updated via sysend
// the button is only disabled when the state is updated
cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
'not.be.disabled'
)
cy.get('.synctex-spin-icon').should('not.exist')
cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
() => {
const message = sysendTestHelper.getMessageWithEvent(
'action-sync-to-code'
)
expect(message).to.deep.equal({
cy.get('@postDetachMessage').should('be.calledWith', {
role: 'detached',
event: 'action-sync-to-code',
data: {
args: [mockPosition, 72],
},
})
}
)
})
it('update inflight state', function () {
@ -413,8 +400,10 @@ describe.skip('<PdfSynctexControls/>', function () {
<WithPosition mockPosition={mockPosition} />
<PdfSynctexControls />
</EditorProviders>
).then(() => {
sysendTestHelper.receiveMessage({
)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: `state-position`,
data: { value: mockPosition },
@ -425,10 +414,10 @@ describe.skip('<PdfSynctexControls/>', function () {
'not.be.disabled'
)
cy.get('.synctex-spin-icon')
.should('not.exist')
.then(() => {
sysendTestHelper.receiveMessage({
cy.get('.synctex-spin-icon').should('not.exist')
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'state-sync-to-code-inflight',
data: { value: true },
@ -439,10 +428,10 @@ describe.skip('<PdfSynctexControls/>', function () {
'be.disabled'
)
cy.get('.synctex-spin-icon')
.should('have.length', 1)
.then(() => {
sysendTestHelper.receiveMessage({
cy.get('.synctex-spin-icon').should('have.length', 1)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'state-sync-to-code-inflight',
data: { value: false },

View file

@ -0,0 +1,10 @@
import {
detachChannelId,
detachChannel as _detachChannel,
} from '../../../frontend/js/shared/context/detach-context'
// for tests, assert that detachChannel is defined, as BroadcastChannel is available
export const detachChannel = _detachChannel!
// simulate messages from another tab by posting them to this channel
export const testDetachChannel = new BroadcastChannel(detachChannelId)

View file

@ -2,7 +2,6 @@
/* eslint-disable react/prop-types */
import { render } from '@testing-library/react'
import { renderHook } from '@testing-library/react-hooks'
import { ChatProvider } from '../../../frontend/js/features/chat/context/chat-context'
import { EditorProviders } from './editor-providers'
@ -21,14 +20,6 @@ export function renderWithEditorContext(
})
}
export function renderHookWithEditorContext(hook, contextProps) {
const EditorProvidersWrapper = ({ children }) => (
<EditorProviders {...contextProps}>{children}</EditorProviders>
)
return renderHook(hook, { wrapper: EditorProvidersWrapper })
}
export function ChatProviders({ children, ...props }) {
return (
<EditorProviders {...props}>

View file

@ -1,53 +0,0 @@
import sysend from 'sysend'
import sinon from 'sinon'
const sysendSpy = sinon.spy(sysend)
function resetHistory() {
for (const method of Object.keys(sysendSpy)) {
if (sysendSpy[method].resetHistory) sysendSpy[method].resetHistory()
}
}
// sysends sends and receives custom calls in the background. This Helps
// filtering them out
function getDetachCalls(method) {
return sysend[method]
.getCalls()
.filter(call => call.args[0].startsWith('detach-'))
}
function getLastDetachCall(method) {
return getDetachCalls(method).pop()
}
function getLastBroacastMessage() {
return getLastDetachCall('broadcast')?.args[1]
}
function getAllBroacastMessages() {
return getDetachCalls('broadcast')
}
// this fakes receiving a message by calling the handler add to `on`. A bit
// funky, but works for now
function receiveMessage(message) {
getLastDetachCall('on').args[1](message)
}
function getMessageWithEvent(eventName) {
return getAllBroacastMessages()
.map(item => item.args[1])
.find(item => item.event === eventName)
}
export default {
spy: sysendSpy,
resetHistory,
getDetachCalls,
getLastDetachCall,
getLastBroacastMessage,
getAllBroacastMessages,
getMessageWithEvent,
receiveMessage,
}

View file

@ -0,0 +1,121 @@
import { FC } from 'react'
import useDetachAction from '../../../../frontend/js/shared/hooks/use-detach-action'
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
import { EditorProviders } from '../../helpers/editor-providers'
const DetachActionTest: FC<{
actionName: string
actionFunction: () => void
handleClick: (trigger: (value: any) => void) => void
}> = ({ actionName, actionFunction, handleClick }) => {
const trigger = useDetachAction(
actionName,
actionFunction,
'detacher',
'detached'
)
return (
<button id="trigger" onClick={() => handleClick(trigger)}>
trigger
</button>
)
}
describe('useDetachAction', function () {
beforeEach(function () {
window.metaAttributesCache = new Map()
})
afterEach(function () {
window.metaAttributesCache = new Map()
})
it('broadcast message as sender', function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
cy.mount(
<EditorProviders>
<DetachActionTest
actionName="some-action"
actionFunction={cy.stub().as('actionFunction')}
handleClick={trigger => trigger('foo')}
/>
</EditorProviders>
)
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
cy.get('#trigger').click()
cy.get('@postDetachMessage').should('be.calledWith', {
role: 'detacher',
event: 'action-some-action',
data: { args: ['foo'] },
})
cy.get('@actionFunction').should('not.be.called')
})
it('call function as non-sender', function () {
cy.mount(
<EditorProviders>
<DetachActionTest
actionName="some-action"
actionFunction={cy.stub().as('actionFunction')}
handleClick={trigger => trigger('foo')}
/>
</EditorProviders>
)
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
cy.get('#trigger').click()
cy.get('@postDetachMessage').should('not.be.called')
cy.get('@actionFunction').should('be.calledWith', 'foo')
})
it('receive message and call function as target', function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
cy.mount(
<EditorProviders>
<DetachActionTest
actionName="some-action"
actionFunction={cy.stub().as('actionFunction')}
handleClick={trigger => trigger('foo')}
/>
</EditorProviders>
)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detached',
event: 'action-some-action',
data: { args: ['foo'] },
})
})
cy.get('@actionFunction').should('be.calledWith', 'foo')
})
it('receive message and does not call function as non-target', function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
cy.mount(
<EditorProviders>
<DetachActionTest
actionName="some-action"
actionFunction={cy.stub().as('actionFunction')}
handleClick={trigger => trigger('foo')}
/>
</EditorProviders>
)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detached',
event: 'action-some-action',
data: { args: [] },
})
})
cy.get('@actionFunction').should('not.be.called')
})
})

View file

@ -1,82 +0,0 @@
import sinon from 'sinon'
import { expect } from 'chai'
import { renderHookWithEditorContext } from '../../helpers/render-with-context'
import sysendTestHelper from '../../helpers/sysend'
import useDetachAction from '../../../../frontend/js/shared/hooks/use-detach-action'
const actionName = 'some-action'
const actionFunction = sinon.stub()
describe('useDetachAction', function () {
beforeEach(function () {
window.metaAttributesCache = new Map()
})
afterEach(function () {
window.metaAttributesCache = new Map()
actionFunction.reset()
})
it('broadcast message as sender', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
const { result } = renderHookWithEditorContext(() =>
useDetachAction(actionName, actionFunction, 'detacher', 'detached')
)
const triggerFn = result.current
sysendTestHelper.resetHistory()
triggerFn('param')
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detacher',
event: `action-${actionName}`,
data: { args: ['param'] },
})
sinon.assert.notCalled(actionFunction)
})
it('call function as non-sender', async function () {
const { result } = renderHookWithEditorContext(() =>
useDetachAction(actionName, actionFunction, 'detacher', 'detached')
)
const triggerFn = result.current
sysendTestHelper.resetHistory()
triggerFn('param')
expect(sysendTestHelper.getDetachCalls('broadcast').length).to.equal(0)
sinon.assert.calledWith(actionFunction, 'param')
})
it('receive message and call function as target', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
renderHookWithEditorContext(() =>
useDetachAction(actionName, actionFunction, 'detacher', 'detached')
)
sysendTestHelper.receiveMessage({
role: 'detached',
event: `action-${actionName}`,
data: { args: ['param'] },
})
sinon.assert.calledWith(actionFunction, 'param')
})
it('receive message and does not call function as non-target', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
renderHookWithEditorContext(() =>
useDetachAction(actionName, actionFunction, 'detacher', 'detached')
)
sysendTestHelper.receiveMessage({
role: 'detached',
event: `action-${actionName}`,
data: { args: [] },
})
sinon.assert.notCalled(actionFunction)
})
})

View file

@ -0,0 +1,244 @@
import useDetachLayout from '../../../../frontend/js/shared/hooks/use-detach-layout'
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
import { EditorProviders } from '../../helpers/editor-providers'
import { Button, Checkbox, ControlLabel, FormGroup } from 'react-bootstrap'
const DetachLayoutTest = () => {
const { role, reattach, detach, isLinked, isLinking, isRedundant } =
useDetachLayout()
return (
<fieldset>
<legend>
role: <span id="role">{role || 'none'}</span>
</legend>
<FormGroup>
<Checkbox id="isLinked" inline checked={isLinked} readOnly />
<ControlLabel>linked</ControlLabel>
</FormGroup>
<FormGroup>
<Checkbox id="isLinking" inline checked={isLinking} readOnly />
<ControlLabel>linking</ControlLabel>
</FormGroup>
<FormGroup>
<Checkbox id="isRedundant" inline checked={isRedundant} readOnly />
<ControlLabel>redundant</ControlLabel>
</FormGroup>
<Button id="reattach" onClick={reattach}>
reattach
</Button>
<Button id="detach" onClick={detach}>
detach
</Button>
</fieldset>
)
}
describe('useDetachLayout', function () {
beforeEach(function () {
window.metaAttributesCache = new Map()
cy.stub(window, 'open').as('openWindow')
cy.stub(window, 'close').as('closeWindow')
})
afterEach(function () {
window.metaAttributesCache = new Map()
})
it('detaching', function () {
// 1. create hook in normal mode
cy.mount(
<EditorProviders>
<DetachLayoutTest />
</EditorProviders>
)
cy.get('#isLinked').should('not.be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'none')
// 2. detach
cy.get('#detach').click()
cy.get('@openWindow').should(
'be.calledOnceWith',
Cypress.sinon.match(/\/detached$/),
'_blank'
)
cy.get('#isLinked').should('not.be.checked')
cy.get('#isLinking').should('be.checked')
cy.get('#role').should('have.text', 'detacher')
})
it('detacher role', function () {
// 1. create hook in detacher mode
window.metaAttributesCache.set('ol-detachRole', 'detacher')
cy.mount(
<EditorProviders>
<DetachLayoutTest />
</EditorProviders>
)
cy.get('#isLinked').should('not.be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'detacher')
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detached',
event: 'connected',
})
})
// 2. simulate connected detached tab
cy.get('@postDetachMessage').should('be.calledWith', {
role: 'detacher',
event: 'up',
})
cy.get('#isLinked').should('be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'detacher')
// 3. simulate closed detached tab
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detached',
event: 'closed',
})
})
cy.get('#isLinked').should('not.be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'detacher')
// 4. simulate up detached tab
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detached',
event: 'up',
})
})
cy.get('#isLinked').should('be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'detacher')
// 5. reattach
cy.get('@postDetachMessage').invoke('resetHistory')
cy.get('#reattach').click()
cy.get('#isLinked').should('not.be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'none')
cy.get('@postDetachMessage').should('be.calledWith', {
role: 'detacher',
event: 'reattach',
})
})
it('reset detacher role when other detacher tab connects', function () {
// 1. create hook in detacher mode
window.metaAttributesCache.set('ol-detachRole', 'detacher')
cy.mount(
<EditorProviders>
<DetachLayoutTest />
</EditorProviders>
)
cy.get('#isLinked').should('not.be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'detacher')
// 2. simulate other detacher tab
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'up',
})
})
cy.get('#isRedundant').should('be.checked')
cy.get('#role').should('have.text', 'none')
})
it('detached role', function () {
// 1. create hook in detached mode
window.metaAttributesCache.set('ol-detachRole', 'detached')
cy.mount(
<EditorProviders>
<DetachLayoutTest />
</EditorProviders>
)
cy.get('#isLinked').should('not.be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'detached')
// 2. simulate up detacher tab
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'up',
})
})
cy.get('#isLinked').should('be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'detached')
// 3. simulate closed detacher tab
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'closed',
})
})
cy.get('#isLinked').should('not.be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'detached')
// 4. simulate up detacher tab
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'up',
})
})
cy.get('#isLinked').should('be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'detached')
// 5. simulate closed detached tab
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detached',
event: 'closed',
})
})
cy.get('#isLinked').should('be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'detached')
cy.get('@postDetachMessage').should('be.calledWith', {
role: 'detached',
event: 'up',
})
// 6. simulate reattach event
cy.get('@postDetachMessage').invoke('resetHistory')
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detacher',
event: 'reattach',
})
})
cy.get('#isLinked').should('not.be.checked')
cy.get('#isLinking').should('not.be.checked')
cy.get('#role').should('have.text', 'detached')
cy.get('@closeWindow').should('be.called')
})
})

View file

@ -1,210 +0,0 @@
import { waitFor } from '@testing-library/react'
import { act } from '@testing-library/react-hooks'
import { expect } from 'chai'
import sinon from 'sinon'
import { renderHookWithEditorContext } from '../../helpers/render-with-context'
import sysendTestHelper from '../../helpers/sysend'
import useDetachLayout from '../../../../frontend/js/shared/hooks/use-detach-layout'
describe('useDetachLayout', function () {
let openStub
let closeStub
beforeEach(function () {
window.metaAttributesCache = new Map()
openStub = sinon.stub(window, 'open')
closeStub = sinon.stub(window, 'close')
})
afterEach(function () {
window.metaAttributesCache = new Map()
openStub.restore()
closeStub.restore()
})
it('detaching', async function () {
// 1. create hook in normal mode
const { result } = renderHookWithEditorContext(() => useDetachLayout())
expect(result.current.reattach).to.be.a('function')
expect(result.current.detach).to.be.a('function')
expect(result.current.isLinked).to.be.false
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.be.null
// 2. detach
act(() => {
result.current.detach()
})
expect(result.current.isLinked).to.be.false
expect(result.current.isLinking).to.be.true
expect(result.current.role).to.equal('detacher')
sinon.assert.calledOnce(openStub)
sinon.assert.calledWith(
openStub,
'https://www.test-overleaf.com/detached',
'_blank'
)
})
it('detacher role', async function () {
sysendTestHelper.spy.broadcast.resetHistory()
window.metaAttributesCache.set('ol-detachRole', 'detacher')
// 1. create hook in detacher mode
const { result } = renderHookWithEditorContext(() => useDetachLayout())
expect(result.current.reattach).to.be.a('function')
expect(result.current.detach).to.be.a('function')
expect(result.current.isLinked).to.be.false
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.equal('detacher')
const broadcastMessagesCount =
sysendTestHelper.getAllBroacastMessages().length
// 2. simulate connected detached tab
sysendTestHelper.spy.broadcast.resetHistory()
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'connected',
})
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detacher',
event: 'up',
})
expect(result.current.isLinked).to.be.true
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.equal('detacher')
// check that all message were re-broadcast for the new tab
await nextTick() // necessary to ensure all event handler have run
await waitFor(() => {
const reBroadcastMessagesCount =
sysendTestHelper.getAllBroacastMessages().length
expect(reBroadcastMessagesCount).to.equal(broadcastMessagesCount)
})
// 3. simulate closed detached tab
sysendTestHelper.spy.broadcast.resetHistory()
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'closed',
})
expect(result.current.isLinked).to.be.false
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.equal('detacher')
// 4. simulate up detached tab
sysendTestHelper.spy.broadcast.resetHistory()
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'up',
})
expect(result.current.isLinked).to.be.true
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.equal('detacher')
// 5. reattach
sysendTestHelper.spy.broadcast.resetHistory()
act(() => {
result.current.reattach()
})
expect(result.current.isLinked).to.be.false
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.be.null
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detacher',
event: 'reattach',
})
})
it('reset detacher role when other detacher tab connects', function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
// 1. create hook in detacher mode
const { result } = renderHookWithEditorContext(() => useDetachLayout())
expect(result.current.reattach).to.be.a('function')
expect(result.current.detach).to.be.a('function')
expect(result.current.isLinked).to.be.false
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.equal('detacher')
// 2. simulate other detacher tab
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'up',
})
expect(result.current.isRedundant).to.be.true
expect(result.current.role).to.equal(null)
})
it('detached role', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
// 1. create hook in detached mode
const { result } = renderHookWithEditorContext(() => useDetachLayout())
expect(result.current.reattach).to.be.a('function')
expect(result.current.detach).to.be.a('function')
expect(result.current.isLinked).to.be.false
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.equal('detached')
// 2. simulate up detacher tab
sysendTestHelper.spy.broadcast.resetHistory()
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'up',
})
expect(result.current.isLinked).to.be.true
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.equal('detached')
// 3. simulate closed detacher tab
sysendTestHelper.spy.broadcast.resetHistory()
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'closed',
})
expect(result.current.isLinked).to.be.false
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.equal('detached')
// 4. simulate up detacher tab
sysendTestHelper.spy.broadcast.resetHistory()
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'up',
})
expect(result.current.isLinked).to.be.true
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.equal('detached')
// 5. simulate closed detached tab
sysendTestHelper.spy.broadcast.resetHistory()
sysendTestHelper.receiveMessage({
role: 'detached',
event: 'closed',
})
expect(result.current.isLinked).to.be.true
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.equal('detached')
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detached',
event: 'up',
})
// 6. simulate reattach event
sysendTestHelper.spy.broadcast.resetHistory()
sysendTestHelper.receiveMessage({
role: 'detacher',
event: 'reattach',
})
expect(result.current.isLinked).to.be.false
expect(result.current.isLinking).to.be.false
expect(result.current.role).to.equal('detached')
sinon.assert.called(closeStub)
})
})
const nextTick = () => {
return new Promise(resolve => {
setTimeout(resolve)
})
}

View file

@ -0,0 +1,108 @@
import { FC } from 'react'
import useDetachState from '../../../../frontend/js/shared/hooks/use-detach-state'
import { EditorProviders } from '../../helpers/editor-providers'
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
const DetachStateTest: FC<{
stateKey: string
defaultValue: any
senderRole?: string
targetRole?: string
handleClick: (setValue: (value: any) => void) => void
}> = ({ stateKey, defaultValue, handleClick, senderRole, targetRole }) => {
const [value, setValue] = useDetachState(
stateKey,
defaultValue,
senderRole,
targetRole
)
return (
<div>
<div id="value">{value}</div>
<button id="setValue" onClick={() => handleClick(setValue)}>
set value
</button>
</div>
)
}
describe('useDetachState', function () {
beforeEach(function () {
window.metaAttributesCache = new Map()
})
afterEach(function () {
window.metaAttributesCache = new Map()
})
it('create and update state', function () {
cy.mount(
<EditorProviders>
<DetachStateTest
stateKey="some-key"
defaultValue="foobar"
handleClick={setValue => {
setValue('barbaz')
}}
/>
</EditorProviders>
)
cy.get('#value').should('have.text', 'foobar')
cy.get('#setValue').click()
cy.get('#value').should('have.text', 'barbaz')
})
it('broadcast message as sender', function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
cy.mount(
<EditorProviders>
<DetachStateTest
stateKey="some-key"
defaultValue={null}
senderRole="detacher"
targetRole="detached"
handleClick={setValue => {
setValue('barbaz1')
}}
/>
</EditorProviders>
)
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
cy.get('#setValue').click()
cy.get('@postDetachMessage').should('be.calledWith', {
role: 'detacher',
event: 'state-some-key',
data: { value: 'barbaz1' },
})
})
it('receive message as target', function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
cy.mount(
<EditorProviders>
<DetachStateTest
stateKey="some-key"
defaultValue={null}
senderRole="detacher"
targetRole="detached"
handleClick={() => {}}
/>
</EditorProviders>
)
cy.wrap(null).then(() => {
testDetachChannel.postMessage({
role: 'detached',
event: 'state-some-key',
data: { value: 'barbaz2' },
})
})
cy.get('#value').should('have.text', 'barbaz2')
})
})

View file

@ -1,68 +0,0 @@
import { act } from '@testing-library/react-hooks'
import { expect } from 'chai'
import { renderHookWithEditorContext } from '../../helpers/render-with-context'
import sysendTestHelper from '../../helpers/sysend'
import useDetachState from '../../../../frontend/js/shared/hooks/use-detach-state'
const stateKey = 'some-key'
describe('useDetachState', function () {
beforeEach(function () {
window.metaAttributesCache = new Map()
})
afterEach(function () {
window.metaAttributesCache = new Map()
})
it('create and update state', async function () {
const defaultValue = 'foobar'
const { result } = renderHookWithEditorContext(() =>
useDetachState(stateKey, defaultValue)
)
const [value, setValue] = result.current
expect(value).to.equal(defaultValue)
expect(setValue).to.be.a('function')
const newValue = 'barbaz'
act(() => {
setValue(newValue)
})
expect(result.current[0]).to.equal(newValue)
})
it('broadcast message as sender', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detacher')
const { result } = renderHookWithEditorContext(() =>
useDetachState(stateKey, null, 'detacher', 'detached')
)
const [, setValue] = result.current
sysendTestHelper.resetHistory()
const newValue = 'barbaz'
act(() => {
setValue(newValue)
})
expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({
role: 'detacher',
event: `state-${stateKey}`,
data: { value: newValue },
})
})
it('receive message as target', async function () {
window.metaAttributesCache.set('ol-detachRole', 'detached')
const { result } = renderHookWithEditorContext(() =>
useDetachState(stateKey, null, 'detacher', 'detached')
)
const newValue = 'barbaz'
sysendTestHelper.receiveMessage({
role: 'detached',
event: `state-${stateKey}`,
data: { value: newValue },
})
expect(result.current[0]).to.equal(newValue)
})
})