@sanity/sdk-react 0.0.0-alpha.21 → 0.0.0-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +502 -3460
- package/dist/index.js +400 -465
- package/dist/index.js.map +1 -1
- package/package.json +17 -15
- package/src/_exports/index.ts +4 -5
- package/src/components/SDKProvider.test.tsx +78 -54
- package/src/components/SDKProvider.tsx +31 -26
- package/src/components/SanityApp.test.tsx +121 -15
- package/src/components/SanityApp.tsx +26 -15
- package/src/components/auth/AuthBoundary.test.tsx +32 -14
- package/src/components/auth/AuthBoundary.tsx +53 -23
- package/src/components/auth/LoginCallback.test.tsx +19 -6
- package/src/components/auth/LoginCallback.tsx +2 -11
- package/src/components/auth/LoginError.test.tsx +12 -4
- package/src/components/auth/LoginError.tsx +13 -21
- package/src/components/auth/LoginFooter.test.tsx +7 -3
- package/src/context/ResourceProvider.test.tsx +157 -0
- package/src/context/ResourceProvider.tsx +111 -0
- package/src/context/SanityInstanceContext.ts +1 -1
- package/src/hooks/auth/useLoginUrl.tsx +14 -0
- package/src/hooks/client/useClient.ts +2 -1
- package/src/hooks/comlink/useManageFavorite.test.ts +16 -8
- package/src/hooks/comlink/useManageFavorite.ts +37 -13
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +8 -4
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +10 -8
- package/src/hooks/context/useSanityInstance.test.tsx +157 -15
- package/src/hooks/context/useSanityInstance.ts +66 -26
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +13 -31
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +12 -15
- package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.test.tsx → useStudioWorkspacesByProjectIdDataset.test.tsx} +13 -13
- package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.ts → useStudioWorkspacesByProjectIdDataset.ts} +10 -9
- package/src/hooks/datasets/useDatasets.ts +15 -4
- package/src/hooks/document/useApplyDocumentActions.test.ts +4 -9
- package/src/hooks/document/useApplyDocumentActions.ts +6 -31
- package/src/hooks/document/useDocument.test.ts +2 -2
- package/src/hooks/document/useDocument.ts +40 -19
- package/src/hooks/document/useDocumentEvent.test.ts +2 -3
- package/src/hooks/document/useDocumentEvent.ts +7 -11
- package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
- package/src/hooks/document/useDocumentPermissions.ts +31 -23
- package/src/hooks/document/useDocumentSyncStatus.ts +5 -4
- package/src/hooks/document/useEditDocument.test.ts +2 -3
- package/src/hooks/document/useEditDocument.ts +43 -29
- package/src/hooks/documents/useDocuments.test.tsx +30 -3
- package/src/hooks/documents/useDocuments.ts +20 -7
- package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
- package/src/hooks/helpers/createCallbackHook.tsx +2 -3
- package/src/hooks/helpers/createStateSourceHook.test.tsx +1 -1
- package/src/hooks/helpers/createStateSourceHook.tsx +5 -8
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +43 -18
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +36 -50
- package/src/hooks/preview/usePreview.test.tsx +66 -7
- package/src/hooks/preview/usePreview.tsx +17 -12
- package/src/hooks/projection/useProjection.test.tsx +68 -3
- package/src/hooks/projection/useProjection.ts +21 -24
- package/src/hooks/projects/useProject.ts +7 -4
- package/src/hooks/query/useQuery.ts +32 -14
- package/src/hooks/users/useUsers.test.tsx +330 -0
- package/src/hooks/users/useUsers.ts +65 -52
- package/src/components/Login/LoginLinks.test.tsx +0 -90
- package/src/components/Login/LoginLinks.tsx +0 -58
- package/src/components/auth/Login.test.tsx +0 -27
- package/src/components/auth/Login.tsx +0 -39
- package/src/components/auth/LoginLayout.test.tsx +0 -19
- package/src/components/auth/LoginLayout.tsx +0 -69
- package/src/components/auth/authTestHelpers.tsx +0 -11
- package/src/context/SanityProvider.test.tsx +0 -25
- package/src/context/SanityProvider.tsx +0 -50
- package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
- package/src/hooks/auth/useLoginUrls.tsx +0 -52
- package/src/hooks/users/useUsers.test.ts +0 -163
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {AuthStateType} from '@sanity/sdk'
|
|
2
|
-
import {
|
|
3
|
-
import {screen, waitFor} from '@testing-library/react'
|
|
2
|
+
import {render, screen, waitFor} from '@testing-library/react'
|
|
4
3
|
import {beforeEach, describe, expect, it, type MockInstance, vi} from 'vitest'
|
|
5
4
|
|
|
5
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
6
|
+
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
6
7
|
import {AuthBoundary} from './AuthBoundary'
|
|
7
|
-
import {renderWithWrappers} from './authTestHelpers'
|
|
8
8
|
|
|
9
9
|
// Mock hooks
|
|
10
10
|
vi.mock('../../hooks/auth/useAuthState', () => ({
|
|
@@ -46,27 +46,37 @@ describe('AuthBoundary', () => {
|
|
|
46
46
|
consoleErrorSpy?.mockRestore()
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
it('
|
|
49
|
+
it.skip('redirects to the sanity.io/login url when authState="logged-out"', async () => {
|
|
50
50
|
vi.mocked(useAuthState).mockReturnValue({
|
|
51
51
|
type: AuthStateType.LOGGED_OUT,
|
|
52
52
|
isDestroyingSession: false,
|
|
53
53
|
})
|
|
54
|
-
|
|
54
|
+
render(
|
|
55
|
+
<ResourceProvider fallback={null}>
|
|
56
|
+
<AuthBoundary>Protected Content</AuthBoundary>
|
|
57
|
+
</ResourceProvider>,
|
|
58
|
+
)
|
|
55
59
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
// Wait for the redirect to happen
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(window.location.href).toBe('https://sanity.io/login')
|
|
63
|
+
})
|
|
59
64
|
})
|
|
60
65
|
|
|
61
|
-
it('renders the LoginCallback component when authState="logging-in"', () => {
|
|
66
|
+
it('renders the empty LoginCallback component when authState="logging-in"', () => {
|
|
62
67
|
vi.mocked(useAuthState).mockReturnValue({
|
|
63
68
|
type: AuthStateType.LOGGING_IN,
|
|
64
69
|
isExchangingToken: false,
|
|
65
70
|
})
|
|
66
|
-
|
|
71
|
+
const {container} = render(
|
|
72
|
+
<ResourceProvider fallback={null}>
|
|
73
|
+
<AuthBoundary>Protected Content</AuthBoundary>
|
|
74
|
+
</ResourceProvider>,
|
|
75
|
+
)
|
|
67
76
|
|
|
68
|
-
// The callback screen
|
|
69
|
-
expect(
|
|
77
|
+
// The callback screen renders null check that it renders nothing
|
|
78
|
+
expect(container.innerHTML).toBe('')
|
|
79
|
+
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument()
|
|
70
80
|
})
|
|
71
81
|
|
|
72
82
|
it('renders children when authState="logged-in"', () => {
|
|
@@ -75,7 +85,11 @@ describe('AuthBoundary', () => {
|
|
|
75
85
|
currentUser: null,
|
|
76
86
|
token: 'exampleToken',
|
|
77
87
|
})
|
|
78
|
-
|
|
88
|
+
render(
|
|
89
|
+
<ResourceProvider fallback={null}>
|
|
90
|
+
<AuthBoundary>Protected Content</AuthBoundary>
|
|
91
|
+
</ResourceProvider>,
|
|
92
|
+
)
|
|
79
93
|
|
|
80
94
|
expect(screen.getByText('Protected Content')).toBeInTheDocument()
|
|
81
95
|
})
|
|
@@ -85,7 +99,11 @@ describe('AuthBoundary', () => {
|
|
|
85
99
|
type: AuthStateType.ERROR,
|
|
86
100
|
error: new Error('test error'),
|
|
87
101
|
})
|
|
88
|
-
|
|
102
|
+
render(
|
|
103
|
+
<ResourceProvider fallback={null}>
|
|
104
|
+
<AuthBoundary>Protected Content</AuthBoundary>
|
|
105
|
+
</ResourceProvider>,
|
|
106
|
+
)
|
|
89
107
|
|
|
90
108
|
// The AuthBoundary should throw an AuthError internally
|
|
91
109
|
// and then display the LoginError component as the fallback.
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import {AuthStateType} from '@sanity/sdk'
|
|
2
|
-
import {useMemo} from 'react'
|
|
2
|
+
import {useEffect, useMemo} from 'react'
|
|
3
3
|
import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
|
|
4
4
|
|
|
5
5
|
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
6
|
+
import {useLoginUrl} from '../../hooks/auth/useLoginUrl'
|
|
6
7
|
import {isInIframe} from '../utils'
|
|
7
8
|
import {AuthError} from './AuthError'
|
|
8
|
-
import {Login} from './Login'
|
|
9
9
|
import {LoginCallback} from './LoginCallback'
|
|
10
10
|
import {LoginError, type LoginErrorProps} from './LoginError'
|
|
11
|
-
import {type LoginLayoutProps} from './LoginLayout'
|
|
12
11
|
|
|
13
12
|
// Only import bridge if we're in an iframe. This assumes that the app is
|
|
14
13
|
// running within SanityOS if it is in an iframe.
|
|
@@ -26,27 +25,42 @@ if (isInIframe()) {
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
|
-
* @
|
|
28
|
+
* @public
|
|
30
29
|
*/
|
|
31
|
-
interface AuthBoundaryProps
|
|
30
|
+
export interface AuthBoundaryProps {
|
|
32
31
|
/**
|
|
33
32
|
* Custom component to render the login screen.
|
|
34
|
-
* Receives all
|
|
33
|
+
* Receives all props. Defaults to {@link Login}.
|
|
35
34
|
*/
|
|
36
|
-
LoginComponent?: React.ComponentType<
|
|
35
|
+
LoginComponent?: React.ComponentType<{
|
|
36
|
+
header?: React.ReactNode
|
|
37
|
+
footer?: React.ReactNode
|
|
38
|
+
}>
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
41
|
* Custom component to render during OAuth callback processing.
|
|
40
|
-
* Receives all
|
|
42
|
+
* Receives all props. Defaults to {@link LoginCallback}.
|
|
41
43
|
*/
|
|
42
|
-
CallbackComponent?: React.ComponentType<
|
|
44
|
+
CallbackComponent?: React.ComponentType<{
|
|
45
|
+
header?: React.ReactNode
|
|
46
|
+
footer?: React.ReactNode
|
|
47
|
+
}>
|
|
43
48
|
|
|
44
49
|
/**
|
|
45
50
|
* Custom component to render when authentication errors occur.
|
|
46
|
-
* Receives
|
|
51
|
+
* Receives error boundary props and layout props. Defaults to
|
|
47
52
|
* {@link LoginError}
|
|
48
53
|
*/
|
|
49
54
|
LoginErrorComponent?: React.ComponentType<LoginErrorProps>
|
|
55
|
+
|
|
56
|
+
/** Header content to display */
|
|
57
|
+
header?: React.ReactNode
|
|
58
|
+
|
|
59
|
+
/** Footer content to display */
|
|
60
|
+
footer?: React.ReactNode
|
|
61
|
+
|
|
62
|
+
/** Protected content to render when authenticated */
|
|
63
|
+
children?: React.ReactNode
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
/**
|
|
@@ -74,12 +88,11 @@ export function AuthBoundary({
|
|
|
74
88
|
LoginErrorComponent = LoginError,
|
|
75
89
|
...props
|
|
76
90
|
}: AuthBoundaryProps): React.ReactNode {
|
|
77
|
-
const {header, footer} = props
|
|
78
91
|
const FallbackComponent = useMemo(() => {
|
|
79
92
|
return function LoginComponentWithLayoutProps(fallbackProps: FallbackProps) {
|
|
80
|
-
return <LoginErrorComponent {...fallbackProps}
|
|
93
|
+
return <LoginErrorComponent {...fallbackProps} />
|
|
81
94
|
}
|
|
82
|
-
}, [
|
|
95
|
+
}, [LoginErrorComponent])
|
|
83
96
|
|
|
84
97
|
return (
|
|
85
98
|
<ErrorBoundary FallbackComponent={FallbackComponent}>
|
|
@@ -88,19 +101,32 @@ export function AuthBoundary({
|
|
|
88
101
|
)
|
|
89
102
|
}
|
|
90
103
|
|
|
91
|
-
interface AuthSwitchProps
|
|
92
|
-
LoginComponent?: React.ComponentType<
|
|
93
|
-
|
|
104
|
+
interface AuthSwitchProps {
|
|
105
|
+
LoginComponent?: React.ComponentType<{
|
|
106
|
+
header?: React.ReactNode
|
|
107
|
+
footer?: React.ReactNode
|
|
108
|
+
}>
|
|
109
|
+
CallbackComponent?: React.ComponentType<{
|
|
110
|
+
header?: React.ReactNode
|
|
111
|
+
footer?: React.ReactNode
|
|
112
|
+
}>
|
|
113
|
+
header?: React.ReactNode
|
|
114
|
+
footer?: React.ReactNode
|
|
115
|
+
children?: React.ReactNode
|
|
94
116
|
}
|
|
95
117
|
|
|
96
|
-
function AuthSwitch({
|
|
97
|
-
LoginComponent = Login,
|
|
98
|
-
CallbackComponent = LoginCallback,
|
|
99
|
-
children,
|
|
100
|
-
...props
|
|
101
|
-
}: AuthSwitchProps) {
|
|
118
|
+
function AuthSwitch({CallbackComponent = LoginCallback, children, ...props}: AuthSwitchProps) {
|
|
102
119
|
const authState = useAuthState()
|
|
103
120
|
|
|
121
|
+
const isLoggedOut = authState.type === AuthStateType.LOGGED_OUT && !authState.isDestroyingSession
|
|
122
|
+
const loginUrl = useLoginUrl()
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (isLoggedOut) {
|
|
126
|
+
window.location.href = loginUrl
|
|
127
|
+
}
|
|
128
|
+
}, [isLoggedOut, loginUrl])
|
|
129
|
+
|
|
104
130
|
switch (authState.type) {
|
|
105
131
|
case AuthStateType.ERROR: {
|
|
106
132
|
throw new AuthError(authState.error)
|
|
@@ -111,8 +137,12 @@ function AuthSwitch({
|
|
|
111
137
|
case AuthStateType.LOGGED_IN: {
|
|
112
138
|
return children
|
|
113
139
|
}
|
|
140
|
+
case AuthStateType.LOGGED_OUT: {
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
114
143
|
default: {
|
|
115
|
-
|
|
144
|
+
// @ts-expect-error - This state should never happen
|
|
145
|
+
throw new Error(`Invalid auth state: ${authState.type}`)
|
|
116
146
|
}
|
|
117
147
|
}
|
|
118
148
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {render, waitFor} from '@testing-library/react'
|
|
2
2
|
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
5
5
|
|
|
6
6
|
// Mock `useHandleAuthCallback`
|
|
7
7
|
vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
|
|
@@ -37,8 +37,13 @@ describe('LoginCallback', () => {
|
|
|
37
37
|
|
|
38
38
|
it('renders a loading message', async () => {
|
|
39
39
|
const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const {container} = render(
|
|
41
|
+
<ResourceProvider fallback={null}>
|
|
42
|
+
<LoginCallback />
|
|
43
|
+
</ResourceProvider>,
|
|
44
|
+
)
|
|
45
|
+
// The callback screen renders null check that it renders nothing
|
|
46
|
+
expect(container.innerHTML).toBe('')
|
|
42
47
|
})
|
|
43
48
|
|
|
44
49
|
it('handles a successful callback and calls history.replaceState', async () => {
|
|
@@ -46,7 +51,11 @@ describe('LoginCallback', () => {
|
|
|
46
51
|
vi.stubGlobal('location', {href: 'http://localhost#sid=valid'})
|
|
47
52
|
const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
render(
|
|
55
|
+
<ResourceProvider fallback={null}>
|
|
56
|
+
<LoginCallback />
|
|
57
|
+
</ResourceProvider>,
|
|
58
|
+
)
|
|
50
59
|
|
|
51
60
|
await waitFor(() => {
|
|
52
61
|
expect(history.replaceState).toHaveBeenCalledWith(
|
|
@@ -62,7 +71,11 @@ describe('LoginCallback', () => {
|
|
|
62
71
|
vi.stubGlobal('location', {href: 'http://localhost#sid=invalid'})
|
|
63
72
|
const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
render(
|
|
75
|
+
<ResourceProvider fallback={null}>
|
|
76
|
+
<LoginCallback />
|
|
77
|
+
</ResourceProvider>,
|
|
78
|
+
)
|
|
66
79
|
|
|
67
80
|
await waitFor(() => {
|
|
68
81
|
expect(history.replaceState).not.toHaveBeenCalled()
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import {useEffect} from 'react'
|
|
2
2
|
|
|
3
3
|
import {useHandleAuthCallback} from '../../hooks/auth/useHandleAuthCallback'
|
|
4
|
-
import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
|
|
5
4
|
|
|
6
|
-
/**
|
|
7
5
|
/**
|
|
8
6
|
* Component shown during auth callback processing that handles login completion.
|
|
9
7
|
* Automatically processes the auth callback when mounted and updates the URL
|
|
@@ -11,7 +9,7 @@ import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
|
|
|
11
9
|
*
|
|
12
10
|
* @alpha
|
|
13
11
|
*/
|
|
14
|
-
export function LoginCallback(
|
|
12
|
+
export function LoginCallback(): React.ReactNode {
|
|
15
13
|
const handleAuthCallback = useHandleAuthCallback()
|
|
16
14
|
|
|
17
15
|
useEffect(() => {
|
|
@@ -25,12 +23,5 @@ export function LoginCallback({header, footer}: LoginLayoutProps): React.ReactNo
|
|
|
25
23
|
})
|
|
26
24
|
}, [handleAuthCallback])
|
|
27
25
|
|
|
28
|
-
return
|
|
29
|
-
<LoginLayout header={header} footer={footer}>
|
|
30
|
-
<div className="sc-login-callback">
|
|
31
|
-
<h1 className="sc-login-callback__title">Logging you in…</h1>
|
|
32
|
-
<div className="sc-login-callback__loading">Loading…</div>
|
|
33
|
-
</div>
|
|
34
|
-
</LoginLayout>
|
|
35
|
-
)
|
|
26
|
+
return null
|
|
36
27
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {fireEvent, screen, waitFor} from '@testing-library/react'
|
|
1
|
+
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
|
2
2
|
import {describe, expect, it, vi} from 'vitest'
|
|
3
3
|
|
|
4
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
4
5
|
import {AuthError} from './AuthError'
|
|
5
|
-
import {renderWithWrappers} from './authTestHelpers'
|
|
6
6
|
import {LoginError} from './LoginError'
|
|
7
7
|
|
|
8
8
|
vi.mock('../../hooks/auth/useLogOut', () => ({
|
|
@@ -14,7 +14,11 @@ describe('LoginError', () => {
|
|
|
14
14
|
const mockReset = vi.fn()
|
|
15
15
|
const error = new AuthError(new Error('Test error'))
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
render(
|
|
18
|
+
<ResourceProvider fallback={null}>
|
|
19
|
+
<LoginError error={error} resetErrorBoundary={mockReset} />
|
|
20
|
+
</ResourceProvider>,
|
|
21
|
+
)
|
|
18
22
|
|
|
19
23
|
expect(screen.getByText('Authentication Error')).toBeInTheDocument()
|
|
20
24
|
const retryButton = screen.getByRole('button', {name: 'Retry'})
|
|
@@ -33,7 +37,11 @@ describe('LoginError', () => {
|
|
|
33
37
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
34
38
|
|
|
35
39
|
expect(() => {
|
|
36
|
-
|
|
40
|
+
render(
|
|
41
|
+
<ResourceProvider fallback={null}>
|
|
42
|
+
<LoginError error={nonAuthError} resetErrorBoundary={mockReset} />
|
|
43
|
+
</ResourceProvider>,
|
|
44
|
+
)
|
|
37
45
|
}).toThrow('Non-auth error')
|
|
38
46
|
|
|
39
47
|
consoleErrorSpy.mockRestore() // Restore original console.error behavior
|
|
@@ -3,12 +3,11 @@ import {type FallbackProps} from 'react-error-boundary'
|
|
|
3
3
|
|
|
4
4
|
import {useLogOut} from '../../hooks/auth/useLogOut'
|
|
5
5
|
import {AuthError} from './AuthError'
|
|
6
|
-
import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* @alpha
|
|
10
9
|
*/
|
|
11
|
-
export type LoginErrorProps = FallbackProps
|
|
10
|
+
export type LoginErrorProps = FallbackProps
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Displays authentication error details and provides retry functionality.
|
|
@@ -16,12 +15,7 @@ export type LoginErrorProps = FallbackProps & LoginLayoutProps
|
|
|
16
15
|
*
|
|
17
16
|
* @alpha
|
|
18
17
|
*/
|
|
19
|
-
export function LoginError({
|
|
20
|
-
error,
|
|
21
|
-
resetErrorBoundary,
|
|
22
|
-
header,
|
|
23
|
-
footer,
|
|
24
|
-
}: LoginErrorProps): React.ReactNode {
|
|
18
|
+
export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.ReactNode {
|
|
25
19
|
if (!(error instanceof AuthError)) throw error
|
|
26
20
|
const logout = useLogOut()
|
|
27
21
|
|
|
@@ -31,19 +25,17 @@ export function LoginError({
|
|
|
31
25
|
}, [logout, resetErrorBoundary])
|
|
32
26
|
|
|
33
27
|
return (
|
|
34
|
-
<
|
|
35
|
-
<div className="sc-login-
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</p>
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
<button className="sc-login-error__button" onClick={handleRetry}>
|
|
44
|
-
Retry
|
|
45
|
-
</button>
|
|
28
|
+
<div className="sc-login-error">
|
|
29
|
+
<div className="sc-login-error__content">
|
|
30
|
+
<h2 className="sc-login-error__title">Authentication Error</h2>
|
|
31
|
+
<p className="sc-login-error__description">
|
|
32
|
+
Please try again or contact support if the problem persists.
|
|
33
|
+
</p>
|
|
46
34
|
</div>
|
|
47
|
-
|
|
35
|
+
|
|
36
|
+
<button className="sc-login-error__button" onClick={handleRetry}>
|
|
37
|
+
Retry
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
48
40
|
)
|
|
49
41
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import {screen} from '@testing-library/react'
|
|
1
|
+
import {render, screen} from '@testing-library/react'
|
|
2
2
|
import {describe, expect, it} from 'vitest'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
5
5
|
import {LoginFooter} from './LoginFooter'
|
|
6
6
|
|
|
7
7
|
describe('LoginFooter', () => {
|
|
8
8
|
it('renders footer links', () => {
|
|
9
|
-
|
|
9
|
+
render(
|
|
10
|
+
<ResourceProvider fallback={null}>
|
|
11
|
+
<LoginFooter />
|
|
12
|
+
</ResourceProvider>,
|
|
13
|
+
)
|
|
10
14
|
expect(screen.getByText('Community')).toBeInTheDocument()
|
|
11
15
|
expect(screen.getByText('Docs')).toBeInTheDocument()
|
|
12
16
|
expect(screen.getByText('Privacy')).toBeInTheDocument()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {type SanityConfig, type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {render, screen} from '@testing-library/react'
|
|
3
|
+
import {StrictMode, use, useEffect} from 'react'
|
|
4
|
+
import {describe, expect, it} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {ResourceProvider} from './ResourceProvider'
|
|
7
|
+
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
8
|
+
|
|
9
|
+
const testConfig: SanityConfig = {
|
|
10
|
+
projectId: 'test-project',
|
|
11
|
+
dataset: 'test-dataset',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function promiseWithResolvers<T = void>(): {
|
|
15
|
+
promise: Promise<T>
|
|
16
|
+
resolve: (t: T) => void
|
|
17
|
+
reject: (error: unknown) => void
|
|
18
|
+
} {
|
|
19
|
+
let resolve!: (t: T) => void
|
|
20
|
+
let reject!: (error: unknown) => void
|
|
21
|
+
const promise = new Promise<T>((res, rej) => {
|
|
22
|
+
resolve = res
|
|
23
|
+
reject = rej
|
|
24
|
+
})
|
|
25
|
+
return {resolve, reject, promise}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('ResourceProvider', () => {
|
|
29
|
+
it('renders children when loaded', () => {
|
|
30
|
+
render(
|
|
31
|
+
<ResourceProvider {...testConfig} fallback={<div>Loading...</div>}>
|
|
32
|
+
<div data-testid="test-child">Child Component</div>
|
|
33
|
+
</ResourceProvider>,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
expect(screen.getByTestId('test-child')).toBeInTheDocument()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('shows fallback during loading', () => {
|
|
40
|
+
const {promise, resolve} = promiseWithResolvers()
|
|
41
|
+
function SuspendingChild(): React.ReactNode {
|
|
42
|
+
throw promise
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
render(
|
|
46
|
+
<ResourceProvider {...testConfig} fallback={<div data-testid="fallback">Loading...</div>}>
|
|
47
|
+
<SuspendingChild />
|
|
48
|
+
</ResourceProvider>,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
expect(screen.getByTestId('fallback')).toBeInTheDocument()
|
|
52
|
+
resolve()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('creates root instance when no parent context exists', async () => {
|
|
56
|
+
const {promise, resolve} = promiseWithResolvers<SanityInstance | null>()
|
|
57
|
+
|
|
58
|
+
const CaptureInstance = () => {
|
|
59
|
+
const instance = use(SanityInstanceContext)
|
|
60
|
+
useEffect(() => resolve(instance), [instance])
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
render(
|
|
65
|
+
<ResourceProvider {...testConfig} fallback={null}>
|
|
66
|
+
<CaptureInstance />
|
|
67
|
+
</ResourceProvider>,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
await expect(promise).resolves.toMatchObject({
|
|
71
|
+
config: testConfig,
|
|
72
|
+
isDisposed: expect.any(Function),
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('creates child instance when parent context exists', async () => {
|
|
77
|
+
const parentConfig: SanityConfig = {...testConfig, dataset: 'parent-dataset'}
|
|
78
|
+
const child = promiseWithResolvers<SanityInstance | null>()
|
|
79
|
+
|
|
80
|
+
const CaptureInstance = () => {
|
|
81
|
+
const childInstance = use(SanityInstanceContext)
|
|
82
|
+
useEffect(() => child.resolve(childInstance), [childInstance])
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
render(
|
|
87
|
+
<ResourceProvider {...parentConfig} fallback={null}>
|
|
88
|
+
<ResourceProvider {...testConfig} fallback={null}>
|
|
89
|
+
<CaptureInstance />
|
|
90
|
+
</ResourceProvider>
|
|
91
|
+
</ResourceProvider>,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
const childInstance = await child.promise
|
|
95
|
+
expect(childInstance?.config).toEqual(testConfig)
|
|
96
|
+
expect(childInstance?.isDisposed()).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('disposes instance when unmounted', async () => {
|
|
100
|
+
const {promise, resolve} = promiseWithResolvers<SanityInstance | null>()
|
|
101
|
+
const CaptureInstance = () => {
|
|
102
|
+
const instance = use(SanityInstanceContext)
|
|
103
|
+
useEffect(() => resolve(instance), [instance])
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const {unmount} = render(
|
|
108
|
+
<ResourceProvider {...testConfig} fallback={null}>
|
|
109
|
+
<CaptureInstance />
|
|
110
|
+
</ResourceProvider>,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
unmount()
|
|
114
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
115
|
+
const instance = await promise
|
|
116
|
+
|
|
117
|
+
expect(instance?.isDisposed()).toBe(true)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('does not dispose on quick remount (Strict Mode)', async () => {
|
|
121
|
+
const {promise, resolve} = promiseWithResolvers<SanityInstance | null>()
|
|
122
|
+
const CaptureInstance = () => {
|
|
123
|
+
const instance = use(SanityInstanceContext)
|
|
124
|
+
useEffect(() => resolve(instance), [instance])
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
render(
|
|
129
|
+
<StrictMode>
|
|
130
|
+
<ResourceProvider {...testConfig} fallback={null}>
|
|
131
|
+
<CaptureInstance />
|
|
132
|
+
</ResourceProvider>
|
|
133
|
+
</StrictMode>,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const instance = await promise
|
|
137
|
+
|
|
138
|
+
expect(instance?.isDisposed()).toBe(false)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('uses default fallback when none provided', () => {
|
|
142
|
+
const {promise, resolve} = promiseWithResolvers()
|
|
143
|
+
function SuspendingChild(): React.ReactNode {
|
|
144
|
+
throw promise
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
render(
|
|
148
|
+
// @ts-expect-error Testing fallback behavior
|
|
149
|
+
<ResourceProvider {...testConfig}>
|
|
150
|
+
<SuspendingChild />
|
|
151
|
+
</ResourceProvider>,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
expect(screen.getByText(/Warning: No fallback provided/)).toBeInTheDocument()
|
|
155
|
+
resolve()
|
|
156
|
+
})
|
|
157
|
+
})
|