Add IdeProvider (#4161)

GitOrigin-RevId: cab09354cf4b325a1ea3814a8c4c49fac7c831be
This commit is contained in:
Alf Eaton 2021-06-16 10:32:38 +01:00 committed by Copybot
parent 9d2edb0c45
commit ad3c66b36e
13 changed files with 302 additions and 333 deletions

View file

@ -84,12 +84,11 @@ export default function ShareProjectModal({
show,
animation = true,
isAdmin,
ide,
}) {
const [inFlight, setInFlight] = useState(false)
const [error, setError] = useState()
const [project, setProject] = useScopeValue('project', ide.$scope, true)
const [project, setProject] = useScopeValue('project', true)
// reset error when the modal is opened
useEffect(() => {
@ -167,8 +166,5 @@ ShareProjectModal.propTypes = {
animation: PropTypes.bool,
handleHide: PropTypes.func.isRequired,
isAdmin: PropTypes.bool.isRequired,
ide: PropTypes.shape({
$scope: PropTypes.object.isRequired,
}).isRequired,
show: PropTypes.bool.isRequired,
}

View file

@ -9,8 +9,7 @@ App.component(
'shareProjectModal',
react2angular(
rootContext.use(ShareProjectModal),
Object.keys(ShareProjectModal.propTypes),
['ide']
Object.keys(ShareProjectModal.propTypes)
)
)

View file

@ -13,11 +13,11 @@ CompileContext.Provider.propTypes = {
}),
}
export function CompileProvider({ children, $scope }) {
const [pdfUrl] = useScopeValue('pdf.url', $scope)
const [pdfDownloadUrl] = useScopeValue('pdf.downloadUrl', $scope)
const [logEntries] = useScopeValue('pdf.logEntries', $scope)
const [uncompiled] = useScopeValue('pdf.uncompiled', $scope)
export function CompileProvider({ children }) {
const [pdfUrl] = useScopeValue('pdf.url')
const [pdfDownloadUrl] = useScopeValue('pdf.downloadUrl')
const [logEntries] = useScopeValue('pdf.logEntries')
const [uncompiled] = useScopeValue('pdf.uncompiled')
const value = {
pdfUrl,
@ -27,17 +27,12 @@ export function CompileProvider({ children, $scope }) {
}
return (
<>
<CompileContext.Provider value={value}>
{children}
</CompileContext.Provider>
</>
<CompileContext.Provider value={value}>{children}</CompileContext.Provider>
)
}
CompileProvider.propTypes = {
children: PropTypes.any,
$scope: PropTypes.any.isRequired,
}
export function useCompileContext(propTypes) {

View file

@ -2,6 +2,7 @@ import React, { createContext, useCallback, useContext, useEffect } from 'react'
import PropTypes from 'prop-types'
import useScopeValue from './util/scope-value-hook'
import useBrowserWindow from '../hooks/use-browser-window'
import { useIdeContext } from './ide-context'
export const EditorContext = createContext()
@ -30,7 +31,9 @@ EditorContext.Provider.propTypes = {
}),
}
export function EditorProvider({ children, ide, settings }) {
export function EditorProvider({ children, settings }) {
const ide = useIdeContext()
const cobranding = window.brandVariation
? {
logoImgUrl: window.brandVariation.logo_url,
@ -45,26 +48,12 @@ export function EditorProvider({ children, ide, settings }) {
}
: undefined
const ownerId =
ide.$scope.project && ide.$scope.project.owner
? ide.$scope.project.owner._id
: null
const [loading] = useScopeValue('state.loading', ide.$scope)
const [projectRootDocId] = useScopeValue('project.rootDoc_id', ide.$scope)
const [projectName, setProjectName] = useScopeValue(
'project.name',
ide.$scope
)
const [compileGroup] = useScopeValue(
'project.features.compileGroup',
ide.$scope
)
const [rootFolder] = useScopeValue('rootFolder', ide.$scope)
const [loading] = useScopeValue('state.loading')
const [projectRootDocId] = useScopeValue('project.rootDoc_id')
const [projectName, setProjectName] = useScopeValue('project.name')
const [compileGroup] = useScopeValue('project.features.compileGroup')
const [rootFolder] = useScopeValue('rootFolder')
const [ownerId] = useScopeValue('project.owner.id')
const renameProject = useCallback(
newName => {
@ -112,22 +101,25 @@ export function EditorProvider({ children, ide, settings }) {
}
return (
<>
<EditorContext.Provider value={editorContextValue}>
{children}
</EditorContext.Provider>
</>
<EditorContext.Provider value={editorContextValue}>
{children}
</EditorContext.Provider>
)
}
EditorProvider.propTypes = {
children: PropTypes.any,
ide: PropTypes.any.isRequired,
settings: PropTypes.any.isRequired,
}
export function useEditorContext(propTypes) {
const data = useContext(EditorContext)
PropTypes.checkPropTypes(propTypes, data, 'data', 'EditorContext.Provider')
return data
const context = useContext(EditorContext)
if (!context) {
throw new Error('useEditorContext is only available inside EditorProvider')
}
PropTypes.checkPropTypes(propTypes, context, 'data', 'EditorContext.Provider')
return context
}

View file

@ -0,0 +1,30 @@
import React, { createContext, useContext } from 'react'
import PropTypes from 'prop-types'
const IdeContext = createContext()
IdeContext.Provider.propTypes = {
value: PropTypes.shape({
$scope: PropTypes.object.isRequired,
}),
}
export function useIdeContext() {
const context = useContext(IdeContext)
if (!context) {
throw new Error('useIdeContext is only available inside IdeProvider')
}
return context
}
export function IdeProvider({ ide, children }) {
return <IdeContext.Provider value={ide}>{children}</IdeContext.Provider>
}
IdeProvider.propTypes = {
children: PropTypes.any.isRequired,
ide: PropTypes.shape({
$scope: PropTypes.object.isRequired,
}).isRequired,
}

View file

@ -1,6 +1,7 @@
import React, { createContext, useContext, useCallback } from 'react'
import PropTypes from 'prop-types'
import useScopeValue from './util/scope-value-hook'
import { useIdeContext } from './ide-context'
export const LayoutContext = createContext()
@ -18,8 +19,11 @@ LayoutContext.Provider.propTypes = {
}).isRequired,
}
export function LayoutProvider({ children, $scope }) {
const [view, _setView] = useScopeValue('ui.view', $scope)
export function LayoutProvider({ children }) {
const { $scope } = useIdeContext()
const [view, _setView] = useScopeValue('ui.view')
const setView = useCallback(
value => {
_setView(value)
@ -30,19 +34,14 @@ export function LayoutProvider({ children, $scope }) {
[$scope, _setView]
)
const [chatIsOpen, setChatIsOpen] = useScopeValue('ui.chatOpen', $scope)
const [chatIsOpen, setChatIsOpen] = useScopeValue('ui.chatOpen')
const [reviewPanelOpen, setReviewPanelOpen] = useScopeValue(
'ui.reviewPanelOpen',
$scope
)
const [leftMenuShown, setLeftMenuShown] = useScopeValue(
'ui.leftMenuShown',
$scope
'ui.reviewPanelOpen'
)
const [leftMenuShown, setLeftMenuShown] = useScopeValue('ui.leftMenuShown')
const [pdfLayout] = useScopeValue('ui.pdfLayout')
const [pdfLayout] = useScopeValue('ui.pdfLayout', $scope)
const layoutContextValue = {
const value = {
view,
setView,
chatIsOpen,
@ -55,17 +54,12 @@ export function LayoutProvider({ children, $scope }) {
}
return (
<LayoutContext.Provider value={layoutContextValue}>
{children}
</LayoutContext.Provider>
<LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>
)
}
LayoutProvider.propTypes = {
children: PropTypes.any,
$scope: PropTypes.shape({
toggleHistory: PropTypes.func.isRequired,
}).isRequired,
}
export function useLayoutContext(propTypes) {

View file

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import createSharedContext from 'react2angular-shared-context'
import { ApplicationProvider } from './application-context'
import { IdeProvider } from './ide-context'
import { EditorProvider } from './editor-context'
import { CompileProvider } from './compile-context'
import { LayoutProvider } from './layout-context'
@ -13,17 +14,19 @@ export function ContextRoot({ children, ide, settings }) {
return (
<ApplicationProvider>
<EditorProvider ide={ide} settings={settings}>
<CompileProvider $scope={ide.$scope}>
<LayoutProvider $scope={ide.$scope}>
{isAnonymousUser ? (
children
) : (
<ChatProvider>{children}</ChatProvider>
)}
</LayoutProvider>
</CompileProvider>
</EditorProvider>
<IdeProvider ide={ide}>
<EditorProvider settings={settings}>
<CompileProvider>
<LayoutProvider>
{isAnonymousUser ? (
children
) : (
<ChatProvider>{children}</ChatProvider>
)}
</LayoutProvider>
</CompileProvider>
</EditorProvider>
</IdeProvider>
</ApplicationProvider>
)
}

View file

@ -1,17 +1,22 @@
import { useCallback, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import { useIdeContext } from '../ide-context'
/**
* Binds a property in an Angular scope making it accessible in a React
* component. The interface is compatible with React.useState(), including
* the option of passing a function to the setter.
*
* @param {string} path - dot '.' path of a property in `sourceScope`.
* @param {object} $scope - Angular $scope containing the value to bind.
* @param {string} path - dot '.' path of a property in the Angular scope.
* @param {boolean} deep
* @returns {[any, function]} - Binded value and setter function tuple.
*/
export default function useScopeValue(path, $scope, deep = false) {
export default function useScopeValue(path, deep = false) {
const { $scope } = useIdeContext({
$scope: PropTypes.object.isRequired,
})
const [value, setValue] = useState(() => _.get($scope, path))
useEffect(() => {

View file

@ -3,6 +3,7 @@ import PreviewLogsPane from '../js/features/preview/components/preview-logs-pane
import { EditorProvider } from '../js/shared/context/editor-context'
import { ApplicationProvider } from '../js/shared/context/application-context'
import useFetchMock from './hooks/use-fetch-mock'
import { IdeProvider } from '../js/shared/context/ide-context'
export const TimedOutError = args => {
useFetchMock(fetchMock => {
@ -25,9 +26,11 @@ export const TimedOutError = args => {
return (
<ApplicationProvider>
<EditorProvider ide={ide} settings={{}}>
<PreviewLogsPane {...args} />
</EditorProvider>
<IdeProvider ide={ide}>
<EditorProvider settings={{}}>
<PreviewLogsPane {...args} />
</EditorProvider>
</IdeProvider>
</ApplicationProvider>
)
}
@ -58,9 +61,11 @@ export const TimedOutErrorWithPriorityCompile = args => {
return (
<ApplicationProvider>
<EditorProvider ide={ide} settings={{}}>
<PreviewLogsPane {...args} />
</EditorProvider>
<IdeProvider ide={ide}>
<EditorProvider settings={{}}>
<PreviewLogsPane {...args} />
</EditorProvider>
</IdeProvider>
</ApplicationProvider>
)
}

View file

@ -1,7 +1,7 @@
import React, { useEffect } from 'react'
import ShareProjectModal from '../js/features/share-project-modal/components/share-project-modal'
import { ContextRoot } from '../js/shared/context/root-context'
import useFetchMock from './hooks/use-fetch-mock'
import { withContextRoot } from './utils/with-context-root'
export const LinkSharingOff = args => {
useFetchMock(setupFetchMock)
@ -11,9 +11,7 @@ export const LinkSharingOff = args => {
publicAccesLevel: 'private',
}
return renderWithContext(
<ShareProjectModal {...args} ide={ideWithProject(project)} />
)
return withContextRoot(<ShareProjectModal {...args} />, { project })
}
export const LinkSharingOn = args => {
@ -24,9 +22,7 @@ export const LinkSharingOn = args => {
publicAccesLevel: 'tokenBased',
}
return renderWithContext(
<ShareProjectModal {...args} ide={ideWithProject(project)} />
)
return withContextRoot(<ShareProjectModal {...args} />, { project })
}
export const LinkSharingLoading = args => {
@ -38,9 +34,7 @@ export const LinkSharingLoading = args => {
tokens: undefined,
}
return renderWithContext(
<ShareProjectModal {...args} ide={ideWithProject(project)} />
)
return withContextRoot(<ShareProjectModal {...args} />, { project })
}
export const NonAdminLinkSharingOff = args => {
@ -49,13 +43,9 @@ export const NonAdminLinkSharingOff = args => {
publicAccesLevel: 'private',
}
return renderWithContext(
<ShareProjectModal
{...args}
isAdmin={false}
ide={ideWithProject(project)}
/>
)
return withContextRoot(<ShareProjectModal {...args} isAdmin={false} />, {
project,
})
}
export const NonAdminLinkSharingOn = args => {
@ -64,13 +54,9 @@ export const NonAdminLinkSharingOn = args => {
publicAccesLevel: 'tokenBased',
}
return renderWithContext(
<ShareProjectModal
{...args}
isAdmin={false}
ide={ideWithProject(project)}
/>
)
return withContextRoot(<ShareProjectModal {...args} isAdmin={false} />, {
project,
})
}
export const RestrictedTokenMember = args => {
@ -91,9 +77,7 @@ export const RestrictedTokenMember = args => {
publicAccesLevel: 'tokenBased',
}
return renderWithContext(
<ShareProjectModal {...args} ide={ideWithProject(project)} />
)
return withContextRoot(<ShareProjectModal {...args} />, { project })
}
export const LegacyLinkSharingReadAndWrite = args => {
@ -104,9 +88,7 @@ export const LegacyLinkSharingReadAndWrite = args => {
publicAccesLevel: 'readAndWrite',
}
return renderWithContext(
<ShareProjectModal {...args} ide={ideWithProject(project)} />
)
return withContextRoot(<ShareProjectModal {...args} />, { project })
}
export const LegacyLinkSharingReadOnly = args => {
@ -117,9 +99,7 @@ export const LegacyLinkSharingReadOnly = args => {
publicAccesLevel: 'readOnly',
}
return renderWithContext(
<ShareProjectModal {...args} ide={ideWithProject(project)} />
)
return withContextRoot(<ShareProjectModal {...args} />, { project })
}
export const LimitedCollaborators = args => {
@ -133,9 +113,7 @@ export const LimitedCollaborators = args => {
},
}
return renderWithContext(
<ShareProjectModal {...args} ide={ideWithProject(project)} />
)
return withContextRoot(<ShareProjectModal {...args} />, { project })
}
const project = {
@ -199,18 +177,6 @@ export default {
},
}
// Unfortunately, we cannot currently use decorators here, since we need to
// set a value on window, before the contexts are rendered.
// When using decorators, the contexts are rendered before the story, so we
// don't have the opportunity to set the window value first.
function renderWithContext(Story) {
return (
<ContextRoot ide={window._ide} settings={{}}>
{Story}
</ContextRoot>
)
}
const contacts = [
// user with edited name
{
@ -282,13 +248,3 @@ function setupFetchMock(fetchMock) {
// send analytics event
.post('express:/event/:key', 200)
}
function ideWithProject(project) {
return {
$scope: {
$watch: () => () => {},
$applyAsync: () => {},
project,
},
}
}

View file

@ -0,0 +1,22 @@
import React from 'react'
import { ContextRoot } from '../../js/shared/context/root-context'
// Unfortunately, we cannot currently use decorators here, since we need to
// set a value on window, before the contexts are rendered.
// When using decorators, the contexts are rendered before the story, so we
// don't have the opportunity to set the window value first.
export function withContextRoot(Story, scope) {
const ide = {
...window._ide,
$scope: {
...window._ide.$scope,
...scope,
},
}
return (
<ContextRoot ide={ide} settings={{}}>
{Story}
</ContextRoot>
)
}

View file

@ -4,16 +4,17 @@ import React from 'react'
import {
screen,
fireEvent,
render,
waitFor,
waitForElementToBeRemoved,
} from '@testing-library/react'
import fetchMock from 'fetch-mock'
import { get } from 'lodash'
import ShareProjectModal from '../../../../../frontend/js/features/share-project-modal/components/share-project-modal'
import {
renderWithEditorContext,
cleanUpContext,
EditorProviders,
} from '../../../helpers/render-with-context'
import * as locationModule from '../../../../../frontend/js/features/share-project-modal/utils/location'
@ -73,23 +74,7 @@ describe('<ShareProjectModal/>', function () {
},
]
const ideWithProject = project => {
const scope = { project }
return {
$scope: {
$watch: (path, callback) => {
callback(get(scope, path))
return () => null
},
$applyAsync: () => {},
...scope,
},
}
}
const modalProps = {
ide: ideWithProject(project),
show: true,
isAdmin: true,
handleHide: sinon.stub(),
@ -105,7 +90,9 @@ describe('<ShareProjectModal/>', function () {
})
it('renders the modal', async function () {
renderWithEditorContext(<ShareProjectModal {...modalProps} />)
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: { project },
})
await screen.findByText('Share Project')
})
@ -114,7 +101,8 @@ describe('<ShareProjectModal/>', function () {
const handleHide = sinon.stub()
renderWithEditorContext(
<ShareProjectModal {...modalProps} handleHide={handleHide} />
<ShareProjectModal {...modalProps} handleHide={handleHide} />,
{ scope: { project } }
)
const [
@ -129,12 +117,9 @@ describe('<ShareProjectModal/>', function () {
})
it('handles access level "private"', async function () {
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({ ...project, publicAccesLevel: 'private' })}
/>
)
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: { project: { ...project, publicAccesLevel: 'private' } },
})
await screen.findByText(
'Link sharing is off, only invited users can view this project.'
@ -148,12 +133,9 @@ describe('<ShareProjectModal/>', function () {
})
it('handles access level "tokenBased"', async function () {
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({ ...project, publicAccesLevel: 'tokenBased' })}
/>
)
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: { project: { ...project, publicAccesLevel: 'tokenBased' } },
})
await screen.findByText('Link sharing is on')
await screen.findByRole('button', { name: 'Turn off link sharing' })
@ -165,12 +147,9 @@ describe('<ShareProjectModal/>', function () {
})
it('handles legacy access level "readAndWrite"', async function () {
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({ ...project, publicAccesLevel: 'readAndWrite' })}
/>
)
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: { project: { ...project, publicAccesLevel: 'readAndWrite' } },
})
await screen.findByText(
'This project is public and can be edited by anyone with the URL.'
@ -179,12 +158,9 @@ describe('<ShareProjectModal/>', function () {
})
it('handles legacy access level "readOnly"', async function () {
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({ ...project, publicAccesLevel: 'readOnly' })}
/>
)
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: { project: { ...project, publicAccesLevel: 'readOnly' } },
})
await screen.findByText(
'This project is public and can be viewed but not edited by anyone with the URL'
@ -202,16 +178,18 @@ describe('<ShareProjectModal/>', function () {
]
// render as admin: actions should be present
const { rerender } = renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
...project,
invites,
publicAccesLevel: 'tokenBased',
})}
isAdmin
/>
const { rerender } = render(
<EditorProviders
scope={{
project: {
...project,
invites,
publicAccesLevel: 'tokenBased',
},
}}
>
<ShareProjectModal {...modalProps} isAdmin />
</EditorProviders>
)
await screen.findByRole('button', { name: 'Turn off link sharing' })
@ -219,15 +197,17 @@ describe('<ShareProjectModal/>', function () {
// render as non-admin (non-owner), link sharing on: actions should be missing and message should be present
rerender(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
...project,
invites,
publicAccesLevel: 'tokenBased',
})}
isAdmin={false}
/>
<EditorProviders
scope={{
project: {
...project,
invites,
publicAccesLevel: 'tokenBased',
},
}}
>
<ShareProjectModal {...modalProps} isAdmin={false} />
</EditorProviders>
)
await screen.findByText(
@ -242,15 +222,17 @@ describe('<ShareProjectModal/>', function () {
// render as non-admin (non-owner), link sharing off: actions should be missing and message should be present
rerender(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
...project,
invites,
publicAccesLevel: 'private',
})}
isAdmin={false}
/>
<EditorProviders
scope={{
project: {
...project,
invites,
publicAccesLevel: 'private',
},
}}
>
<ShareProjectModal {...modalProps} isAdmin={false} />
</EditorProviders>
)
await screen.findByText(
@ -265,13 +247,10 @@ describe('<ShareProjectModal/>', function () {
})
it('only shows read-only token link to restricted token members', async function () {
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({ ...project, publicAccesLevel: 'tokenBased' })}
/>,
{ isRestrictedTokenMember: true }
)
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
isRestrictedTokenMember: true,
scope: { project: { ...project, publicAccesLevel: 'tokenBased' } },
})
// no buttons
expect(screen.queryByRole('button', { name: 'Turn on link sharing' })).to.be
@ -312,17 +291,16 @@ describe('<ShareProjectModal/>', function () {
},
]
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
...project,
members,
invites,
publicAccesLevel: 'tokenBased',
})}
/>
)
},
},
})
expect(screen.queryAllByText('project-owner@example.com')).to.have.length(1)
expect(screen.queryAllByText('member-author@example.com')).to.have.length(1)
@ -356,16 +334,15 @@ describe('<ShareProjectModal/>', function () {
},
]
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
...project,
invites,
publicAccesLevel: 'tokenBased',
})}
/>
)
},
},
})
const [, closeButton] = screen.getAllByRole('button', {
name: 'Close',
@ -391,16 +368,15 @@ describe('<ShareProjectModal/>', function () {
},
]
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
...project,
invites,
publicAccesLevel: 'tokenBased',
})}
/>
)
},
},
})
const [, closeButton] = screen.getAllByRole('button', {
name: 'Close',
@ -425,16 +401,15 @@ describe('<ShareProjectModal/>', function () {
},
]
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
...project,
members,
publicAccesLevel: 'tokenBased',
})}
/>
)
},
},
})
const [, closeButton] = await screen.getAllByRole('button', {
name: 'Close',
@ -468,16 +443,15 @@ describe('<ShareProjectModal/>', function () {
},
]
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
...project,
members,
publicAccesLevel: 'tokenBased',
})}
/>
)
},
},
})
expect(screen.queryAllByText('member-viewer@example.com')).to.have.length(1)
@ -508,16 +482,15 @@ describe('<ShareProjectModal/>', function () {
},
]
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
...project,
members,
publicAccesLevel: 'tokenBased',
})}
/>
)
},
},
})
expect(screen.queryAllByText('member-viewer@example.com')).to.have.length(1)
@ -551,15 +524,14 @@ describe('<ShareProjectModal/>', function () {
})
it('sends invites to input email addresses', async function () {
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
...project,
publicAccesLevel: 'tokenBased',
})}
/>
)
},
},
})
const [inputElement] = await screen.findAllByLabelText(
'Share with your collaborators'
@ -641,24 +613,21 @@ describe('<ShareProjectModal/>', function () {
it('displays a message when the collaborator limit is reached', async function () {
fetchMock.post('/event/project-sharing-paywall-prompt', {})
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
user: {
id: '123abd',
allowedFreeTrial: true,
},
scope: {
project: {
...project,
publicAccesLevel: 'tokenBased',
features: {
collaborators: 0,
},
})}
/>,
{
user: {
id: '123abd',
allowedFreeTrial: true,
},
}
)
},
})
expect(screen.queryByLabelText('Share with your collaborators')).to.be.null
@ -668,15 +637,14 @@ describe('<ShareProjectModal/>', function () {
})
it('handles server error responses', async function () {
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: {
project: {
...project,
publicAccesLevel: 'tokenBased',
})}
/>
)
},
},
})
// loading contacts
await waitFor(() => {
@ -738,25 +706,19 @@ describe('<ShareProjectModal/>', function () {
const watchCallbacks = {}
const ideWithProject = project => {
const scopeWithProject = project => {
return {
$scope: {
$watch: (path, callback, deep) => {
watchCallbacks[path] = callback
return () => {}
},
$applyAsync: () => {},
project,
$watch: (path, callback) => {
watchCallbacks[path] = callback
return () => {}
},
project,
}
}
renderWithEditorContext(
<ShareProjectModal
{...modalProps}
ide={ideWithProject({ ...project, publicAccesLevel: 'private' })}
/>
)
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: scopeWithProject({ ...project, publicAccesLevel: 'private' }),
})
await screen.findByText(
'Link sharing is off, only invited users can view this project.'
@ -799,7 +761,9 @@ describe('<ShareProjectModal/>', function () {
})
it('avoids selecting unmatched contact', async function () {
renderWithEditorContext(<ShareProjectModal {...modalProps} />)
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
scope: { project },
})
const [inputElement] = await screen.findAllByLabelText(
'Share with your collaborators'

View file

@ -8,6 +8,8 @@ import { ApplicationProvider } from '../../../frontend/js/shared/context/applica
import { EditorProvider } from '../../../frontend/js/shared/context/editor-context'
import { LayoutProvider } from '../../../frontend/js/shared/context/layout-context'
import { ChatProvider } from '../../../frontend/js/features/chat/context/chat-context'
import { IdeProvider } from '../../../frontend/js/shared/context/ide-context'
import { get } from 'lodash'
export function EditorProviders({
user = { id: '123abd' },
@ -17,6 +19,7 @@ export function EditorProviders({
removeListener: sinon.stub(),
},
isRestrictedTokenMember = false,
scope,
children,
}) {
window.user = user || window.user
@ -24,38 +27,44 @@ export function EditorProviders({
window.project_id = projectId != null ? projectId : window.project_id
window.isRestrictedTokenMember = isRestrictedTokenMember
window._ide = {
$scope: {
project: {
owner: {
_id: '124abd',
},
const $scope = {
project: {
owner: {
_id: '124abd',
},
ui: {
chatOpen: true,
pdfLayout: 'flat',
},
$watch: () => {},
toggleHistory: () => {},
},
socket,
ui: {
chatOpen: true,
pdfLayout: 'flat',
},
$watch: (path, callback) => {
callback(get($scope, path))
return () => null
},
$applyAsync: () => {},
toggleHistory: () => {},
...scope,
}
window._ide = { $scope, socket }
return (
<ApplicationProvider>
<EditorProvider ide={window._ide} settings={{}}>
<LayoutProvider $scope={window._ide.$scope}>{children}</LayoutProvider>
</EditorProvider>
<IdeProvider ide={window._ide}>
<EditorProvider settings={{}}>
<LayoutProvider>{children}</LayoutProvider>
</EditorProvider>
</IdeProvider>
</ApplicationProvider>
)
}
export function renderWithEditorContext(component, contextProps) {
return render(component, {
// eslint-disable-next-line react/display-name
wrapper: ({ children }) => (
<EditorProviders {...contextProps}>{children}</EditorProviders>
),
})
const EditorProvidersWrapper = ({ children }) => (
<EditorProviders {...contextProps}>{children}</EditorProviders>
)
return render(component, { wrapper: EditorProvidersWrapper })
}
export function ChatProviders({ children, ...props }) {
@ -67,12 +76,11 @@ export function ChatProviders({ children, ...props }) {
}
export function renderWithChatContext(component, props) {
return render(component, {
// eslint-disable-next-line react/display-name
wrapper: ({ children }) => (
<ChatProviders {...props}>{children}</ChatProviders>
),
})
const ChatProvidersWrapper = ({ children }) => (
<ChatProviders {...props}>{children}</ChatProviders>
)
return render(component, { wrapper: ChatProvidersWrapper })
}
export function cleanUpContext() {