diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessController.js b/services/web/app/src/Features/TokenAccess/TokenAccessController.js
index c311929793..e9d9b30077 100644
--- a/services/web/app/src/Features/TokenAccess/TokenAccessController.js
+++ b/services/web/app/src/Features/TokenAccess/TokenAccessController.js
@@ -12,6 +12,7 @@ const {
handleAdminDomainRedirect,
} = require('../Authorization/AuthorizationMiddleware')
const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
+const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const orderedPrivilegeLevels = [
PrivilegeLevels.NONE,
@@ -97,7 +98,18 @@ async function tokenAccessPage(req, res, next) {
}
}
- res.render('project/token/access', {
+ const { variant } = await SplitTestHandler.promises.getAssignment(
+ req,
+ res,
+ 'token-access-page'
+ )
+
+ const view =
+ variant === 'react'
+ ? 'project/token/access-react'
+ : 'project/token/access'
+
+ res.render(view, {
postUrl: makePostUrl(token),
})
} catch (err) {
diff --git a/services/web/app/views/project/token/access-react.pug b/services/web/app/views/project/token/access-react.pug
new file mode 100644
index 0000000000..157d806f60
--- /dev/null
+++ b/services/web/app/views/project/token/access-react.pug
@@ -0,0 +1,16 @@
+extends ../../layout-marketing
+
+block entrypointVar
+ - entrypoint = 'pages/token-access'
+
+block vars
+ - var suppressFooter = true
+ - var suppressCookieBanner = true
+ - var suppressSkipToContent = true
+
+block append meta
+ meta(name="ol-postUrl" data-type="string" content=postUrl)
+ meta(name="ol-user" data-type="json" content=user)
+
+block content
+ div#token-access-page
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 5fc97510da..b47ff57c82 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -22,6 +22,7 @@
"accept_or_reject_each_changes_individually": "",
"accept_terms_and_conditions": "",
"accepted_invite": "",
+ "accepting_invite_as": "",
"access_denied": "",
"account_has_been_link_to_institution_account": "",
"account_has_past_due_invoice_change_plan_warning": "",
@@ -532,6 +533,7 @@
"history_view_all": "",
"history_view_labels": "",
"hit_enter_to_reply": "",
+ "home": "",
"hotkey_add_a_comment": "",
"hotkey_autocomplete_menu": "",
"hotkey_beginning_of_document": "",
@@ -619,6 +621,7 @@
"invite_not_accepted": "",
"invited_to_group": "",
"invited_to_group_have_individual_subcription": "",
+ "invited_to_join": "",
"ip_address": "",
"is_email_affiliated": "",
"issued_on": "",
@@ -1335,6 +1338,7 @@
"to_use_text_wrapping_in_your_table_make_sure_you_include_the_array_package": "",
"toggle_compile_options_menu": "",
"token": "",
+ "token_access_failure": "",
"token_limit_reached": "",
"token_read_only": "",
"token_read_write": "",
diff --git a/services/web/frontend/js/features/token-access/components/access-attempt-screen.tsx b/services/web/frontend/js/features/token-access/components/access-attempt-screen.tsx
new file mode 100644
index 0000000000..0813325bd7
--- /dev/null
+++ b/services/web/frontend/js/features/token-access/components/access-attempt-screen.tsx
@@ -0,0 +1,55 @@
+import { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+
+export const AccessAttemptScreen: FC<{
+ loadingScreenBrandHeight: string
+ inflight: boolean
+ accessError: string | boolean
+}> = ({ loadingScreenBrandHeight, inflight, accessError }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+ {t('join_project')}
+ {inflight && }
+
+
+ {accessError && (
+
+
+
+ {accessError === 'not_found' ? (
+
+
Project not found
+
+ ) : (
+
+ )}
+
+
+ )}
+
+ )
+}
+const LoadingScreenEllipses = () => (
+
+ .
+ .
+ .
+
+)
diff --git a/services/web/frontend/js/features/token-access/components/require-accept-screen.tsx b/services/web/frontend/js/features/token-access/components/require-accept-screen.tsx
new file mode 100644
index 0000000000..1bcdcdcc4d
--- /dev/null
+++ b/services/web/frontend/js/features/token-access/components/require-accept-screen.tsx
@@ -0,0 +1,57 @@
+import { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import getMeta from '@/utils/meta'
+
+export type RequireAcceptData = {
+ projectName?: string
+}
+
+export const RequireAcceptScreen: FC<{
+ requireAcceptData: RequireAcceptData
+ sendPostRequest: (confirmedByUser: boolean) => void
+}> = ({ requireAcceptData, sendPostRequest }) => {
+ const { t } = useTranslation()
+ const user = getMeta('ol-user')
+
+ return (
+
+
+
+
+
+
+
+ {t('invited_to_join')}
+
+ {requireAcceptData.projectName || 'This project'}
+
+
+
+ {user && (
+
+
+
+ {t('accepting_invite_as')} {user.email}
+
+
+
+ )}
+
+
+
+ sendPostRequest(true)}
+ >
+ {t('join_project')}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/token-access/components/token-access-root.tsx b/services/web/frontend/js/features/token-access/components/token-access-root.tsx
new file mode 100644
index 0000000000..faef02760f
--- /dev/null
+++ b/services/web/frontend/js/features/token-access/components/token-access-root.tsx
@@ -0,0 +1,124 @@
+import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
+import withErrorBoundary from '@/infrastructure/error-boundary'
+import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import getMeta from '@/utils/meta'
+import { postJSON } from '@/infrastructure/fetch-json'
+import { debugConsole } from '@/utils/debugging'
+import { useLocation } from '@/shared/hooks/use-location'
+import {
+ V1ImportData,
+ V1ImportDataScreen,
+} from '@/features/token-access/components/v1-import-data-screen'
+import { AccessAttemptScreen } from '@/features/token-access/components/access-attempt-screen'
+import {
+ RequireAcceptData,
+ RequireAcceptScreen,
+} from '@/features/token-access/components/require-accept-screen'
+import Icon from '@/shared/components/icon'
+
+type Mode = 'access-attempt' | 'v1Import' | 'requireAccept'
+
+function TokenAccessRoot() {
+ const [mode, setMode] = useState('access-attempt')
+ const [inflight, setInflight] = useState(false)
+ const [accessError, setAccessError] = useState(false)
+ const [v1ImportData, setV1ImportData] = useState()
+ const [requireAcceptData, setRequireAcceptData] =
+ useState()
+ const [loadingScreenBrandHeight, setLoadingScreenBrandHeight] =
+ useState('0px')
+ const location = useLocation()
+
+ const sendPostRequest = useCallback(
+ (confirmedByUser = false) => {
+ setInflight(true)
+
+ postJSON(getMeta('ol-postUrl'), {
+ body: {
+ confirmedByUser,
+ tokenHashPrefix: document.location.hash,
+ },
+ })
+ .then(async data => {
+ setAccessError(false)
+
+ if (data.redirect) {
+ location.replace(data.redirect)
+ } else if (data.v1Import) {
+ setMode('v1Import')
+ setV1ImportData(data.v1Import)
+ } else if (data.requireAccept) {
+ setMode('requireAccept')
+ setRequireAcceptData(data.requireAccept)
+ } else {
+ debugConsole.warn(
+ 'invalid data from server in success response',
+ data
+ )
+ setAccessError(true)
+ }
+ })
+ .catch(error => {
+ debugConsole.warn('error response from server', error)
+ setAccessError(error.response?.status === 404 ? 'not_found' : 'error')
+ })
+ .finally(() => {
+ setInflight(false)
+ })
+ },
+ [location]
+ )
+
+ const postedRef = useRef(false)
+ useEffect(() => {
+ if (!postedRef.current) {
+ postedRef.current = true
+ sendPostRequest()
+ setTimeout(() => {
+ setLoadingScreenBrandHeight('20%')
+ }, 500)
+ }
+ }, [sendPostRequest])
+
+ const { isReady } = useWaitForI18n()
+
+ if (!isReady) {
+ return null
+ }
+
+ return (
+
+
+
+ {mode === 'access-attempt' && (
+
+ )}
+
+ {mode === 'v1Import' && v1ImportData && (
+
+ )}
+
+ {mode === 'requireAccept' && requireAcceptData && (
+
+ )}
+
+ )
+}
+
+export default withErrorBoundary(TokenAccessRoot, GenericErrorBoundaryFallback)
diff --git a/services/web/frontend/js/features/token-access/components/v1-import-data-screen.tsx b/services/web/frontend/js/features/token-access/components/v1-import-data-screen.tsx
new file mode 100644
index 0000000000..947f041d53
--- /dev/null
+++ b/services/web/frontend/js/features/token-access/components/v1-import-data-screen.tsx
@@ -0,0 +1,84 @@
+import { FC } from 'react'
+
+export type V1ImportData = {
+ name?: string
+ status: string
+ projectId: string
+}
+export const V1ImportDataScreen: FC<{ v1ImportData: V1ImportData }> = ({
+ v1ImportData,
+}) => {
+ return (
+
+
+
+
+
+ {v1ImportData.status === 'mustLogin'
+ ? 'Please log in'
+ : 'Overleaf v1 Project'}
+
+
+
+
+ {v1ImportData.status === 'cannotImport' && (
+
+
+ Cannot Access Overleaf v1 Project
+
+
+
+ Please contact the project owner or{' '}
+ contact support for assistance.
+
+
+ )}
+
+ {v1ImportData.status === 'mustLogin' && (
+
+
+ You will need to log in to access this project.
+
+
+
+
+ )}
+
+ {v1ImportData.status === 'canDownloadZip' && (
+
+
+ {v1ImportData.name || 'This project'} has not
+ yet been moved into the new version of Overleaf. This project
+ was created anonymously and therefore cannot be automatically
+ imported. Please download a zip file of the project and upload
+ that to continue editing it. If you would like to delete this
+ project after you have made a copy, please contact support.
+
+
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/pages/token-access.tsx b/services/web/frontend/js/pages/token-access.tsx
new file mode 100644
index 0000000000..003406f5c3
--- /dev/null
+++ b/services/web/frontend/js/pages/token-access.tsx
@@ -0,0 +1,13 @@
+import 'jquery'
+import 'bootstrap'
+import './../utils/meta'
+import './../utils/webpack-public-path'
+import './../infrastructure/error-reporter'
+import './../i18n'
+import ReactDOM from 'react-dom'
+import TokenAccessRoot from '../features/token-access/components/token-access-root'
+
+const element = document.getElementById('token-access-page')
+if (element) {
+ ReactDOM.render( , element)
+}
diff --git a/services/web/frontend/js/shared/components/location.js b/services/web/frontend/js/shared/components/location.js
index 86dcb397f3..67ba24662d 100644
--- a/services/web/frontend/js/shared/components/location.js
+++ b/services/web/frontend/js/shared/components/location.js
@@ -5,6 +5,10 @@ export const location = {
// eslint-disable-next-line no-restricted-syntax
window.location.assign(url)
},
+ replace(url) {
+ // eslint-disable-next-line no-restricted-syntax
+ window.location.replace(url)
+ },
reload() {
// eslint-disable-next-line no-restricted-syntax
window.location.reload()
diff --git a/services/web/frontend/js/shared/hooks/use-location.ts b/services/web/frontend/js/shared/hooks/use-location.ts
index 12ee581605..3185fa8d06 100644
--- a/services/web/frontend/js/shared/hooks/use-location.ts
+++ b/services/web/frontend/js/shared/hooks/use-location.ts
@@ -14,11 +14,20 @@ export const useLocation = () => {
[isMounted]
)
+ const replace = useCallback(
+ url => {
+ if (isMounted.current) {
+ location.replace(url)
+ }
+ },
+ [isMounted]
+ )
+
const reload = useCallback(() => {
if (isMounted.current) {
location.reload()
}
}, [isMounted])
- return useMemo(() => ({ assign, reload }), [assign, reload])
+ return useMemo(() => ({ assign, replace, reload }), [assign, replace, reload])
}
diff --git a/services/web/test/frontend/components/token-access/token-access-page.spec.tsx b/services/web/test/frontend/components/token-access/token-access-page.spec.tsx
new file mode 100644
index 0000000000..0a9f47e2e9
--- /dev/null
+++ b/services/web/test/frontend/components/token-access/token-access-page.spec.tsx
@@ -0,0 +1,168 @@
+import TokenAccessPage from '@/features/token-access/components/token-access-root'
+import { location } from '@/shared/components/location'
+
+describe(' ', function () {
+ // this is a URL for a read-only token, but the process is the same for read-write tokens
+ const url = '/read/123/grant'
+
+ beforeEach(function () {
+ cy.window().then(win => {
+ win.metaAttributesCache = new Map([
+ ['ol-postUrl', url],
+ ['ol-user', { email: 'test@example.com' }],
+ ])
+ })
+ })
+
+ it('handles a successful token access request', function () {
+ cy.intercept(
+ { method: 'post', url, times: 1 },
+ {
+ body: {
+ requireAccept: { projectName: 'Test Project' },
+ },
+ }
+ ).as('grantRequest')
+
+ cy.mount( )
+
+ cy.wait('@grantRequest').then(interception => {
+ expect(interception.request.body.confirmedByUser).to.be.false
+ })
+
+ cy.get('h1').should(
+ 'have.text',
+ ['You have been invited to join', 'Test Project'].join('')
+ )
+
+ cy.contains('You are accepting this invite as test@example.com')
+
+ cy.intercept(
+ { method: 'post', url, times: 1 },
+ {
+ body: {
+ redirect: '/project/123',
+ },
+ }
+ ).as('confirmedGrantRequest')
+
+ cy.stub(location, 'replace').as('replaceLocation')
+
+ cy.findByRole('button', { name: 'Join Project' }).click()
+
+ cy.wait('@confirmedGrantRequest').then(interception => {
+ expect(interception.request.body.confirmedByUser).to.be.true
+ })
+
+ cy.get('@replaceLocation').should(
+ 'have.been.calledOnceWith',
+ '/project/123'
+ )
+ })
+
+ it('handles a project not found response', function () {
+ cy.intercept({ method: 'post', url, times: 1 }, { statusCode: 404 }).as(
+ 'grantRequest'
+ )
+
+ cy.mount( )
+
+ cy.wait('@grantRequest')
+
+ cy.get('h3').should('have.text', 'Join Project')
+ cy.get('h4').should('have.text', 'Project not found')
+
+ cy.findByRole('button', { name: 'Join Project' }).should('not.exist')
+ })
+
+ it('handles a redirect response', function () {
+ cy.intercept(
+ { method: 'post', url, times: 1 },
+ {
+ body: {
+ redirect: '/restricted',
+ },
+ }
+ ).as('grantRequest')
+
+ cy.stub(location, 'replace').as('replaceLocation')
+
+ cy.mount( )
+
+ cy.wait('@grantRequest')
+
+ cy.get('@replaceLocation').should('have.been.calledOnceWith', '/restricted')
+ })
+
+ it('handles a v1 "must login" response', function () {
+ cy.intercept(
+ { method: 'post', url, times: 1 },
+ {
+ body: {
+ v1Import: { status: 'mustLogin' },
+ },
+ }
+ ).as('grantRequest')
+
+ cy.stub(location, 'replace').as('replaceLocation')
+
+ cy.mount( )
+
+ cy.wait('@grantRequest')
+
+ cy.get('h1').should('have.text', 'Please log in')
+
+ cy.findByRole('link', { name: 'Log in to access project' })
+ .should('have.attr', 'href')
+ .and('match', /^\/login\?redir=/)
+ })
+
+ it('handles a v1 "cannot import" response', function () {
+ cy.intercept(
+ { method: 'post', url, times: 1 },
+ {
+ body: {
+ v1Import: { status: 'cannotImport' },
+ },
+ }
+ ).as('grantRequest')
+
+ cy.stub(location, 'replace').as('replaceLocation')
+
+ cy.mount( )
+
+ cy.wait('@grantRequest')
+
+ cy.get('h1').should('have.text', 'Overleaf v1 Project')
+ cy.get('h2').should('have.text', 'Cannot Access Overleaf v1 Project')
+ })
+
+ it('handles a v1 "can download zip" response', function () {
+ cy.intercept(
+ { method: 'post', url, times: 1 },
+ {
+ body: {
+ v1Import: {
+ status: 'canDownloadZip',
+ projectId: '123',
+ name: 'Test Project',
+ },
+ },
+ }
+ ).as('grantRequest')
+
+ cy.stub(location, 'replace').as('replaceLocation')
+
+ cy.mount( )
+
+ cy.wait('@grantRequest')
+
+ cy.get('h1').should('have.text', 'Overleaf v1 Project')
+
+ cy.findByRole('link', { name: 'Download project zip file' }).should(
+ 'have.attr',
+ 'href',
+ '/overleaf/project/123/download/zip'
+ )
+ })
+})
diff --git a/services/web/test/frontend/features/editor-left-menu/components/actions-copy-project.test.jsx b/services/web/test/frontend/features/editor-left-menu/components/actions-copy-project.test.jsx
index 24710ff5b1..5ec018b489 100644
--- a/services/web/test/frontend/features/editor-left-menu/components/actions-copy-project.test.jsx
+++ b/services/web/test/frontend/features/editor-left-menu/components/actions-copy-project.test.jsx
@@ -14,6 +14,7 @@ describe(' ', function () {
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
+ replace: sinon.stub(),
reload: sinon.stub(),
})
})
diff --git a/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx b/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx
index 720eacf5a6..61504a597a 100644
--- a/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx
+++ b/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx
@@ -12,6 +12,7 @@ describe(' ', function () {
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
+ replace: sinon.stub(),
reload: sinon.stub(),
})
})
diff --git a/services/web/test/frontend/features/project-list/components/notifications.test.tsx b/services/web/test/frontend/features/project-list/components/notifications.test.tsx
index 661afab257..f6ec623ce4 100644
--- a/services/web/test/frontend/features/project-list/components/notifications.test.tsx
+++ b/services/web/test/frontend/features/project-list/components/notifications.test.tsx
@@ -699,6 +699,7 @@ describe(' ', function () {
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
+ replace: sinon.stub(),
reload: sinon.stub(),
})
fetchMock.reset()
diff --git a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx
index b65464315c..eab21bb644 100644
--- a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx
+++ b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx
@@ -57,6 +57,7 @@ describe(' ', function () {
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
+ replace: sinon.stub(),
reload: sinon.stub(),
})
})
diff --git a/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx b/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx
index e736b01ab4..f330f71406 100644
--- a/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx
+++ b/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx
@@ -17,6 +17,7 @@ describe(' ', function () {
assignStub = sinon.stub()
locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
+ replace: sinon.stub(),
reload: sinon.stub(),
})
render(
diff --git a/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/download-project-button.test.tsx b/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/download-project-button.test.tsx
index c75f05f622..cdfca90646 100644
--- a/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/download-project-button.test.tsx
+++ b/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/download-project-button.test.tsx
@@ -12,6 +12,7 @@ describe(' ', function () {
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
+ replace: sinon.stub(),
reload: sinon.stub(),
})
render( )
diff --git a/services/web/test/frontend/features/settings/components/emails/reconfirmation-info.test.tsx b/services/web/test/frontend/features/settings/components/emails/reconfirmation-info.test.tsx
index 2b4d74402c..2ee0653de6 100644
--- a/services/web/test/frontend/features/settings/components/emails/reconfirmation-info.test.tsx
+++ b/services/web/test/frontend/features/settings/components/emails/reconfirmation-info.test.tsx
@@ -35,6 +35,7 @@ describe(' ', function () {
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
+ replace: sinon.stub(),
reload: sinon.stub(),
})
})
diff --git a/services/web/test/frontend/features/settings/components/leave/modal-form.test.tsx b/services/web/test/frontend/features/settings/components/leave/modal-form.test.tsx
index 2ae4f87e18..c391e78d8f 100644
--- a/services/web/test/frontend/features/settings/components/leave/modal-form.test.tsx
+++ b/services/web/test/frontend/features/settings/components/leave/modal-form.test.tsx
@@ -61,6 +61,7 @@ describe(' ', function () {
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
+ replace: sinon.stub(),
reload: sinon.stub(),
})
window.metaAttributesCache.set('ol-ExposedSettings', { isOverleaf: true })
diff --git a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx
index c41efdcc52..254d9997b0 100644
--- a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx
+++ b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx
@@ -88,6 +88,7 @@ describe(' ', function () {
beforeEach(function () {
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
+ replace: sinon.stub(),
reload: sinon.stub(),
})
fetchMock.get('/user/contacts', { contacts })
diff --git a/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx
index 2dc18ebbc5..91f319ee21 100644
--- a/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx
+++ b/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx
@@ -82,6 +82,7 @@ describe(' ', function () {
reloadStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
+ replace: sinon.stub(),
reload: reloadStub,
})
diff --git a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
index 00a0ca4b1e..1f2fe3973d 100644
--- a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
+++ b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
@@ -54,6 +54,7 @@ describe(' ', function () {
reloadStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
+ replace: sinon.stub(),
reload: reloadStub,
})
})
diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx
index 16fcd78e53..f91a117238 100644
--- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx
+++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx
@@ -196,6 +196,7 @@ describe(' ', function () {
beforeEach(function () {
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
+ replace: sinon.stub(),
reload: reloadStub,
})
})
diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx
index 4e0542c6bc..aeaf1046ee 100644
--- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx
+++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx
@@ -31,6 +31,7 @@ describe(' ', function () {
reloadStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: sinon.stub(),
+ replace: sinon.stub(),
reload: reloadStub,
})
})
diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js b/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js
index fd0b9758e9..b731142949 100644
--- a/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js
+++ b/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js
@@ -66,6 +66,12 @@ describe('TokenAccessController', function () {
},
}
+ this.SplitTestHandler = {
+ promises: {
+ getAssignment: sinon.stub().resolves({ variant: 'default' }),
+ },
+ }
+
this.TokenAccessController = SandboxedModule.require(MODULE_PATH, {
requires: {
'@overleaf/settings': this.Settings,
@@ -77,6 +83,7 @@ describe('TokenAccessController', function () {
'../Authorization/AuthorizationMiddleware':
this.AuthorizationMiddleware,
'../Project/ProjectAuditLogHandler': this.ProjectAuditLogHandler,
+ '../SplitTests/SplitTestHandler': this.SplitTestHandler,
'../Errors/Errors': (this.Errors = { NotFoundError: sinon.stub() }),
},
})