diff --git a/package-lock.json b/package-lock.json index e14839553f..f0a02ba701 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/services/web/frontend/js/shared/context/detach-context.js b/services/web/frontend/js/shared/context/detach-context.js index e9b21c9d9d..ea558fbcdf 100644 --- a/services/web/frontend/js/shared/context/detach-context.js +++ b/services/web/frontend/js/shared/context/detach-context.js @@ -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 (debugPdfDetach) { - console.log(`Receiving:`, message) + if (detachChannel) { + const listener = event => { + if (debugPdfDetach) { + console.log(`Receiving:`, event.data) + } + callEventHandlers(event.data) } - callEventHandlers(message) - }) - return () => sysend.off(SYSEND_CHANNEL) + + detachChannel.addEventListener('message', listener) + + return () => { + detachChannel.removeEventListener('message', listener) + } + } }, [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] ) diff --git a/services/web/package.json b/services/web/package.json index e7bbd60e03..9c5e1100ad 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -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", diff --git a/services/web/test/frontend/bootstrap.js b/services/web/test/frontend/bootstrap.js index f4344339ed..6e09370c42 100644 --- a/services/web/test/frontend/bootstrap.js +++ b/services/web/test/frontend/bootstrap.js @@ -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 = diff --git a/services/web/test/frontend/components/pdf-preview/detach-compile-button.spec.tsx b/services/web/test/frontend/components/pdf-preview/detach-compile-button.spec.tsx index 1323657248..be8caadb34 100644 --- a/services/web/test/frontend/components/pdf-preview/detach-compile-button.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/detach-compile-button.spec.tsx @@ -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('', function () { beforeEach(function () { @@ -11,7 +11,6 @@ describe('', function () { afterEach(function () { window.metaAttributesCache = new Map() - sysendTestHelper.resetHistory() }) it('detacher mode and not linked: does not show button ', function () { @@ -30,7 +29,7 @@ describe('', function () { cy.findByRole('button', { name: 'Recompile' }).should('not.exist') }) - it('detacher mode and linked: show button ', function () { + it('detacher mode and linked: show button', function () { cy.window().then(win => { win.metaAttributesCache = new Map([['ol-detachRole', 'detacher']]) }) @@ -41,8 +40,10 @@ describe('', function () { - ).then(() => { - sysendTestHelper.receiveMessage({ + ) + + cy.wrap(null).then(() => { + testDetachChannel.postMessage({ role: 'detached', event: 'connected', }) @@ -62,8 +63,10 @@ describe('', function () { - ).then(() => { - sysendTestHelper.receiveMessage({ + ) + + cy.wrap(null).then(() => { + testDetachChannel.postMessage({ role: 'detacher', event: 'connected', }) diff --git a/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx index 669e254ccf..e42f2216ff 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx @@ -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('', function () { @@ -38,7 +38,6 @@ describe('', function () { afterEach(function () { window.metaAttributesCache = new Map() - sysendTestHelper.resetHistory() }) it('displays human readable hint', function () { @@ -60,16 +59,13 @@ describe('', function () { cy.findByRole('button', { name: 'Navigate to log position in source code: main.tex, 9', + }).click() + + cy.get('@findEntityByPath').should('be.calledOnce') + cy.get('@openDoc').should('be.calledOnceWith', fakeEntity, { + gotoLine: 9, + gotoColumn: 8, }) - .click() - .then(() => { - expect(props.fileTreeManager.findEntityByPath).to.be.calledOnce - expect(props.editorManager.openDoc).to.be.calledOnce - expect(props.editorManager.openDoc).to.be.calledWith(fakeEntity, { - gotoLine: 9, - gotoColumn: 8, - }) - }) }) it('opens doc via detached action', function () { @@ -82,7 +78,7 @@ describe('', function () { ).then(() => { - sysendTestHelper.receiveMessage({ + testDetachChannel.postMessage({ role: 'detached', event: 'action-sync-to-entry', data: { @@ -95,13 +91,12 @@ describe('', function () { ], }, }) + }) - expect(props.fileTreeManager.findEntityByPath).to.be.calledOnce - expect(props.editorManager.openDoc).to.be.calledOnce - expect(props.editorManager.openDoc).to.be.calledWith(fakeEntity, { - gotoLine: 7, - gotoColumn: 6, - }) + cy.get('@findEntityByPath').should('be.calledOnce') + cy.get('@openDoc').should('be.calledOnceWith', fakeEntity, { + gotoLine: 7, + gotoColumn: 6, }) }) @@ -116,27 +111,26 @@ describe('', function () { ) + 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({ - role: 'detached', - event: 'action-sync-to-entry', - data: { - args: [ - { - file: 'main.tex', - line: 9, - column: 8, - }, - ], + 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: { + args: [ + { + file: 'main.tex', + line: 9, + column: 8, }, - }) - }) + ], + }, + }) }) }) diff --git a/services/web/test/frontend/components/pdf-preview/pdf-preview-detached-root.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-preview-detached-root.spec.tsx index ee760ff24f..007fa48d76 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-preview-detached-root.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-preview-detached-root.spec.tsx @@ -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('', function () { +describe('', function () { beforeEach(function () { window.user = { id: 'user1' } as User @@ -21,17 +19,18 @@ describe.skip('', function () { afterEach(function () { window.metaAttributesCache = new Map() - sysendTestHelper.resetHistory() }) it('syncs compiling state', function () { - cy.mount().then(() => { - sysendTestHelper.receiveMessage({ + cy.mount() + + cy.wrap(null).then(() => { + testDetachChannel.postMessage({ role: 'detacher', event: 'connected', }) - sysendTestHelper.receiveMessage({ + testDetachChannel.postMessage({ role: 'detacher', event: 'state-compiling', data: { value: true }, @@ -39,39 +38,41 @@ describe.skip('', function () { }) cy.findByRole('button', { name: 'Compiling…' }) - cy.findByRole('button', { name: 'Recompile' }) - .should('not.exist') - .then(() => { - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'state-compiling', - data: { value: false }, - }) + cy.findByRole('button', { name: 'Recompile' }).should('not.exist') + cy.wrap(null).then(() => { + testDetachChannel.postMessage({ + role: 'detacher', + event: 'state-compiling', + data: { value: false }, }) + }) cy.findByRole('button', { name: 'Recompile' }) cy.findByRole('button', { name: 'Compiling…' }).should('not.exist') }) it('sends a clear cache request when the button is pressed', function () { - cy.mount().then(() => { - sysendTestHelper.receiveMessage({ + cy.mount() + + 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({ - role: 'detached', - event: 'action-clearCache', - data: { - args: [], - }, - }) - }) + + cy.get('@postDetachMessage').should('be.calledWith', { + role: 'detached', + event: 'action-clearCache', + data: { + args: [], + }, + }) }) }) diff --git a/services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx index a110e34029..c88c9e219a 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx @@ -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('', function () { beforeEach(function () { @@ -10,7 +10,6 @@ describe('', function () { afterEach(function () { window.metaAttributesCache = new Map() - sysendTestHelper.resetHistory() }) it('shows normal mode', function () { @@ -47,8 +46,10 @@ describe('', function () { - ).then(() => { - sysendTestHelper.receiveMessage({ + ) + + cy.wrap(null).then(() => { + testDetachChannel.postMessage({ role: 'detacher', event: 'connected', }) @@ -66,12 +67,14 @@ describe('', function () { - ).then(() => { - sysendTestHelper.receiveMessage({ + ) + + cy.wrap(null).then(() => { + testDetachChannel.postMessage({ role: 'detacher', event: 'connected', }) - sysendTestHelper.receiveMessage({ + testDetachChannel.postMessage({ role: 'detacher', event: 'closed', }) diff --git a/services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx index 759885bb8e..e340ca1092 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx @@ -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('', function () { +describe('', function () { beforeEach(function () { window.metaAttributesCache = new Map() @@ -255,9 +254,7 @@ describe.skip('', function () { - ).then(() => { - sysendTestHelper.resetHistory() - }) + ) cy.wait('@compile') @@ -270,6 +267,8 @@ describe.skip('', function () { ) }) + cy.spy(detachChannel, 'postMessage').as('postDetachMessage') + const syncing = interceptSyncCodeAsync() cy.findByRole('button', { @@ -290,18 +289,14 @@ describe.skip('', 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({ - role: 'detacher', - event: 'action-setHighlights', - data: { args: [mockHighlights] }, - }) + // 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 + cy.get('@postDetachMessage').should('be.calledWith', { + role: 'detacher', + event: 'action-setHighlights', + data: { args: [mockHighlights] }, }) }) @@ -317,7 +312,7 @@ describe.skip('', function () { ).then(() => { - sysendTestHelper.receiveMessage({ + testDetachChannel.postMessage({ role: 'detached', event: 'action-sync-to-code', data: { @@ -362,7 +357,7 @@ describe.skip('', function () { ) cy.wait('@compile').then(() => { - sysendTestHelper.receiveMessage({ + testDetachChannel.postMessage({ role: 'detacher', event: `state-position`, data: { value: mockPosition }, @@ -373,36 +368,28 @@ describe.skip('', 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({ - role: 'detached', - event: 'action-sync-to-code', - data: { - args: [mockPosition, 72], - }, - }) - } - ) + 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('', function () { - ).then(() => { - sysendTestHelper.receiveMessage({ + ) + + cy.wrap(null).then(() => { + testDetachChannel.postMessage({ role: 'detacher', event: `state-position`, data: { value: mockPosition }, @@ -425,29 +414,29 @@ describe.skip('', function () { 'not.be.disabled' ) - cy.get('.synctex-spin-icon') - .should('not.exist') - .then(() => { - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'state-sync-to-code-inflight', - data: { value: true }, - }) + 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 }, }) + }) cy.findByRole('button', { name: /^Go to PDF location in code/ }).should( 'be.disabled' ) - cy.get('.synctex-spin-icon') - .should('have.length', 1) - .then(() => { - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'state-sync-to-code-inflight', - data: { value: false }, - }) + 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 }, }) + }) cy.findByRole('button', { name: /^Go to PDF location in code/ }).should( 'not.be.disabled' diff --git a/services/web/test/frontend/helpers/detach-channel.ts b/services/web/test/frontend/helpers/detach-channel.ts new file mode 100644 index 0000000000..bd140bfa38 --- /dev/null +++ b/services/web/test/frontend/helpers/detach-channel.ts @@ -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) diff --git a/services/web/test/frontend/helpers/render-with-context.js b/services/web/test/frontend/helpers/render-with-context.js index eee4ab782c..d6d1146225 100644 --- a/services/web/test/frontend/helpers/render-with-context.js +++ b/services/web/test/frontend/helpers/render-with-context.js @@ -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 }) => ( - {children} - ) - - return renderHook(hook, { wrapper: EditorProvidersWrapper }) -} - export function ChatProviders({ children, ...props }) { return ( diff --git a/services/web/test/frontend/helpers/sysend.js b/services/web/test/frontend/helpers/sysend.js deleted file mode 100644 index cffa687f21..0000000000 --- a/services/web/test/frontend/helpers/sysend.js +++ /dev/null @@ -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, -} diff --git a/services/web/test/frontend/shared/hooks/use-detach-action.spec.tsx b/services/web/test/frontend/shared/hooks/use-detach-action.spec.tsx new file mode 100644 index 0000000000..567e547e48 --- /dev/null +++ b/services/web/test/frontend/shared/hooks/use-detach-action.spec.tsx @@ -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 ( + + ) +} + +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( + + trigger('foo')} + /> + + ) + + 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( + + trigger('foo')} + /> + + ) + + 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( + + trigger('foo')} + /> + + ) + + 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( + + trigger('foo')} + /> + + ) + + cy.wrap(null).then(() => { + testDetachChannel.postMessage({ + role: 'detached', + event: 'action-some-action', + data: { args: [] }, + }) + }) + + cy.get('@actionFunction').should('not.be.called') + }) +}) diff --git a/services/web/test/frontend/shared/hooks/use-detach-action.test.js b/services/web/test/frontend/shared/hooks/use-detach-action.test.js deleted file mode 100644 index 9c0bdfd955..0000000000 --- a/services/web/test/frontend/shared/hooks/use-detach-action.test.js +++ /dev/null @@ -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) - }) -}) diff --git a/services/web/test/frontend/shared/hooks/use-detach-layout.spec.tsx b/services/web/test/frontend/shared/hooks/use-detach-layout.spec.tsx new file mode 100644 index 0000000000..931da32ac7 --- /dev/null +++ b/services/web/test/frontend/shared/hooks/use-detach-layout.spec.tsx @@ -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 ( +
+ + role: {role || 'none'} + + + + linked + + + + linking + + + + redundant + + + +
+ ) +} + +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( + + + + ) + + 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( + + + + ) + + 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( + + + + ) + + 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( + + + + ) + + 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') + }) +}) diff --git a/services/web/test/frontend/shared/hooks/use-detach-layout.test.js b/services/web/test/frontend/shared/hooks/use-detach-layout.test.js deleted file mode 100644 index 359df11cf4..0000000000 --- a/services/web/test/frontend/shared/hooks/use-detach-layout.test.js +++ /dev/null @@ -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) - }) -} diff --git a/services/web/test/frontend/shared/hooks/use-detach-state.spec.tsx b/services/web/test/frontend/shared/hooks/use-detach-state.spec.tsx new file mode 100644 index 0000000000..429a38c431 --- /dev/null +++ b/services/web/test/frontend/shared/hooks/use-detach-state.spec.tsx @@ -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 ( +
+
{value}
+ +
+ ) +} + +describe('useDetachState', function () { + beforeEach(function () { + window.metaAttributesCache = new Map() + }) + + afterEach(function () { + window.metaAttributesCache = new Map() + }) + + it('create and update state', function () { + cy.mount( + + { + setValue('barbaz') + }} + /> + + ) + + 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( + + { + setValue('barbaz1') + }} + /> + + ) + + 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( + + {}} + /> + + ) + + cy.wrap(null).then(() => { + testDetachChannel.postMessage({ + role: 'detached', + event: 'state-some-key', + data: { value: 'barbaz2' }, + }) + }) + + cy.get('#value').should('have.text', 'barbaz2') + }) +}) diff --git a/services/web/test/frontend/shared/hooks/use-detach-state.test.js b/services/web/test/frontend/shared/hooks/use-detach-state.test.js deleted file mode 100644 index 520bdd6b22..0000000000 --- a/services/web/test/frontend/shared/hooks/use-detach-state.test.js +++ /dev/null @@ -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) - }) -})