You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Chore: Mobile: Add NoteBodyViewer tests (#10747)
				
					
				
			This commit is contained in:
		| @@ -520,12 +520,15 @@ packages/app-mobile/components/CameraView.js | ||||
| packages/app-mobile/components/DismissibleDialog.js | ||||
| packages/app-mobile/components/Dropdown.test.js | ||||
| packages/app-mobile/components/Dropdown.js | ||||
| packages/app-mobile/components/ExtendedWebView.js | ||||
| packages/app-mobile/components/ExtendedWebView/index.jest.js | ||||
| packages/app-mobile/components/ExtendedWebView/index.js | ||||
| packages/app-mobile/components/ExtendedWebView/types.js | ||||
| packages/app-mobile/components/FolderPicker.js | ||||
| packages/app-mobile/components/Icon.js | ||||
| packages/app-mobile/components/IconButton.js | ||||
| packages/app-mobile/components/Modal.js | ||||
| packages/app-mobile/components/ModalDialog.js | ||||
| packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.js | ||||
| packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js | ||||
| packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js | ||||
| packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js | ||||
| @@ -716,6 +719,7 @@ packages/app-mobile/utils/getVersionInfoText.js | ||||
| packages/app-mobile/utils/image/getImageDimensions.js | ||||
| packages/app-mobile/utils/image/resizeImage.js | ||||
| packages/app-mobile/utils/initializeCommandService.js | ||||
| packages/app-mobile/utils/injectedJs.js | ||||
| packages/app-mobile/utils/ipc/RNToWebViewMessenger.js | ||||
| packages/app-mobile/utils/ipc/WebViewToRNMessenger.js | ||||
| packages/app-mobile/utils/pickDocument.js | ||||
| @@ -1259,6 +1263,7 @@ packages/lib/urlUtils.js | ||||
| packages/lib/utils/ActionLogger.test.js | ||||
| packages/lib/utils/ActionLogger.js | ||||
| packages/lib/utils/credentialFiles.js | ||||
| packages/lib/utils/dom/makeSandboxedIframe.js | ||||
| packages/lib/utils/focusHandler.js | ||||
| packages/lib/utils/frontMatter.js | ||||
| packages/lib/utils/ipc/RemoteMessenger.test.js | ||||
|   | ||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -499,12 +499,15 @@ packages/app-mobile/components/CameraView.js | ||||
| packages/app-mobile/components/DismissibleDialog.js | ||||
| packages/app-mobile/components/Dropdown.test.js | ||||
| packages/app-mobile/components/Dropdown.js | ||||
| packages/app-mobile/components/ExtendedWebView.js | ||||
| packages/app-mobile/components/ExtendedWebView/index.jest.js | ||||
| packages/app-mobile/components/ExtendedWebView/index.js | ||||
| packages/app-mobile/components/ExtendedWebView/types.js | ||||
| packages/app-mobile/components/FolderPicker.js | ||||
| packages/app-mobile/components/Icon.js | ||||
| packages/app-mobile/components/IconButton.js | ||||
| packages/app-mobile/components/Modal.js | ||||
| packages/app-mobile/components/ModalDialog.js | ||||
| packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.js | ||||
| packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js | ||||
| packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js | ||||
| packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js | ||||
| @@ -695,6 +698,7 @@ packages/app-mobile/utils/getVersionInfoText.js | ||||
| packages/app-mobile/utils/image/getImageDimensions.js | ||||
| packages/app-mobile/utils/image/resizeImage.js | ||||
| packages/app-mobile/utils/initializeCommandService.js | ||||
| packages/app-mobile/utils/injectedJs.js | ||||
| packages/app-mobile/utils/ipc/RNToWebViewMessenger.js | ||||
| packages/app-mobile/utils/ipc/WebViewToRNMessenger.js | ||||
| packages/app-mobile/utils/pickDocument.js | ||||
| @@ -1238,6 +1242,7 @@ packages/lib/urlUtils.js | ||||
| packages/lib/utils/ActionLogger.test.js | ||||
| packages/lib/utils/ActionLogger.js | ||||
| packages/lib/utils/credentialFiles.js | ||||
| packages/lib/utils/dom/makeSandboxedIframe.js | ||||
| packages/lib/utils/focusHandler.js | ||||
| packages/lib/utils/frontMatter.js | ||||
| packages/lib/utils/ipc/RemoteMessenger.test.js | ||||
|   | ||||
| @@ -0,0 +1,83 @@ | ||||
| import * as React from 'react'; | ||||
|  | ||||
| import { | ||||
| 	forwardRef, Ref, useEffect, useImperativeHandle, useMemo, useRef, | ||||
| } from 'react'; | ||||
|  | ||||
| import { View } from 'react-native'; | ||||
| import Logger from '@joplin/utils/Logger'; | ||||
| import { Props, WebViewControl } from './types'; | ||||
| import { JSDOM } from 'jsdom'; | ||||
|  | ||||
| const logger = Logger.create('ExtendedWebView'); | ||||
|  | ||||
| const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => { | ||||
| 	const dom = useMemo(() => { | ||||
| 		// Note: Adding `runScripts: 'dangerously'` to allow running inline <script></script>s. | ||||
| 		// Use with caution. | ||||
| 		return new JSDOM(props.html, { runScripts: 'dangerously' }); | ||||
| 	}, [props.html]); | ||||
|  | ||||
| 	useImperativeHandle(ref, (): WebViewControl => { | ||||
| 		const result = { | ||||
| 			injectJS(js: string) { | ||||
| 				return dom.window.eval(js); | ||||
| 			}, | ||||
| 			postMessage(message: unknown) { | ||||
| 				const messageEventContent = { | ||||
| 					data: message, | ||||
| 					source: 'react-native', | ||||
| 				}; | ||||
| 				return dom.window.eval(` | ||||
| 					window.dispatchEvent( | ||||
| 						new MessageEvent('message', ${JSON.stringify(messageEventContent)}), | ||||
| 					); | ||||
| 				`); | ||||
| 			}, | ||||
| 		}; | ||||
| 		return result; | ||||
| 	}, [dom]); | ||||
|  | ||||
| 	const onMessageRef = useRef(props.onMessage); | ||||
| 	onMessageRef.current = props.onMessage; | ||||
|  | ||||
| 	// Don't re-load when injected JS changes. This should match the behavior of the native webview. | ||||
| 	const injectedJavaScriptRef = useRef(props.injectedJavaScript); | ||||
| 	injectedJavaScriptRef.current = props.injectedJavaScript; | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		dom.window.eval(` | ||||
| 			window.setWebViewApi = (api) => { | ||||
| 				window.ReactNativeWebView = api; | ||||
| 			}; | ||||
| 		`); | ||||
| 		dom.window.setWebViewApi({ | ||||
| 			postMessage: (message: unknown) => { | ||||
| 				logger.debug('Got message', message); | ||||
| 				onMessageRef.current({ nativeEvent: { data: message } }); | ||||
| 			}, | ||||
| 		}); | ||||
|  | ||||
| 		dom.window.eval(injectedJavaScriptRef.current); | ||||
| 	}, [dom]); | ||||
|  | ||||
|  | ||||
| 	const onLoadEndRef = useRef(props.onLoadEnd); | ||||
| 	onLoadEndRef.current = props.onLoadEnd; | ||||
| 	const onLoadStartRef = useRef(props.onLoadStart); | ||||
| 	onLoadStartRef.current = props.onLoadStart; | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		logger.debug(`DOM at ${dom.window?.location?.href} is reloading.`); | ||||
| 		onLoadStartRef.current?.(); | ||||
| 		onLoadEndRef.current?.(); | ||||
| 	}, [dom]); | ||||
|  | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- HACK: Allow wrapper testing logic to access the DOM. | ||||
| 	const additionalProps: any = { document: dom?.window?.document }; | ||||
| 	return ( | ||||
| 		<View style={props.style} testID={props.testID} {...additionalProps}/> | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| export default forwardRef(ExtendedWebView); | ||||
| @@ -5,73 +5,17 @@ import * as React from 'react'; | ||||
| import { | ||||
| 	forwardRef, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, | ||||
| } from 'react'; | ||||
| import { WebView, WebViewMessageEvent } from 'react-native-webview'; | ||||
| import { WebViewErrorEvent, WebViewEvent, WebViewSource } from 'react-native-webview/lib/WebViewTypes'; | ||||
| import { WebView } from 'react-native-webview'; | ||||
| import { WebViewErrorEvent, WebViewSource } from 'react-native-webview/lib/WebViewTypes'; | ||||
| 
 | ||||
| import Setting from '@joplin/lib/models/Setting'; | ||||
| import shim from '@joplin/lib/shim'; | ||||
| import { StyleProp, ViewStyle } from 'react-native'; | ||||
| import Logger from '@joplin/utils/Logger'; | ||||
| import { Props, WebViewControl } from './types'; | ||||
| 
 | ||||
| const logger = Logger.create('ExtendedWebView'); | ||||
| 
 | ||||
| export interface WebViewControl { | ||||
| 	// Evaluate the given [script] in the context of the page.
 | ||||
| 	// Unlike react-native-webview/WebView, this does not need to return true.
 | ||||
| 	injectJS(script: string): void; | ||||
| 
 | ||||
| 	// message must be convertible to JSON
 | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
 | ||||
| 	postMessage(message: any): void; | ||||
| } | ||||
| 
 | ||||
| interface SourceFileUpdateEvent { | ||||
| 	uri: string; | ||||
| 	baseUrl: string; | ||||
| 
 | ||||
| 	filePath: string; | ||||
| } | ||||
| 
 | ||||
| export type OnMessageCallback = (event: WebViewMessageEvent)=> void; | ||||
| export type OnErrorCallback = (event: WebViewErrorEvent)=> void; | ||||
| export type OnLoadCallback = (event: WebViewEvent)=> void; | ||||
| type OnFileUpdateCallback = (event: SourceFileUpdateEvent)=> void; | ||||
| 
 | ||||
| interface Props { | ||||
| 	// A name to be associated with the WebView (e.g. NoteEditor)
 | ||||
| 	// This name should be unique.
 | ||||
| 	webviewInstanceId: string; | ||||
| 
 | ||||
| 	// If HTML is still being loaded, [html] should be an empty string.
 | ||||
| 	html: string; | ||||
| 
 | ||||
| 	// Allow a secure origin to load content from any other origin.
 | ||||
| 	// Defaults to 'never'.
 | ||||
| 	// See react-native-webview's prop with the same name.
 | ||||
| 	mixedContentMode?: 'never' | 'always'; | ||||
| 
 | ||||
| 	allowFileAccessFromJs?: boolean; | ||||
| 	hasPluginScripts?: boolean; | ||||
| 
 | ||||
| 	// Initial javascript. Must evaluate to true.
 | ||||
| 	injectedJavaScript: string; | ||||
| 
 | ||||
| 	// iOS only: Scroll the outer content of the view. Set this to `false` if
 | ||||
| 	// the main view container doesn't scroll.
 | ||||
| 	scrollEnabled?: boolean; | ||||
| 
 | ||||
| 	style?: StyleProp<ViewStyle>; | ||||
| 	onMessage: OnMessageCallback; | ||||
| 	onError?: OnErrorCallback; | ||||
| 	onLoadStart?: OnLoadCallback; | ||||
| 	onLoadEnd?: OnLoadCallback; | ||||
| 
 | ||||
| 	// Triggered when the file containing [html] is overwritten with new content.
 | ||||
| 	onFileUpdate?: OnFileUpdateCallback; | ||||
| 
 | ||||
| 	// Defaults to the resource directory
 | ||||
| 	baseDirectory?: string; | ||||
| } | ||||
| export { WebViewControl, Props }; | ||||
| 
 | ||||
| const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => { | ||||
| 	const webviewRef = useRef(null); | ||||
| @@ -124,11 +68,6 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => { | ||||
| 				baseUrl, | ||||
| 			}; | ||||
| 			setSource(newSource); | ||||
| 
 | ||||
| 			props.onFileUpdate?.({ | ||||
| 				...newSource, | ||||
| 				filePath: tempFile, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		if (props.html && props.html.length > 0) { | ||||
| @@ -140,7 +79,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => { | ||||
| 		return () => { | ||||
| 			cancelled = true; | ||||
| 		}; | ||||
| 	}, [props.html, props.webviewInstanceId, props.onFileUpdate, baseDirectory, baseUrl]); | ||||
| 	}, [props.html, props.webviewInstanceId, baseDirectory, baseUrl]); | ||||
| 
 | ||||
| 	const onError = useCallback((event: WebViewErrorEvent) => { | ||||
| 		logger.error('Error', event.nativeEvent.description); | ||||
							
								
								
									
										46
									
								
								packages/app-mobile/components/ExtendedWebView/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								packages/app-mobile/components/ExtendedWebView/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import { StyleProp, ViewStyle } from 'react-native'; | ||||
| import { WebViewErrorEvent } from 'react-native-webview/lib/WebViewTypes'; | ||||
|  | ||||
| export interface WebViewControl { | ||||
| 	// Evaluate the given [script] in the context of the page. | ||||
| 	// Unlike react-native-webview/WebView, this does not need to return true. | ||||
| 	injectJS(script: string): void; | ||||
|  | ||||
| 	// message must be convertible to JSON | ||||
| 	postMessage(message: unknown): void; | ||||
| } | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needs to interface with old code from before rule was applied. | ||||
| export type OnMessageEvent = { nativeEvent: { data: any } }; | ||||
|  | ||||
| export type OnMessageCallback = (event: OnMessageEvent)=> void; | ||||
| export type OnErrorCallback = (event: WebViewErrorEvent)=> void; | ||||
| export type OnLoadCallback = ()=> void; | ||||
|  | ||||
| export interface Props { | ||||
| 	// A name to be associated with the WebView (e.g. NoteEditor) | ||||
| 	// This name should be unique. | ||||
| 	webviewInstanceId: string; | ||||
| 	testID?: string; | ||||
| 	hasPluginScripts?: boolean; | ||||
|  | ||||
| 	// Forwarded to RN WebView | ||||
| 	scrollEnabled?: boolean; | ||||
| 	allowFileAccessFromJs?: boolean; | ||||
| 	mixedContentMode?: 'never'|'always'; | ||||
|  | ||||
| 	// If HTML is still being loaded, [html] should be an empty string. | ||||
| 	html: string; | ||||
|  | ||||
| 	// Initial javascript. Must evaluate to true. | ||||
| 	injectedJavaScript: string; | ||||
|  | ||||
| 	style?: StyleProp<ViewStyle>; | ||||
| 	onMessage: OnMessageCallback; | ||||
| 	onError?: OnErrorCallback; | ||||
| 	onLoadStart?: OnLoadCallback; | ||||
| 	onLoadEnd?: OnLoadCallback; | ||||
|  | ||||
| 	// Defaults to the resource directory | ||||
| 	baseDirectory?: string; | ||||
| } | ||||
| @@ -0,0 +1,181 @@ | ||||
| import * as React from 'react'; | ||||
|  | ||||
| import { describe, it, beforeEach } from '@jest/globals'; | ||||
| import { render, screen, waitFor } from '@testing-library/react-native'; | ||||
| import '@testing-library/jest-native/extend-expect'; | ||||
|  | ||||
|  | ||||
| import NoteBodyViewer from './NoteBodyViewer'; | ||||
| import Setting from '@joplin/lib/models/Setting'; | ||||
| import { MenuProvider } from 'react-native-popup-menu'; | ||||
| import { resourceFetcher, setupDatabaseAndSynchronizer, supportDir, switchClient, synchronizerStart } from '@joplin/lib/testing/test-utils'; | ||||
| import { MarkupLanguage } from '@joplin/renderer'; | ||||
| import { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage'; | ||||
| import Resource from '@joplin/lib/models/Resource'; | ||||
| import shim from '@joplin/lib/shim'; | ||||
| import Note from '@joplin/lib/models/Note'; | ||||
|  | ||||
| interface WrapperProps { | ||||
| 	noteBody: string; | ||||
| 	highlightedKeywords?: string[]; | ||||
| 	noteResources?: unknown; | ||||
| 	onJoplinLinkClick?: HandleMessageCallback; | ||||
| 	onScroll?: (percent: number)=> void; | ||||
| 	onMarkForDownload?: OnMarkForDownloadCallback; | ||||
| } | ||||
|  | ||||
| const emptyObject = {}; | ||||
| const emptyArray: string[] = []; | ||||
| const noOpFunction = () => {}; | ||||
| const WrappedNoteViewer: React.FC<WrapperProps> = ( | ||||
| 	{ | ||||
| 		noteBody, | ||||
| 		highlightedKeywords = emptyArray, | ||||
| 		noteResources = emptyObject, | ||||
| 		onJoplinLinkClick = noOpFunction, | ||||
| 		onScroll = noOpFunction, | ||||
| 		onMarkForDownload, | ||||
| 	}: WrapperProps, | ||||
| ) => { | ||||
| 	return <MenuProvider> | ||||
| 		<NoteBodyViewer | ||||
| 			themeId={Setting.THEME_LIGHT} | ||||
| 			style={emptyObject} | ||||
| 			noteBody={noteBody} | ||||
| 			noteMarkupLanguage={MarkupLanguage.Markdown} | ||||
| 			highlightedKeywords={highlightedKeywords} | ||||
| 			noteResources={noteResources} | ||||
| 			paddingBottom={0} | ||||
| 			initialScroll={0} | ||||
| 			noteHash={''} | ||||
| 			onJoplinLinkClick={onJoplinLinkClick} | ||||
| 			onMarkForDownload={onMarkForDownload} | ||||
| 			onScroll={onScroll} | ||||
| 			pluginStates={emptyObject} | ||||
| 		/> | ||||
| 	</MenuProvider>; | ||||
| }; | ||||
|  | ||||
| const getNoteViewerDom = async (): Promise<Document> => { | ||||
| 	const webviewContent = await screen.findByTestId('NoteBodyViewer'); | ||||
| 	expect(webviewContent).toBeVisible(); | ||||
|  | ||||
| 	await waitFor(() => { | ||||
| 		expect(!!webviewContent.props.document).toBe(true); | ||||
| 	}); | ||||
|  | ||||
| 	// Return the composite ExtendedWebView component | ||||
| 	// See https://callstack.github.io/react-native-testing-library/docs/advanced/testing-env#tree-navigation | ||||
| 	return webviewContent.props.document; | ||||
| }; | ||||
|  | ||||
| describe('NoteBodyViewer', () => { | ||||
| 	beforeEach(async () => { | ||||
| 		await setupDatabaseAndSynchronizer(0); | ||||
| 		await switchClient(0); | ||||
| 	}); | ||||
|  | ||||
| 	afterEach(() => { | ||||
| 		screen.unmount(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should render markdown and re-render on change', async () => { | ||||
| 		render(<WrappedNoteViewer noteBody='# Test'/>); | ||||
|  | ||||
| 		const expectHeaderToBe = async (text: string) => { | ||||
| 			const noteViewer = await getNoteViewerDom(); | ||||
| 			await waitFor(async () => { | ||||
| 				expect(noteViewer.querySelector('h1').textContent).toBe(text); | ||||
| 			}); | ||||
| 		}; | ||||
|  | ||||
| 		await expectHeaderToBe('Test'); | ||||
| 		screen.rerender(<WrappedNoteViewer noteBody='# Test 2'/>); | ||||
| 		await expectHeaderToBe('Test 2'); | ||||
| 		screen.rerender(<WrappedNoteViewer noteBody='# Test 3'/>); | ||||
| 		await expectHeaderToBe('Test 3'); | ||||
| 	}); | ||||
|  | ||||
| 	it.each([ | ||||
| 		{ keywords: ['match'], body: 'A match and another match. Both should be highlighted.', expectedMatchCount: 2 }, | ||||
| 		{ keywords: ['test'], body: 'No match.', expectedMatchCount: 0 }, | ||||
| 		{ keywords: ['a', 'b'], body: 'a, a, a, b, b, b', expectedMatchCount: 6 }, | ||||
| 	])('should highlight search terms (case %#)', async ({ keywords, body, expectedMatchCount }) => { | ||||
| 		render( | ||||
| 			<WrappedNoteViewer | ||||
| 				highlightedKeywords={keywords} | ||||
| 				noteBody={body} | ||||
| 			/>, | ||||
| 		); | ||||
|  | ||||
| 		let noteViewerDom = await getNoteViewerDom(); | ||||
| 		await waitFor(() => { | ||||
| 			expect(noteViewerDom.querySelectorAll('.highlighted-keyword')).toHaveLength(expectedMatchCount); | ||||
| 		}); | ||||
|  | ||||
| 		// Should update highlights when the keywords change | ||||
| 		screen.rerender( | ||||
| 			<WrappedNoteViewer | ||||
| 				highlightedKeywords={[]} | ||||
| 				noteBody={body} | ||||
| 			/>, | ||||
| 		); | ||||
| 		noteViewerDom = await getNoteViewerDom(); | ||||
| 		await waitFor(() => { | ||||
| 			expect(noteViewerDom.querySelectorAll('.highlighted-keyword')).toHaveLength(0); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	it('tapping on resource download icons should mark the resources for download', async () => { | ||||
| 		await setupDatabaseAndSynchronizer(1); | ||||
| 		await switchClient(1); | ||||
|  | ||||
| 		let note1 = await Note.save({ title: 'Note 1', parent_id: '' }); | ||||
| 		note1 = await shim.attachFileToNote(note1, `${supportDir}/photo.jpg`); | ||||
|  | ||||
| 		await synchronizerStart(); | ||||
| 		await switchClient(0); | ||||
| 		Setting.setValue('sync.resourceDownloadMode', 'manual'); | ||||
| 		await synchronizerStart(); | ||||
|  | ||||
| 		const allResources = await Resource.all(); | ||||
| 		expect(allResources.length).toBe(1); | ||||
| 		const localResource = allResources[0]; | ||||
| 		const localState = await Resource.localState(localResource); | ||||
| 		expect(localState.fetch_status).toBe(Resource.FETCH_STATUS_IDLE); | ||||
|  | ||||
| 		const onMarkForDownload: OnMarkForDownloadCallback = jest.fn(({ resourceId }) => { | ||||
| 			return resourceFetcher().markForDownload([resourceId]); | ||||
| 		}); | ||||
| 		render( | ||||
| 			<WrappedNoteViewer | ||||
| 				noteBody={note1.body} | ||||
| 				noteResources={{ [localResource.id]: { localState, item: localResource } }} | ||||
| 				onMarkForDownload={onMarkForDownload} | ||||
| 			/>, | ||||
| 		); | ||||
|  | ||||
| 		// The resource placeholder should have rendered | ||||
| 		const noteViewerDom = await getNoteViewerDom(); | ||||
| 		let resourcePlaceholder: HTMLElement|null = null; | ||||
| 		await waitFor(() => { | ||||
| 			const placeholders = noteViewerDom.querySelectorAll<HTMLElement>(`[data-resource-id=${JSON.stringify(localResource.id)}]`); | ||||
| 			expect(placeholders).toHaveLength(1); | ||||
| 			resourcePlaceholder = placeholders[0]; | ||||
| 		}); | ||||
|  | ||||
| 		expect([...resourcePlaceholder.classList]).toContain('resource-status-notDownloaded'); | ||||
|  | ||||
| 		// Clicking on the placeholder should download its resource | ||||
| 		await waitFor(() => { | ||||
| 			resourcePlaceholder.click(); | ||||
| 			expect(onMarkForDownload).toHaveBeenCalled(); | ||||
| 		}); | ||||
|  | ||||
| 		await resourceFetcher().waitForAllFinished(); | ||||
|  | ||||
| 		await waitFor(async () => { | ||||
| 			expect(await Resource.localState(localResource.id)).toMatchObject({ fetch_status: Resource.FETCH_STATUS_DONE }); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
| @@ -2,7 +2,7 @@ import * as React from 'react'; | ||||
|  | ||||
| import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage'; | ||||
| import { useRef, useCallback, useState, useMemo } from 'react'; | ||||
| import { View } from 'react-native'; | ||||
| import { View, ViewStyle } from 'react-native'; | ||||
| import BackButtonDialogBox from '../BackButtonDialogBox'; | ||||
| import ExtendedWebView, { WebViewControl } from '../ExtendedWebView'; | ||||
| import useOnResourceLongPress from './hooks/useOnResourceLongPress'; | ||||
| @@ -14,13 +14,13 @@ import Setting from '@joplin/lib/models/Setting'; | ||||
| import uuid from '@joplin/lib/uuid'; | ||||
| import { PluginStates } from '@joplin/lib/services/plugins/reducer'; | ||||
| import useContentScripts from './hooks/useContentScripts'; | ||||
| import { MarkupLanguage } from '@joplin/renderer'; | ||||
|  | ||||
| interface Props { | ||||
| 	themeId: number; | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 	style: any; | ||||
| 	style: ViewStyle; | ||||
| 	noteBody: string; | ||||
| 	noteMarkupLanguage: number; | ||||
| 	noteMarkupLanguage: MarkupLanguage; | ||||
| 	highlightedKeywords: string[]; | ||||
| 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied | ||||
| 	noteResources: any; | ||||
| @@ -111,6 +111,7 @@ export default function NoteBodyViewer(props: Props) { | ||||
| 			<ExtendedWebView | ||||
| 				ref={webviewRef} | ||||
| 				webviewInstanceId='NoteBodyViewer' | ||||
| 				testID='NoteBodyViewer' | ||||
| 				html={html} | ||||
| 				allowFileAccessFromJs={true} | ||||
| 				injectedJavaScript={injectedJs} | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| /** @jest-environment jsdom */ | ||||
| import Setting from '@joplin/lib/models/Setting'; | ||||
| import Renderer, { RendererSettings, RendererSetupOptions } from './Renderer'; | ||||
| import shim from '@joplin/lib/shim'; | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import Renderer from './Renderer'; | ||||
| declare global { | ||||
| 	interface Window { | ||||
| 		rendererWebViewOptions: RendererWebViewOptions; | ||||
| 		webviewLib: { postMessage: (message: string)=> void }; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -32,6 +33,8 @@ webviewLib.initialize({ | ||||
| 		messenger.remoteApi.onPostMessage(message); | ||||
| 	}, | ||||
| }); | ||||
| // Share the webview library globally so that the renderer can access it. | ||||
| window.webviewLib = webviewLib; | ||||
|  | ||||
| const renderer = new Renderer({ | ||||
| 	...window.rendererWebViewOptions, | ||||
| @@ -58,5 +61,5 @@ const onMainContentScroll = () => { | ||||
| // scroll. However, window.addEventListener('scroll', callback) does. | ||||
| // - iOS needs a listener to be added to scrollingElement -- events aren't received when | ||||
| //   the listener is added to window with window.addEventListener('scroll', ...). | ||||
| document.scrollingElement.addEventListener('scroll', onMainContentScroll); | ||||
| document.scrollingElement?.addEventListener('scroll', onMainContentScroll); | ||||
| window.addEventListener('scroll', onMainContentScroll); | ||||
|   | ||||
| @@ -19,7 +19,10 @@ const getEditIconSrc = (theme: Theme) => { | ||||
| 	// Copy in the background -- the edit icon popover script doesn't need the | ||||
| 	// icon immediately. | ||||
| 	void (async () => { | ||||
| 		await shim.fsDriver().copy(iconUri, destPath); | ||||
| 		// Can be '' in a testing environment. | ||||
| 		if (iconUri) { | ||||
| 			await shim.fsDriver().copy(iconUri, destPath); | ||||
| 		} | ||||
| 	})(); | ||||
|  | ||||
| 	return destPath; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Dispatch, RefObject, SetStateAction, useEffect, useMemo } from 'react'; | ||||
| import { Dispatch, RefObject, SetStateAction, useEffect, useMemo, useRef } from 'react'; | ||||
| import { WebViewControl } from '../../ExtendedWebView'; | ||||
| import { OnScrollCallback, OnWebViewMessageHandler } from '../types'; | ||||
| import RNToWebViewMessenger from '../../../utils/ipc/RNToWebViewMessenger'; | ||||
| @@ -35,11 +35,17 @@ const onPostPluginMessage = async (contentScriptId: string, message: any) => { | ||||
| }; | ||||
|  | ||||
| const useRenderer = (props: Props) => { | ||||
| 	const onScrollRef = useRef(props.onScroll); | ||||
| 	onScrollRef.current = props.onScroll; | ||||
|  | ||||
| 	const onPostMessageRef = useRef(props.onPostMessage); | ||||
| 	onPostMessageRef.current = props.onPostMessage; | ||||
|  | ||||
| 	const messenger = useMemo(() => { | ||||
| 		const fsDriver = shim.fsDriver(); | ||||
| 		const localApi = { | ||||
| 			onScroll: props.onScroll, | ||||
| 			onPostMessage: props.onPostMessage, | ||||
| 			onScroll: (fraction: number) => onScrollRef.current?.(fraction), | ||||
| 			onPostMessage: (message: string) => onPostMessageRef.current?.(message), | ||||
| 			onPostPluginMessage, | ||||
| 			fsDriver: { | ||||
| 				writeFile: async (path: string, content: string, encoding?: string) => { | ||||
| @@ -58,7 +64,7 @@ const useRenderer = (props: Props) => { | ||||
| 		return new RNToWebViewMessenger<NoteViewerRemoteApi, NoteViewerLocalApi>( | ||||
| 			'note-viewer', props.webviewRef, localApi, | ||||
| 		); | ||||
| 	}, [props.onScroll, props.onPostMessage, props.webviewRef, props.tempDir]); | ||||
| 	}, [props.webviewRef, props.tempDir]); | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		props.setOnWebViewMessage(() => (event: WebViewMessageEvent) => { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { WebViewMessageEvent } from 'react-native-webview'; | ||||
| import { OnMessageEvent } from '../ExtendedWebView/types'; | ||||
|  | ||||
| export type OnScrollCallback = (scrollTop: number)=> void; | ||||
| export type OnWebViewMessageHandler = (event: WebViewMessageEvent)=> void; | ||||
| export type OnWebViewMessageHandler = (event: OnMessageEvent)=> void; | ||||
|   | ||||
| @@ -7,11 +7,11 @@ import { themeStyle } from '@joplin/lib/theme'; | ||||
| import { Theme } from '@joplin/lib/themes/type'; | ||||
| import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; | ||||
| import { Alert, BackHandler } from 'react-native'; | ||||
| import { WebViewMessageEvent } from 'react-native-webview'; | ||||
| import ExtendedWebView, { WebViewControl } from '../../ExtendedWebView'; | ||||
| import { clearAutosave, writeAutosave } from './autosave'; | ||||
| import { LocalizedStrings } from './js-draw/types'; | ||||
| import VersionInfo from 'react-native-version-info'; | ||||
| import { OnMessageEvent } from '../../ExtendedWebView/types'; | ||||
|  | ||||
|  | ||||
| const logger = Logger.create('ImageEditor'); | ||||
| @@ -280,7 +280,7 @@ const ImageEditor = (props: Props) => { | ||||
| 		})();`); | ||||
| 	}, [webviewRef, props.resourceFilename]); | ||||
|  | ||||
| 	const onMessage = useCallback(async (event: WebViewMessageEvent) => { | ||||
| 	const onMessage = useCallback(async (event: OnMessageEvent) => { | ||||
| 		const data = event.nativeEvent.data; | ||||
| 		if (data.startsWith('error:')) { | ||||
| 			logger.error('ImageEditor:', data); | ||||
|   | ||||
| @@ -78,5 +78,7 @@ describe('NoteEditor', () => { | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		wrappedNoteEditor.unmount(); | ||||
| 	}); | ||||
| }); | ||||
|   | ||||
| @@ -21,11 +21,11 @@ import { EditorCommandType, EditorKeymap, EditorLanguageType, SearchState } from | ||||
| import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting'; | ||||
| import useCodeMirrorPlugins from './hooks/useCodeMirrorPlugins'; | ||||
| import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger'; | ||||
| import { WebViewMessageEvent } from 'react-native-webview'; | ||||
| import { WebViewErrorEvent } from 'react-native-webview/lib/RNCWebViewNativeComponent'; | ||||
| import Logger from '@joplin/utils/Logger'; | ||||
| import { PluginStates } from '@joplin/lib/services/plugins/reducer'; | ||||
| import useEditorCommandHandler from './hooks/useEditorCommandHandler'; | ||||
| import { OnMessageEvent } from '../ExtendedWebView/types'; | ||||
| import { join, dirname } from 'path'; | ||||
| import * as mimeUtils from '@joplin/lib/mime-utils'; | ||||
| import uuid from '@joplin/lib/uuid'; | ||||
| @@ -466,7 +466,7 @@ function NoteEditor(props: Props, ref: any) { | ||||
| 		editorMessenger.onWebViewLoaded(); | ||||
| 	}, [editorMessenger]); | ||||
|  | ||||
| 	const onMessage = useCallback((event: WebViewMessageEvent) => { | ||||
| 	const onMessage = useCallback((event: OnMessageEvent) => { | ||||
| 		const data = event.nativeEvent.data; | ||||
|  | ||||
| 		if (data.indexOf('error:') === 0) { | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| /** @jest-environment jsdom */ | ||||
|  | ||||
| import CommandService from '@joplin/lib/services/CommandService'; | ||||
| import useEditorCommandHandler from './useEditorCommandHandler'; | ||||
| import commandDeclarations from '../commandDeclarations'; | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| import BasePluginRunner from '@joplin/lib/services/plugins/BasePluginRunner'; | ||||
| import PluginApiGlobal from '@joplin/lib/services/plugins/api/Global'; | ||||
| import Plugin from '@joplin/lib/services/plugins/Plugin'; | ||||
| import { WebViewControl } from '../../components/ExtendedWebView'; | ||||
| import { WebViewControl } from '../ExtendedWebView'; | ||||
| import { RefObject } from 'react'; | ||||
| import { WebViewMessageEvent } from 'react-native-webview'; | ||||
| import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger'; | ||||
| import { PluginMainProcessApi, PluginWebViewApi } from './types'; | ||||
| import shim from '@joplin/lib/shim'; | ||||
| import Logger from '@joplin/utils/Logger'; | ||||
| import createOnLogHander from './utils/createOnLogHandler'; | ||||
| import { OnMessageEvent } from '../ExtendedWebView/types'; | ||||
|  | ||||
| const logger = Logger.create('PluginRunner'); | ||||
|  | ||||
| type MessageEventListener = (event: WebViewMessageEvent)=> boolean; | ||||
| type MessageEventListener = (event: OnMessageEvent)=> boolean; | ||||
|  | ||||
| export default class PluginRunner extends BasePluginRunner { | ||||
| 	private messageEventListeners: MessageEventListener[] = []; | ||||
| @@ -71,7 +71,7 @@ export default class PluginRunner extends BasePluginRunner { | ||||
| 		`); | ||||
| 	} | ||||
|  | ||||
| 	public onWebviewMessage = (event: WebViewMessageEvent) => { | ||||
| 	public onWebviewMessage = (event: OnMessageEvent) => { | ||||
| 		this.messageEventListeners = this.messageEventListeners.filter( | ||||
| 			// Remove all listeners that return false | ||||
| 			listener => listener(event), | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as React from 'react'; | ||||
| import ExtendedWebView, { WebViewControl } from '../../components/ExtendedWebView'; | ||||
| import ExtendedWebView, { WebViewControl } from '../ExtendedWebView'; | ||||
| import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | ||||
| import shim from '@joplin/lib/shim'; | ||||
| import PluginRunner from './PluginRunner'; | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| /** @jest-environment jsdom */ | ||||
| import getFormData from './getFormData'; | ||||
|  | ||||
| describe('getFormData', () => { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import * as React from 'react'; | ||||
| import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | ||||
| import { PluginHtmlContents, ViewInfo } from '@joplin/lib/services/plugins/reducer'; | ||||
| import ExtendedWebView, { WebViewControl } from '../../../components/ExtendedWebView'; | ||||
| import ExtendedWebView, { WebViewControl } from '../../ExtendedWebView'; | ||||
| import { ViewStyle } from 'react-native'; | ||||
| import usePlugin from '@joplin/lib/hooks/usePlugin'; | ||||
| import shim from '@joplin/lib/shim'; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { useMemo, RefObject } from 'react'; | ||||
| import { DialogMainProcessApi, DialogWebViewApi } from '../../types'; | ||||
| import Logger from '@joplin/utils/Logger'; | ||||
| import { WebViewControl } from '../../../../components/ExtendedWebView'; | ||||
| import { WebViewControl } from '../../../ExtendedWebView'; | ||||
| import createOnLogHander from '../../utils/createOnLogHandler'; | ||||
| import RNToWebViewMessenger from '../../../../utils/ipc/RNToWebViewMessenger'; | ||||
| import { SerializableData } from '@joplin/lib/utils/ipc/types'; | ||||
|   | ||||
| @@ -52,6 +52,8 @@ const backlinksPluginId = 'joplin.plugin.ambrt.backlinksToNote'; | ||||
|  | ||||
| describe('PluginStates.installed', () => { | ||||
| 	beforeEach(async () => { | ||||
| 		jest.useRealTimers(); | ||||
|  | ||||
| 		await setupDatabaseAndSynchronizer(0); | ||||
| 		await switchClient(0); | ||||
| 		reduxStore = createMockReduxStore(); | ||||
| @@ -60,6 +62,9 @@ describe('PluginStates.installed', () => { | ||||
|  | ||||
| 		await mockMobilePlatform('android'); | ||||
| 		await mockRepositoryApiConstructor(); | ||||
|  | ||||
| 		// Fake timers are necessary to prevent a warning. | ||||
| 		jest.useFakeTimers(); | ||||
| 	}); | ||||
| 	afterEach(async () => { | ||||
| 		for (const pluginId of PluginService.instance().pluginIds) { | ||||
| @@ -228,7 +233,7 @@ describe('PluginStates.installed', () => { | ||||
|  | ||||
| 		// After updating, the update button should read "updated". Use a large | ||||
| 		// timeout because updating plugins can be slow, particularly in CI. | ||||
| 		const updatedButton = await screen.findByRole('button', { name: 'Updated', disabled: true, timeout: 16000 }); | ||||
| 		const updatedButton = await screen.findByRole('button', { name: 'Updated', disabled: true }, { timeout: 16000 }); | ||||
| 		expect(updatedButton).toBeVisible(); | ||||
|  | ||||
| 		// Should be marked as updated. | ||||
|   | ||||
| @@ -34,6 +34,7 @@ let reduxStore: Store<AppState>; | ||||
|  | ||||
| describe('PluginStates.search', () => { | ||||
| 	beforeEach(async () => { | ||||
| 		jest.useRealTimers(); | ||||
| 		await setupDatabaseAndSynchronizer(0); | ||||
| 		await switchClient(0); | ||||
| 		reduxStore = createMockReduxStore(); | ||||
| @@ -42,6 +43,7 @@ describe('PluginStates.search', () => { | ||||
| 		resetRepoApi(); | ||||
|  | ||||
| 		await mockRepositoryApiConstructor(); | ||||
| 		jest.useFakeTimers(); | ||||
| 	}); | ||||
|  | ||||
| 	it('should find results', async () => { | ||||
|   | ||||
| @@ -12,7 +12,7 @@ module.exports = { | ||||
| 		'\\.(ts|tsx)$': 'ts-jest', | ||||
| 	}, | ||||
|  | ||||
| 	testEnvironment: 'jsdom', | ||||
| 	testEnvironment: 'node', | ||||
| 	testMatch: ['**/*.test.(ts|tsx)'], | ||||
|  | ||||
| 	testPathIgnorePatterns: ['<rootDir>/node_modules/'], | ||||
| @@ -20,7 +20,7 @@ module.exports = { | ||||
|  | ||||
| 	// Do transform most packages in node_modules (transformations correct unrecognized | ||||
| 	// import syntax) | ||||
| 	transformIgnorePatterns: ['<rootDir>/node_modules/jest', '<rootDir>/node_modules/js-draw'], | ||||
| 	transformIgnorePatterns: ['<rootDir>/node_modules/jest', '<rootDir>/node_modules/js-draw', 'node_modules/jsdom'], | ||||
|  | ||||
| 	slowTestThreshold: 40, | ||||
| }; | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| /* eslint-disable jest/require-top-level-describe */ | ||||
|  | ||||
| const { afterEachCleanUp, afterAllCleanUp } = require('@joplin/lib/testing/test-utils.js'); | ||||
| const shim = require('@joplin/lib/shim').default; | ||||
| const { shimInit } = require('@joplin/lib/shim-init-node.js'); | ||||
| const injectedJs = require('./utils/injectedJs.js').default; | ||||
| const { mkdir, rm } = require('fs-extra'); | ||||
| const path = require('path'); | ||||
| const sharp = require('sharp'); | ||||
| const { tmpdir } = require('os'); | ||||
| const uuid = require('@joplin/lib/uuid').default; | ||||
| const sqlite3 = require('sqlite3'); | ||||
| @@ -19,7 +22,14 @@ window.setImmediate = setImmediate; | ||||
| shimInit({ | ||||
| 	nodeSqlite: sqlite3, | ||||
| 	React, | ||||
| 	sharp, | ||||
| }); | ||||
| shim.injectedJs = (name) => { | ||||
| 	if (!(name in injectedJs)) { | ||||
| 		throw new Error(`Cannot find injected JS with ID ${name}`); | ||||
| 	} | ||||
| 	return injectedJs[name]; | ||||
| }; | ||||
|  | ||||
| // This library has the following error when running within Jest: | ||||
| //   Invariant Violation: `new NativeEventEmitter()` requires a non-null argument. | ||||
| @@ -40,17 +50,27 @@ jest.doMock('react-native-version-info', () => { | ||||
| }); | ||||
|  | ||||
| // react-native-webview expects native iOS/Android code so needs to be mocked. | ||||
| jest.mock('react-native-webview', () => { | ||||
| 	const { View } = require('react-native'); | ||||
| 	return { | ||||
| 		WebView: View, | ||||
| 	}; | ||||
| jest.mock('./components/ExtendedWebView', () => { | ||||
| 	return require('./components/ExtendedWebView/index.jest.js'); | ||||
| }); | ||||
|  | ||||
| jest.mock('@react-native-clipboard/clipboard', () => { | ||||
| 	return { default: { getString: jest.fn(), setString: jest.fn() } }; | ||||
| }); | ||||
|  | ||||
| jest.mock('react-native-share', () => { | ||||
| 	return { default: { } }; | ||||
| }); | ||||
|  | ||||
| // Used by the renderer | ||||
| jest.doMock('react-native-vector-icons/Ionicons', () => { | ||||
| 	return { | ||||
| 		default: class extends require('react-native').View { | ||||
| 			static getImageSourceSync = () => ({ uri: '' }); | ||||
| 		}, | ||||
| 	}; | ||||
| }); | ||||
|  | ||||
| // react-native-fs's CachesDirectoryPath export doesn't work in a testing environment. | ||||
| // Use a temporary folder instead. | ||||
| const tempDirectoryPath = path.join(tmpdir(), `appmobile-test-${uuid.createNano()}`); | ||||
|   | ||||
| @@ -112,6 +112,7 @@ | ||||
|     "nodemon": "3.0.3", | ||||
|     "punycode": "2.3.1", | ||||
|     "react-test-renderer": "18.2.0", | ||||
|     "sharp": "0.33.2", | ||||
|     "sqlite3": "5.1.6", | ||||
|     "ts-jest": "29.1.1", | ||||
|     "ts-loader": "9.5.1", | ||||
|   | ||||
							
								
								
									
										10
									
								
								packages/app-mobile/utils/injectedJs.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								packages/app-mobile/utils/injectedJs.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
|  | ||||
| const injectedJs = { | ||||
| 	webviewLib: require('@joplin/lib/rnInjectedJs/webviewLib'), | ||||
| 	codeMirrorBundle: require('../lib/rnInjectedJs/codeMirrorBundle.bundle'), | ||||
| 	svgEditorBundle: require('../lib/rnInjectedJs/svgEditorBundle.bundle'), | ||||
| 	pluginBackgroundPage: require('../lib/rnInjectedJs/pluginBackgroundPage.bundle'), | ||||
| 	noteBodyViewerBundle: require('../lib/rnInjectedJs/noteBodyViewerBundle.bundle'), | ||||
| }; | ||||
|  | ||||
| export default injectedJs; | ||||
| @@ -1,9 +1,9 @@ | ||||
|  | ||||
| import RemoteMessenger from '@joplin/lib/utils/ipc/RemoteMessenger'; | ||||
| import { SerializableData } from '@joplin/lib/utils/ipc/types'; | ||||
| import { WebViewMessageEvent } from 'react-native-webview'; | ||||
| import { WebViewControl } from '../../components/ExtendedWebView'; | ||||
| import { RefObject } from 'react'; | ||||
| import { OnMessageEvent } from '../../components/ExtendedWebView/types'; | ||||
|  | ||||
| export default class RNToWebViewMessenger<LocalInterface, RemoteInterface> extends RemoteMessenger<LocalInterface, RemoteInterface> { | ||||
| 	public constructor(channelId: string, private webviewControl: WebViewControl|RefObject<WebViewControl>, localApi: LocalInterface) { | ||||
| @@ -32,7 +32,7 @@ export default class RNToWebViewMessenger<LocalInterface, RemoteInterface> exten | ||||
| 		`); | ||||
| 	} | ||||
|  | ||||
| 	public onWebViewMessage = (event: WebViewMessageEvent) => { | ||||
| 	public onWebViewMessage = (event: OnMessageEvent) => { | ||||
| 		if (!this.hasBeenClosed()) { | ||||
| 			void this.onMessage(JSON.parse(event.nativeEvent.data)); | ||||
| 		} | ||||
|   | ||||
| @@ -14,14 +14,7 @@ import Resource from '@joplin/lib/models/Resource'; | ||||
| import { getLocales } from 'react-native-localize'; | ||||
| import { setLocale, defaultLocale, closestSupportedLocale } from '@joplin/lib/locale'; | ||||
| import type SettingType from '@joplin/lib/models/Setting'; | ||||
|  | ||||
| const injectedJs = { | ||||
| 	webviewLib: require('@joplin/lib/rnInjectedJs/webviewLib'), | ||||
| 	codeMirrorBundle: require('../lib/rnInjectedJs/codeMirrorBundle.bundle'), | ||||
| 	svgEditorBundle: require('../lib/rnInjectedJs/svgEditorBundle.bundle'), | ||||
| 	pluginBackgroundPage: require('../lib/rnInjectedJs/pluginBackgroundPage.bundle'), | ||||
| 	noteBodyViewerBundle: require('../lib/rnInjectedJs/noteBodyViewerBundle.bundle'), | ||||
| }; | ||||
| import injectedJs from './injectedJs'; | ||||
|  | ||||
| export default function shimInit() { | ||||
| 	shim.Geolocation = GeolocationReact; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user