mirror of https://github.com/overleaf/overleaf.git
Merge pull request #18170 from overleaf/ae-token-access-page
Convert token access page to React GitOrigin-RevId: d7434f0de395c47a95d00767727fbe9d43f9abca
This commit is contained in:
parent
ab5495023a
commit
9729befe59
|
@ -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) {
|
||||
|
|
|
@ -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
|
|
@ -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": "",
|
||||
|
|
|
@ -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 (
|
||||
<div className="loading-screen">
|
||||
<div className="loading-screen-brand-container">
|
||||
<div
|
||||
className="loading-screen-brand"
|
||||
style={{ height: loadingScreenBrandHeight }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="loading-screen-label text-center">
|
||||
{t('join_project')}
|
||||
{inflight && <LoadingScreenEllipses />}
|
||||
</h3>
|
||||
|
||||
{accessError && (
|
||||
<div className="global-alerts text-center">
|
||||
<div>
|
||||
<br />
|
||||
{accessError === 'not_found' ? (
|
||||
<div>
|
||||
<h4 aria-live="assertive">Project not found</h4>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="alert alert-danger" aria-live="assertive">
|
||||
{t('token_access_failure')}
|
||||
</div>
|
||||
<p>
|
||||
<a href="/">{t('home')}</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const LoadingScreenEllipses = () => (
|
||||
<span aria-hidden>
|
||||
<span className="loading-screen-ellip">.</span>
|
||||
<span className="loading-screen-ellip">.</span>
|
||||
<span className="loading-screen-ellip">.</span>
|
||||
</span>
|
||||
)
|
|
@ -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 (
|
||||
<div className="loading-screen">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-8 col-md-offset-2">
|
||||
<div className="card">
|
||||
<div className="page-header text-centered">
|
||||
<h1>
|
||||
{t('invited_to_join')}
|
||||
<br />
|
||||
<em>{requireAcceptData.projectName || 'This project'}</em>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{user && (
|
||||
<div className="row text-center">
|
||||
<div className="col-md-12">
|
||||
<p>
|
||||
{t('accepting_invite_as')} <em>{user.email}</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row text-center">
|
||||
<div className="col-md-12">
|
||||
<button
|
||||
className="btn btn-lg btn-primary"
|
||||
type="submit"
|
||||
onClick={() => sendPostRequest(true)}
|
||||
>
|
||||
{t('join_project')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -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<Mode>('access-attempt')
|
||||
const [inflight, setInflight] = useState(false)
|
||||
const [accessError, setAccessError] = useState<string | boolean>(false)
|
||||
const [v1ImportData, setV1ImportData] = useState<V1ImportData>()
|
||||
const [requireAcceptData, setRequireAcceptData] =
|
||||
useState<RequireAcceptData>()
|
||||
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 (
|
||||
<div className="full-size">
|
||||
<div>
|
||||
<a
|
||||
href="/project"
|
||||
// TODO: class name
|
||||
style={{ fontSize: '2rem', marginLeft: '1rem', color: '#ddd' }}
|
||||
>
|
||||
<Icon type="arrow-left" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{mode === 'access-attempt' && (
|
||||
<AccessAttemptScreen
|
||||
accessError={accessError}
|
||||
inflight={inflight}
|
||||
loadingScreenBrandHeight={loadingScreenBrandHeight}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === 'v1Import' && v1ImportData && (
|
||||
<V1ImportDataScreen v1ImportData={v1ImportData} />
|
||||
)}
|
||||
|
||||
{mode === 'requireAccept' && requireAcceptData && (
|
||||
<RequireAcceptScreen
|
||||
requireAcceptData={requireAcceptData}
|
||||
sendPostRequest={sendPostRequest}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withErrorBoundary(TokenAccessRoot, GenericErrorBoundaryFallback)
|
|
@ -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 (
|
||||
<div className="loading-screen">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-sm-8 col-sm-offset-2">
|
||||
<h1 className="text-center">
|
||||
{v1ImportData.status === 'mustLogin'
|
||||
? 'Please log in'
|
||||
: 'Overleaf v1 Project'}
|
||||
</h1>
|
||||
|
||||
<img
|
||||
className="v2-import__img"
|
||||
src="/img/v1-import/v2-editor.png"
|
||||
alt="The new V2 editor."
|
||||
/>
|
||||
|
||||
{v1ImportData.status === 'cannotImport' && (
|
||||
<div>
|
||||
<h2 className="text-center">
|
||||
Cannot Access Overleaf v1 Project
|
||||
</h2>
|
||||
|
||||
<p className="text-center row-spaced-small">
|
||||
Please contact the project owner or{' '}
|
||||
<a href="/contact">contact support</a> for assistance.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{v1ImportData.status === 'mustLogin' && (
|
||||
<div>
|
||||
<p className="text-center row-spaced-small">
|
||||
You will need to log in to access this project.
|
||||
</p>
|
||||
|
||||
<div className="row-spaced text-center">
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href={`/login?redir=${encodeURIComponent(document.location.pathname)}`}
|
||||
>
|
||||
Log in to access project
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{v1ImportData.status === 'canDownloadZip' && (
|
||||
<div>
|
||||
<p className="text-center row-spaced-small">
|
||||
<strong>{v1ImportData.name || 'This project'}</strong> 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.
|
||||
</p>
|
||||
|
||||
<div className="row-spaced text-center">
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href={`/overleaf/project/${v1ImportData.projectId}/download/zip`}
|
||||
>
|
||||
Download project zip file
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -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(<TokenAccessRoot />, element)
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
import TokenAccessPage from '@/features/token-access/components/token-access-root'
|
||||
import { location } from '@/shared/components/location'
|
||||
|
||||
describe('<TokenAccessPage/>', 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<string, any>([
|
||||
['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(<TokenAccessPage />)
|
||||
|
||||
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(<TokenAccessPage />)
|
||||
|
||||
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(<TokenAccessPage />)
|
||||
|
||||
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(<TokenAccessPage />)
|
||||
|
||||
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(<TokenAccessPage />)
|
||||
|
||||
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(<TokenAccessPage />)
|
||||
|
||||
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'
|
||||
)
|
||||
})
|
||||
})
|
|
@ -14,6 +14,7 @@ describe('<ActionsCopyProject />', function () {
|
|||
assignStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: assignStub,
|
||||
replace: sinon.stub(),
|
||||
reload: sinon.stub(),
|
||||
})
|
||||
})
|
||||
|
|
|
@ -12,6 +12,7 @@ describe('<ModalContentNewProjectForm />', function () {
|
|||
assignStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: assignStub,
|
||||
replace: sinon.stub(),
|
||||
reload: sinon.stub(),
|
||||
})
|
||||
})
|
||||
|
|
|
@ -699,6 +699,7 @@ describe('<UserNotifications />', function () {
|
|||
assignStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: assignStub,
|
||||
replace: sinon.stub(),
|
||||
reload: sinon.stub(),
|
||||
})
|
||||
fetchMock.reset()
|
||||
|
|
|
@ -57,6 +57,7 @@ describe('<ProjectListRoot />', function () {
|
|||
assignStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: assignStub,
|
||||
replace: sinon.stub(),
|
||||
reload: sinon.stub(),
|
||||
})
|
||||
})
|
||||
|
|
|
@ -17,6 +17,7 @@ describe('<CompileAndDownloadProjectPDFButton />', function () {
|
|||
assignStub = sinon.stub()
|
||||
locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: assignStub,
|
||||
replace: sinon.stub(),
|
||||
reload: sinon.stub(),
|
||||
})
|
||||
render(
|
||||
|
|
|
@ -12,6 +12,7 @@ describe('<DownloadProjectButton />', function () {
|
|||
assignStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: assignStub,
|
||||
replace: sinon.stub(),
|
||||
reload: sinon.stub(),
|
||||
})
|
||||
render(<DownloadProjectButtonTooltip project={projectsData[0]} />)
|
||||
|
|
|
@ -35,6 +35,7 @@ describe('<ReconfirmationInfo/>', function () {
|
|||
assignStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: assignStub,
|
||||
replace: sinon.stub(),
|
||||
reload: sinon.stub(),
|
||||
})
|
||||
})
|
||||
|
|
|
@ -61,6 +61,7 @@ describe('<LeaveModalForm />', 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 })
|
||||
|
|
|
@ -88,6 +88,7 @@ describe('<ShareProjectModal/>', function () {
|
|||
beforeEach(function () {
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: sinon.stub(),
|
||||
replace: sinon.stub(),
|
||||
reload: sinon.stub(),
|
||||
})
|
||||
fetchMock.get('/user/contacts', { contacts })
|
||||
|
|
|
@ -82,6 +82,7 @@ describe('<GroupSubscriptionMemberships />', function () {
|
|||
reloadStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: sinon.stub(),
|
||||
replace: sinon.stub(),
|
||||
reload: reloadStub,
|
||||
})
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ describe('<PersonalSubscription />', function () {
|
|||
reloadStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: sinon.stub(),
|
||||
replace: sinon.stub(),
|
||||
reload: reloadStub,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -196,6 +196,7 @@ describe('<ActiveSubscription />', function () {
|
|||
beforeEach(function () {
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: assignStub,
|
||||
replace: sinon.stub(),
|
||||
reload: reloadStub,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -31,6 +31,7 @@ describe('<ChangePlanModal />', function () {
|
|||
reloadStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
assign: sinon.stub(),
|
||||
replace: sinon.stub(),
|
||||
reload: reloadStub,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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() }),
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue