.
68 | <> a space:Storage .
69 | `,
70 | store,
71 | 'https://alice.test/public/'
72 | );
73 | const storageStore = new StorageStore(store);
74 | const result = storageStore.isStorage('https://alice.test/public/');
75 | expect(result).toBe(true);
76 | });
77 | });
78 | describe('createIndexDocumentStatement', () => {
79 | it('creates a single statement to describe the index document', () => {
80 | const store = graph();
81 | const storageStore = new StorageStore(store);
82 | const result = storageStore.createIndexDocumentStatement(
83 | 'https://alice.test/public/'
84 | );
85 | expect(result).toEqual([
86 | st(
87 | sym('https://alice.test/public/index.ttl'),
88 | sym('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
89 | sym('http://www.w3.org/ns/ldp#Resource'),
90 | sym('https://alice.test/public/index.ttl')
91 | ),
92 | ]);
93 | });
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/dev/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 | WebClip Test Page
11 |
34 |
35 |
36 |
37 | This is a test page for development
38 |
39 |
40 |
iOS
41 |
42 |
MONOPOLY
43 | by
Electronic Arts
44 |
45 | **YOU VOTED & THE CAT’S OUT OF THE BAG** Thanks to the votes from YOU and thousands of loyal MONOPOLY Facebook fans from 185 different countries, the CAT mover is now available to play with in this latest update as well as in the classic board game version of MONOPOLY!
46 |
47 | 4 stars -
48 | 33 reviews
49 |
50 |
51 | Price: 1
52 |
53 |
54 | In Stock
55 |
56 | Version:
1.2.50
57 | Updated:
58 |
59 | 09.11.2013
60 |
61 | Age4 +
62 |
63 |
64 |
65 |
66 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy
67 | nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut
68 | wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit
69 | lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum
70 | iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel
71 | illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto
72 | odio dignissim qui blandit praesent luptatum zzril delenit a
73 |
74 |
75 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy
76 | nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut
77 | wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit
78 | lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum
79 | iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel
80 | illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto
81 | odio dignissim qui blandit praesent luptatum zzril delenit a
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/store/BookmarkStore.spec.ts:
--------------------------------------------------------------------------------
1 | import { graph, sym } from 'rdflib';
2 | import { givenStoreContaining } from '../test/givenStoreContaining';
3 | import { BookmarkStore } from './BookmarkStore';
4 |
5 | describe('BookmarkStore', () => {
6 | describe('getIndexedBookmark', () => {
7 | it('does not find index bookmark in empty store', () => {
8 | const store = new BookmarkStore(graph());
9 | const result = store.getIndexedBookmark(
10 | sym('https://page.test'),
11 | sym('https://pod.example/webclip/index.ttl')
12 | );
13 | expect(result).toBeNull();
14 | });
15 |
16 | it('does not find bookmark, if it is not present in index', () => {
17 | const rdfGraph = givenStoreContaining(
18 | 'https://pod.example/webclip/index.ttl',
19 | `
20 | @prefix schema: .
21 |
22 | a schema:BookmarkAction; schema:object .
23 | `
24 | );
25 | const store = new BookmarkStore(rdfGraph);
26 | const result = store.getIndexedBookmark(
27 | sym('https://page.test'),
28 | sym('https://pod.example/webclip/index.ttl')
29 | );
30 | expect(result).toBeNull();
31 | });
32 |
33 | it('finds the bookmark, if it is present in index', () => {
34 | const rdfGraph = givenStoreContaining(
35 | 'https://pod.example/webclip/index.ttl',
36 | `
37 | @prefix schema: .
38 |
39 | a schema:BookmarkAction; schema:object .
40 | `
41 | );
42 | const store = new BookmarkStore(rdfGraph);
43 | const result = store.getIndexedBookmark(
44 | sym('https://page.test'),
45 | sym('https://pod.example/webclip/index.ttl')
46 | );
47 | expect(result).toEqual(sym('http://storage.example/webclip/relevant#it'));
48 | });
49 |
50 | it('does not find bookmark, if the page is not linked by schema:object', () => {
51 | const rdfGraph = givenStoreContaining(
52 | 'https://pod.example/webclip/index.ttl',
53 | `
54 | @prefix schema: .
55 |
56 | a schema:BookmarkAction; schema:irrelevant .
57 | `
58 | );
59 | const store = new BookmarkStore(rdfGraph);
60 | const result = store.getIndexedBookmark(
61 | sym('https://page.test'),
62 | sym('https://pod.example/webclip/index.ttl')
63 | );
64 | expect(result).toBeNull();
65 | });
66 |
67 | it('only finds bookmarks within index document', () => {
68 | const rdfGraph = givenStoreContaining(
69 | 'https://pod.example/somewhere/else.ttl',
70 | `
71 | @prefix schema: .
72 |
73 | a schema:BookmarkAction; schema:object .
74 | `
75 | );
76 | const store = new BookmarkStore(rdfGraph);
77 | const result = store.getIndexedBookmark(
78 | sym('https://page.test'),
79 | sym('https://pod.example/webclip/index.ttl')
80 | );
81 | expect(result).toBeNull();
82 | });
83 | });
84 |
85 | it('does not find bookmark, if it is not a schema:BookmarkAction', () => {
86 | const rdfGraph = givenStoreContaining(
87 | 'https://pod.example/webclip/index.ttl',
88 | `
89 | @prefix schema: .
90 |
91 | a schema:WatchAction; schema:object .
92 | `
93 | );
94 | const store = new BookmarkStore(rdfGraph);
95 | const result = store.getIndexedBookmark(
96 | sym('https://page.test'),
97 | sym('https://pod.example/webclip/index.ttl')
98 | );
99 | expect(result).toBeNull();
100 | });
101 | });
102 |
--------------------------------------------------------------------------------
/src/solid-client-authn-chrome-ext/mock-idp-responses.ts:
--------------------------------------------------------------------------------
1 | export const openIdConfiguration = {
2 | authorization_endpoint: 'https://pod.test/idp/auth',
3 | claims_parameter_supported: true,
4 | claims_supported: ['webid', 'client_id', 'sub', 'sid', 'auth_time', 'iss'],
5 | code_challenge_methods_supported: ['S256'],
6 | end_session_endpoint: 'https://pod.test/idp/session/end',
7 | grant_types_supported: ['implicit', 'authorization_code', 'refresh_token'],
8 | id_token_signing_alg_values_supported: ['HS256', 'RS256'],
9 | issuer: 'https://pod.test/',
10 | jwks_uri: 'https://pod.test/idp/jwks',
11 | registration_endpoint: 'https://pod.test/idp/reg',
12 | response_modes_supported: ['form_post', 'fragment', 'query'],
13 | response_types_supported: ['code id_token', 'code', 'id_token', 'none'],
14 | scopes_supported: ['openid', 'profile', 'offline_access'],
15 | subject_types_supported: ['public', 'pairwise'],
16 | token_endpoint_auth_methods_supported: [
17 | 'none',
18 | 'client_secret_basic',
19 | 'client_secret_jwt',
20 | 'client_secret_post',
21 | 'private_key_jwt',
22 | ],
23 | token_endpoint_auth_signing_alg_values_supported: [
24 | 'HS256',
25 | 'RS256',
26 | 'PS256',
27 | 'ES256',
28 | 'EdDSA',
29 | ],
30 | token_endpoint: 'https://pod.test/idp/token',
31 | request_object_signing_alg_values_supported: [
32 | 'HS256',
33 | 'RS256',
34 | 'PS256',
35 | 'ES256',
36 | 'EdDSA',
37 | ],
38 | request_parameter_supported: false,
39 | request_uri_parameter_supported: true,
40 | require_request_uri_registration: true,
41 | userinfo_endpoint: 'https://pod.test/idp/me',
42 | userinfo_signing_alg_values_supported: ['HS256', 'RS256'],
43 | introspection_endpoint: 'https://pod.test/idp/token/introspection',
44 | introspection_endpoint_auth_methods_supported: [
45 | 'none',
46 | 'client_secret_basic',
47 | 'client_secret_jwt',
48 | 'client_secret_post',
49 | 'private_key_jwt',
50 | ],
51 | introspection_endpoint_auth_signing_alg_values_supported: [
52 | 'HS256',
53 | 'RS256',
54 | 'PS256',
55 | 'ES256',
56 | 'EdDSA',
57 | ],
58 | dpop_signing_alg_values_supported: ['RS256', 'PS256', 'ES256', 'EdDSA'],
59 | revocation_endpoint: 'https://pod.test/idp/token/revocation',
60 | revocation_endpoint_auth_methods_supported: [
61 | 'none',
62 | 'client_secret_basic',
63 | 'client_secret_jwt',
64 | 'client_secret_post',
65 | 'private_key_jwt',
66 | ],
67 | revocation_endpoint_auth_signing_alg_values_supported: [
68 | 'HS256',
69 | 'RS256',
70 | 'PS256',
71 | 'ES256',
72 | 'EdDSA',
73 | ],
74 | claim_types_supported: ['normal'],
75 | solid_oidc_supported: 'https://solidproject.org/TR/solid-oidc',
76 | };
77 |
78 | export const registerResponse = {
79 | application_type: 'web',
80 | grant_types: ['authorization_code'],
81 | id_token_signed_response_alg: 'RS256',
82 | require_auth_time: false,
83 | response_types: ['code'],
84 | subject_type: 'pairwise',
85 | token_endpoint_auth_method: 'client_secret_basic',
86 | introspection_endpoint_auth_method: 'client_secret_basic',
87 | revocation_endpoint_auth_method: 'client_secret_basic',
88 | require_signed_request_object: false,
89 | client_id_issued_at: 1635173530,
90 | client_id: 'mock-client-id',
91 | client_name: 'My Example App',
92 | client_secret_expires_at: 0,
93 | client_secret: 'mock-client-secret',
94 | redirect_uris: ['https://example.test/redirect-url'],
95 | registration_client_uri: 'https://pod.test/idp/reg/mock-client-id',
96 | registration_access_token: 'mock-registration-access-token',
97 | };
98 |
99 | export const tokenResponse = {
100 | access_token: 'mock-access-token',
101 | id_token: 'mock-id-token',
102 | token_type: 'DPoP',
103 | };
104 |
105 | export const jwksResponse = {
106 | keys: [
107 | {
108 | kid: 'mock-kid',
109 | kty: 'RSA',
110 | alg: 'RS256',
111 | key_ops: ['verify'],
112 | ext: true,
113 | n: 'n',
114 | e: 'AQAB',
115 | },
116 | ],
117 | };
118 |
--------------------------------------------------------------------------------
/src/options/OptionsStorage.spec.ts:
--------------------------------------------------------------------------------
1 | import { when } from 'jest-when';
2 | import { OptionsStorage } from './OptionsStorage';
3 | import { load, onChanged } from './optionsStorageApi';
4 |
5 | jest.mock('./optionsStorageApi');
6 |
7 | describe('OptionsStorage', () => {
8 | beforeEach(() => {
9 | jest.resetAllMocks();
10 | });
11 |
12 | it('holds the loaded options initially', async () => {
13 | when(load).mockResolvedValue({
14 | providerUrl: 'https://provider.test',
15 | containerUrl: 'https://container.test',
16 | trustedApp: true,
17 | });
18 | const storage = new OptionsStorage();
19 | await storage.init();
20 | expect(storage.getOptions()).toEqual({
21 | providerUrl: 'https://provider.test',
22 | containerUrl: 'https://container.test',
23 | trustedApp: true,
24 | });
25 | });
26 |
27 | it('holds changed and unchanged values after a change', async () => {
28 | when(load).mockResolvedValue({
29 | providerUrl: 'https://provider.test',
30 | containerUrl: 'https://container.test',
31 | trustedApp: true,
32 | });
33 | const storage = new OptionsStorage();
34 | await storage.init();
35 | expect(onChanged).toHaveBeenCalled();
36 | const change = (onChanged as jest.Mock).mock.calls[0][0];
37 | change(
38 | {
39 | providerUrl: {
40 | newValue: 'https://new.provider.test',
41 | oldValue: 'https://provider.test',
42 | },
43 | containerUrl: {
44 | newValue: 'https://new.container.test',
45 | oldValue: 'https://container.test',
46 | },
47 | },
48 | 'sync'
49 | );
50 | expect(storage.getOptions()).toEqual({
51 | providerUrl: 'https://new.provider.test',
52 | containerUrl: 'https://new.container.test',
53 | trustedApp: true,
54 | });
55 | });
56 |
57 | it('ignore changes in other namspaces than sync', async () => {
58 | when(load).mockResolvedValue({
59 | providerUrl: 'https://provider.test',
60 | containerUrl: 'https://container.test',
61 | trustedApp: true,
62 | });
63 | const storage = new OptionsStorage();
64 | await storage.init();
65 | expect(onChanged).toHaveBeenCalled();
66 | const change = (onChanged as jest.Mock).mock.calls[0][0];
67 | change(
68 | {
69 | providerUrl: {
70 | newValue: 'https://new.provider.test',
71 | oldValue: 'https://provider.test',
72 | },
73 | containerUrl: {
74 | newValue: 'https://new.container.test',
75 | oldValue: 'https://container.test',
76 | },
77 | },
78 | 'managed'
79 | );
80 | expect(storage.getOptions()).toEqual({
81 | providerUrl: 'https://provider.test',
82 | containerUrl: 'https://container.test',
83 | trustedApp: true,
84 | });
85 | });
86 |
87 | describe('events', () => {
88 | it('notifies about all changes', async () => {
89 | when(load).mockResolvedValue({
90 | providerUrl: 'https://provider.test',
91 | containerUrl: 'https://container.test',
92 | trustedApp: true,
93 | });
94 | const storage = new OptionsStorage();
95 | await storage.init();
96 | expect(onChanged).toHaveBeenCalled();
97 | const providerUrlListener = jest.fn();
98 | const containerUrlListener = jest.fn();
99 | const trustedAppListener = jest.fn();
100 | storage.on('providerUrl', providerUrlListener);
101 | storage.on('containerUrl', containerUrlListener);
102 | storage.on('trustedApp', trustedAppListener);
103 | const change = (onChanged as jest.Mock).mock.calls[0][0];
104 | change(
105 | {
106 | providerUrl: {
107 | newValue: 'https://new.provider.test',
108 | oldValue: 'https://provider.test',
109 | },
110 | containerUrl: {
111 | newValue: 'https://new.container.test',
112 | oldValue: 'https://container.test',
113 | },
114 | },
115 | 'sync'
116 | );
117 | expect(providerUrlListener).toHaveBeenCalledWith({
118 | newValue: 'https://new.provider.test',
119 | oldValue: 'https://provider.test',
120 | });
121 | expect(containerUrlListener).toHaveBeenCalledWith({
122 | newValue: 'https://new.container.test',
123 | oldValue: 'https://container.test',
124 | });
125 | expect(trustedAppListener).not.toHaveBeenCalled();
126 | });
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/src/solid-client-authn-chrome-ext/ChromeExtensionRedirector.spec.ts:
--------------------------------------------------------------------------------
1 | import { IIncomingRedirectHandler } from '@inrupt/solid-client-authn-core';
2 | import { ChromeExtensionRedirector } from './ChromeExtensionRedirector';
3 | import { launchWebAuthFlow } from './launchWebAuthFlow';
4 |
5 | jest.mock('./launchWebAuthFlow');
6 |
7 | describe('ChromeExtensionRedirector', () => {
8 | beforeEach(() => {
9 | jest.resetAllMocks();
10 | });
11 |
12 | function mockRedirectHandler(): IIncomingRedirectHandler {
13 | return {
14 | handle: jest.fn(),
15 | canHandle: jest.fn(),
16 | };
17 | }
18 |
19 | describe('redirect', () => {
20 | it('launches a web auth flow', () => {
21 | const redirectHandler = mockRedirectHandler();
22 | const redirector = new ChromeExtensionRedirector(
23 | redirectHandler,
24 | () => null
25 | );
26 | redirector.redirect('/redirect-url');
27 |
28 | expect(launchWebAuthFlow).toHaveBeenCalledWith(
29 | {
30 | url: '/redirect-url',
31 | interactive: true,
32 | },
33 | expect.anything()
34 | );
35 | });
36 |
37 | it('calls the redirect handler when web auth flow returns to extension', () => {
38 | const redirectHandler = mockRedirectHandler();
39 |
40 | const redirector = new ChromeExtensionRedirector(
41 | redirectHandler,
42 | () => null
43 | );
44 | redirector.redirect('/redirect-url');
45 |
46 | const returnToExtension = (launchWebAuthFlow as jest.Mock).mock
47 | .calls[0][1];
48 |
49 | returnToExtension('/redirect-url?code=123');
50 | expect(redirectHandler.handle).toHaveBeenCalledWith(
51 | '/redirect-url?code=123',
52 | undefined
53 | );
54 | });
55 |
56 | it('calls the after redirect handler with the session info and authenticated fetch', async () => {
57 | const redirectHandler = mockRedirectHandler();
58 | const sessionInfo = {
59 | isLoggedIn: true,
60 | sessionID: 'session-id',
61 | fetch: jest.fn(),
62 | };
63 | (redirectHandler.handle as jest.Mock).mockResolvedValue(sessionInfo);
64 |
65 | const afterRedirect = jest.fn();
66 |
67 | const redirector = new ChromeExtensionRedirector(
68 | redirectHandler,
69 | afterRedirect
70 | );
71 | redirector.redirect('/redirect-url');
72 |
73 | const returnToExtension = (launchWebAuthFlow as jest.Mock).mock
74 | .calls[0][1];
75 |
76 | await returnToExtension('/redirect-url?code=123');
77 |
78 | expect(afterRedirect).toHaveBeenCalledWith(sessionInfo);
79 | });
80 |
81 | it('calls the after redirect handler with error from authentication process', async () => {
82 | const redirectHandler = mockRedirectHandler();
83 |
84 | const afterRedirect = jest.fn();
85 |
86 | const redirector = new ChromeExtensionRedirector(
87 | redirectHandler,
88 | afterRedirect
89 | );
90 | redirector.redirect('/redirect-url');
91 |
92 | const returnToExtension = (launchWebAuthFlow as jest.Mock).mock
93 | .calls[0][1];
94 |
95 | await returnToExtension(
96 | '/redirect-url?code=123',
97 | new Error('error during authentication')
98 | );
99 |
100 | expect(afterRedirect).toHaveBeenCalledWith(
101 | {
102 | isLoggedIn: false,
103 | sessionId: '',
104 | fetch: undefined,
105 | },
106 | new Error('error during authentication')
107 | );
108 | });
109 |
110 | it('calls the after redirect handler with an error from redirect handler', async () => {
111 | const redirectHandler = mockRedirectHandler();
112 |
113 | (redirectHandler.handle as jest.Mock).mockRejectedValue(
114 | new Error('error during handler')
115 | );
116 |
117 | const afterRedirect = jest.fn();
118 |
119 | const redirector = new ChromeExtensionRedirector(
120 | redirectHandler,
121 | afterRedirect
122 | );
123 | redirector.redirect('/redirect-url');
124 |
125 | const returnToExtension = (launchWebAuthFlow as jest.Mock).mock
126 | .calls[0][1];
127 |
128 | await returnToExtension('/redirect-url?code=123');
129 |
130 | expect(afterRedirect).toHaveBeenCalledWith(
131 | {
132 | isLoggedIn: false,
133 | sessionId: '',
134 | fetch: undefined,
135 | },
136 | new Error('error during handler')
137 | );
138 | });
139 | });
140 | });
141 |
--------------------------------------------------------------------------------
/src/options/choose-storage/ChooseStorage.spec.tsx:
--------------------------------------------------------------------------------
1 | import userEvent from '@testing-library/user-event';
2 | import { when } from 'jest-when';
3 | import React from 'react';
4 | import { act, render, screen } from '@testing-library/react';
5 | import { ChooseStorage } from './ChooseStorage';
6 | import { useChooseStorage } from './useChooseStorage';
7 |
8 | jest.mock('./useChooseStorage');
9 |
10 | describe('ChooseStorage', () => {
11 | describe('while loading', () => {
12 | beforeEach(() => {
13 | when(useChooseStorage).mockReturnValue({
14 | loading: true,
15 | submitting: false,
16 | containerUrl: '',
17 | manualChanges: false,
18 | setContainerUrl: jest.fn(),
19 | submit: () => null,
20 | });
21 | });
22 |
23 | it('indicates loading', () => {
24 | render( );
25 | expect(
26 | screen.getByText("Let's find a storage location for your clips.")
27 | ).toBeInTheDocument();
28 | expect(screen.getByText('Please wait...')).toBeInTheDocument();
29 | });
30 | });
31 |
32 | describe('after loading and finding a storage', () => {
33 | let setContainerUrl: jest.Mock;
34 | beforeEach(() => {
35 | setContainerUrl = jest.fn();
36 | when(useChooseStorage).mockReturnValue({
37 | loading: false,
38 | submitting: false,
39 | manualChanges: false,
40 | containerUrl: 'https://pod.example/alice/webclip/',
41 | setContainerUrl,
42 | submit: () => null,
43 | });
44 | });
45 |
46 | it('shows the container url', () => {
47 | render( );
48 | expect(
49 | screen.getByDisplayValue('https://pod.example/alice/webclip/')
50 | ).toBeInTheDocument();
51 | });
52 |
53 | it('allows to change the proposed url', () => {
54 | render( );
55 | const input = screen.getByLabelText('Storage Location');
56 | act(() => {
57 | userEvent.paste(input, 'additional-container');
58 | });
59 | expect(setContainerUrl).toHaveBeenLastCalledWith(
60 | 'https://pod.example/alice/webclip/additional-container'
61 | );
62 | });
63 |
64 | it('asks if it is ok', () => {
65 | render( );
66 | expect(
67 | screen.getByText(
68 | 'WebClip is going to store data at the following location. Confirm, if you are fine with that, or enter the URL of a different location.'
69 | )
70 | ).toBeInTheDocument();
71 | });
72 |
73 | it('allows to confirm', () => {
74 | render( );
75 | const continueButton = screen.getByText('Confirm');
76 | expect(continueButton).toBeInTheDocument();
77 | userEvent.click(continueButton);
78 | });
79 | });
80 |
81 | describe('after loading and finding no storage', () => {
82 | let setContainerUrl: jest.Mock;
83 | let submit: jest.Mock;
84 | beforeEach(() => {
85 | submit = jest.fn();
86 | setContainerUrl = jest.fn();
87 | when(useChooseStorage).mockReturnValue({
88 | loading: false,
89 | submitting: false,
90 | containerUrl: null,
91 | manualChanges: true,
92 | setContainerUrl,
93 | submit,
94 | });
95 | });
96 |
97 | it('asks to enter a url', () => {
98 | render( );
99 | expect(
100 | screen.getByText(
101 | 'WebClip could not find a storage associated with your Pod, please enter a URL manually.'
102 | )
103 | ).toBeInTheDocument();
104 | });
105 |
106 | it('allows to enter a url manually', () => {
107 | render( );
108 | const input = screen.getByLabelText('Storage Location');
109 | act(() => {
110 | userEvent.paste(input, 'https://pod.example/alice/webclip/');
111 | });
112 | expect(setContainerUrl).toHaveBeenLastCalledWith(
113 | 'https://pod.example/alice/webclip/'
114 | );
115 | });
116 |
117 | it('allows to submit', () => {
118 | render( );
119 | const continueButton = screen.getByText('Submit');
120 | expect(continueButton).toBeInTheDocument();
121 | userEvent.click(continueButton);
122 | expect(submit).toHaveBeenCalled();
123 | });
124 | });
125 |
126 | describe('after submitting an invalid container url', () => {
127 | beforeEach(() => {
128 | when(useChooseStorage).mockReturnValue({
129 | loading: false,
130 | submitting: false,
131 | manualChanges: false,
132 | containerUrl: 'https://pod.example/alice/webclip/',
133 | setContainerUrl: () => null,
134 | validationError: new Error('Please choose a valid container'),
135 | submit: () => null,
136 | });
137 | });
138 | it('shows the validation error', () => {
139 | render( );
140 | expect(
141 | screen.getByText('Please choose a valid container')
142 | ).toBeInTheDocument();
143 | });
144 | });
145 | });
146 |
--------------------------------------------------------------------------------