diff --git a/jetpack b/jetpack index b443d69447..81d188a99e 160000 --- a/jetpack +++ b/jetpack @@ -1 +1 @@ -Subproject commit b443d694471e1e4e9cc337ae16dd7f5b531ff797 +Subproject commit 81d188a99e4ad35b07718cc566b46442992467ad diff --git a/package.json b/package.json index 7549e7c6cf..23fc8af43f 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "prewpandroid": "rm -Rf $TMPDIR/gbmobile-wpandroidfakernroot && mkdir $TMPDIR/gbmobile-wpandroidfakernroot && ln -s $(cd \"$(dirname \"../../../\")\"; pwd) $TMPDIR/gbmobile-wpandroidfakernroot/android", "wpandroid": "cd gutenberg && react-native run-android --root $TMPDIR/gbmobile-wpandroidfakernroot --variant wasabiDebug --appIdSuffix beta --appFolder WordPress --main-activity=ui.WPLaunchActivity", "test": "cross-env NODE_ENV=test jest --verbose --config ./jest.config.js", + "test:update": "cross-env NODE_ENV=test jest --verbose --config ./jest.config.js --updateSnapshot", "test:debug": "cross-env NODE_ENV=test node --inspect-brk node_modules/.bin/jest --runInBand --verbose --config jest.config.js", "device-tests": "cross-env NODE_ENV=test jest --maxWorkers=2 --testPathIgnorePatterns='canary|gutenberg-editor-rendering' --verbose --config jest_ui.config.js", "device-tests-canary": "cross-env NODE_ENV=test jest --maxWorkers=2 --testPathPattern=@canary --verbose --config jest_ui.config.js", diff --git a/src/test/videopress/__snapshots__/upload.js.snap b/src/test/videopress/__snapshots__/upload.js.snap new file mode 100644 index 0000000000..77038ffadb --- /dev/null +++ b/src/test/videopress/__snapshots__/upload.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VideoPress block - Uploads adds video by inserting URL: video ready 1`] = `""`; + +exports[`VideoPress block - Uploads cancel upload 1`] = `""`; + +exports[`VideoPress block - Uploads finishes pending uploads upon opening the editor 1`] = `""`; + +exports[`VideoPress block - Uploads handles upload failure 1`] = `""`; + +exports[`VideoPress block - Uploads takes a video and uploads it: loading state 1`] = `""`; + +exports[`VideoPress block - Uploads takes a video and uploads it: video ready 1`] = `""`; + +exports[`VideoPress block - Uploads uploads a video from device: loading state 1`] = `""`; + +exports[`VideoPress block - Uploads uploads a video from device: video ready 1`] = `""`; + +exports[`VideoPress block - Uploads uploads a video from media library: video ready 1`] = `""`; diff --git a/src/test/videopress/upload.js b/src/test/videopress/upload.js new file mode 100644 index 0000000000..c21593946e --- /dev/null +++ b/src/test/videopress/upload.js @@ -0,0 +1,646 @@ +/** + * WordPress dependencies + */ +import { Platform } from '@wordpress/element'; +import apiFetch from '@wordpress/api-fetch'; +import { dispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { + requestImageFailedRetryDialog, + requestImageUploadCancelDialog, +} from '@wordpress/react-native-bridge'; + +/** + * External dependencies + */ +import { + act, + fireEvent, + getBlock, + getEditorHtml, + initializeEditor, + setupCoreBlocks, + setupMediaUpload, + setupMediaPicker, + within, + setupPicker, + setupApiFetch, +} from 'test/helpers'; +import { ActionSheetIOS } from 'react-native'; +import prompt from 'react-native-prompt-android'; + +/** + * Internal dependencies + */ +import { + registerJetpackBlocks, + setupJetpackEditor, +} from '../../jetpack-editor-setup'; + +jest.mock( '@wordpress/api-fetch' ); +jest.mock( 'react-native-prompt-android', () => jest.fn() ); + +const initialHtml = ''; + +const MEDIA_OPTIONS = [ + 'Choose from device', + 'Take a Video', + 'WordPress Media Library', + 'Insert from URL', +]; + +const VIDEOPRESS_GUID = 'AbCdEfGh'; +const FETCH_ITEMS = [ + { + request: { + path: `/wpcom/v2/media/videopress-playback-jwt/${ VIDEOPRESS_GUID }`, + method: 'POST', + body: {}, + }, + response: { + metadata_token: 'videopress-token', + }, + }, + { + request: { + path: `/rest/v1.1/videos/${ VIDEOPRESS_GUID }`, + credentials: 'omit', + global: true, + }, + response: { + description: 'video-description', + post_id: 1, + guid: VIDEOPRESS_GUID, + private_enabled_for_site: false, + title: 'video-title', + duration: 1200, + privacy_setting: 2, + original: `https://videos.files.wordpress.com/${ VIDEOPRESS_GUID }/video.mp4`, + allow_download: false, + display_embed: true, + poster: `https://videos.files.wordpress.com/${ VIDEOPRESS_GUID }/video.jpg`, + height: 270, + width: 480, + rating: 'G', + is_private: false, + }, + }, + { + request: { + path: `/oembed/1.0/proxy?url=https%3A%2F%2Fvideopress.com%2Fv%2F${ VIDEOPRESS_GUID }%3FresizeToParent%3Dtrue%26cover%3Dtrue%26preloadContent%3Dmetadata`, + }, + response: { + height: 338, + provider_name: 'VideoPress', + html: ``, + width: 600, + type: 'video', + }, + }, +]; + +const sendWebViewMessage = ( webView, message ) => + fireEvent( webView, 'message', { + nativeEvent: { + data: JSON.stringify( message ), + }, + } ); + +setupCoreBlocks(); + +beforeAll( () => { + // Register VideoPress block + setupJetpackEditor( { + blogId: 1, + isJetpackActive: true, + } ); + registerJetpackBlocks( { + capabilities: { videoPressBlock: true }, + } ); + + // Mock request reponses + setupApiFetch( FETCH_ITEMS ); +} ); + +beforeEach( () => { + // Invalidate `getEmbedPreview` resolutions to avoid + // caching the preview for the same VideoPress GUID. + dispatch( coreStore ).invalidateResolutionForStoreSelector( + 'getEmbedPreview' + ); +} ); + +describe( 'VideoPress block - Uploads', () => { + it( 'displays media options picker when selecting the block', async () => { + // Initialize with an empty gallery + const { + getByLabelText, + getByText, + getByTestId, + } = await initializeEditor( { + initialHtml, + } ); + + fireEvent.press( getByText( 'ADD VIDEO' ) ); + + // Observe that media options picker is displayed + if ( Platform.isIOS ) { + // On iOS the picker is rendered natively, so we have + // to check the arguments passed to `ActionSheetIOS`. + expect( + ActionSheetIOS.showActionSheetWithOptions + ).toHaveBeenCalledWith( + expect.objectContaining( { + title: 'Choose video', + options: [ 'Cancel', ...MEDIA_OPTIONS ], + } ), + expect.any( Function ) + ); + } else { + expect( getByText( 'Choose video' ) ).toBeVisible(); + MEDIA_OPTIONS.forEach( ( option ) => + expect( getByText( option ) ).toBeVisible() + ); + + fireEvent( getByTestId( 'media-options-picker' ), 'backdropPress' ); + } + + // Check that block remains selected after displaying the media + // options picker. This is performed by checking if the block + // actions menu button is visible. + const blockActionsButton = getByLabelText( /Open Block Actions Menu/ ); + expect( blockActionsButton ).toBeVisible(); + } ); + + it( 'uploads a video from device', async () => { + const media = { + type: 'video', + localId: 1, + localUrl: 'file:///local-video-1.mp4', + serverId: 1000, + serverUrl: 'https://videopress.wordpress.com/local-video-1.mp4', + }; + + const { notifyUploadingState, notifySucceedState } = setupMediaUpload(); + const { + expectMediaPickerCall, + mediaPickerCallback, + } = setupMediaPicker(); + + const screen = await initializeEditor( { + initialHtml, + } ); + const { getByText, getByTestId } = screen; + const { selectOption } = setupPicker( screen, MEDIA_OPTIONS ); + // Clear previous calls to `apiFetch` + apiFetch.mockClear(); + + // Block is visible + const block = await getBlock( screen, 'VideoPress' ); + expect( block ).toBeVisible(); + + // Upload video from device + fireEvent.press( getByText( 'ADD VIDEO' ) ); + selectOption( 'Choose from device' ); + expectMediaPickerCall( 'DEVICE_MEDIA_LIBRARY', [ 'video' ], false ); + + // Block is uploading the video + await mediaPickerCallback( media ); + expect( getByTestId( 'videopress-uploading-video' ) ).toBeVisible(); + expect( getEditorHtml() ).toMatchSnapshot( 'loading state' ); + + // During upload progress we keep displaying the loading state + await notifyUploadingState( media ); + expect( getByTestId( 'videopress-uploading-video' ) ).toBeVisible(); + + // Upload finish + await notifySucceedState( { + ...media, + metadata: Platform.select( { + android: { + videopressGUID: VIDEOPRESS_GUID, + }, + ios: { + id: VIDEOPRESS_GUID, + }, + } ), + } ); + + // Requests: + // - Token request + // - Metadata request + // - Check ownership request + // - Oembed request + FETCH_ITEMS.forEach( ( fetch ) => + expect( apiFetch ).toHaveBeenCalledWith( fetch.request ) + ); + + // Check loading overlay is displayed before the player is ready + expect( within( block ).getByText( 'Loading' ) ).toBeVisible(); + + // Notify the player is ready + const player = getByTestId( 'videopress-player' ); + sendWebViewMessage( player, { + type: 'message', + event: 'videopress_ready', + } ); + expect( player ).toBeVisible(); + + // At this point the player should be showing the conversion state. + // Hence, let's notify the loaded state. + sendWebViewMessage( player, { + type: 'message', + event: 'videopress_loading_state', + state: 'loaded', + } ); + + // At this point the player should be ready to be used. + expect( within( block ).queryByText( 'Loading' ) ).toBeNull(); + expect( getEditorHtml() ).toMatchSnapshot( 'video ready' ); + } ); + + it( 'uploads a video from media library', async () => { + const media = { + type: 'video', + id: 2000, + url: 'https://test.files.wordpress.com/local-video-2.mp4', + metadata: { + videopressGUID: VIDEOPRESS_GUID, + }, + }; + const { + expectMediaPickerCall, + mediaPickerCallback, + } = setupMediaPicker(); + + const screen = await initializeEditor( { + initialHtml, + } ); + const { getByText, getByTestId } = screen; + const { selectOption } = setupPicker( screen, MEDIA_OPTIONS ); + // Clear previous calls to `apiFetch` + apiFetch.mockClear(); + + // Block is visible + const block = await getBlock( screen, 'VideoPress' ); + expect( block ).toBeVisible(); + + // Add video from WordPress media library + fireEvent.press( getByText( 'ADD VIDEO' ) ); + selectOption( 'WordPress Media Library' ); + expectMediaPickerCall( 'SITE_MEDIA_LIBRARY', [ 'video' ], false ); + + await mediaPickerCallback( media ); + + // Requests: + // - Token request + // - Metadata request + // - Check ownership request + // - Oembed request + FETCH_ITEMS.forEach( ( fetch ) => + expect( apiFetch ).toHaveBeenCalledWith( fetch.request ) + ); + + // Check loading overlay is displayed before the player is ready + expect( within( block ).getByText( 'Loading' ) ).toBeVisible(); + + // Notify the player is ready + const player = getByTestId( 'videopress-player' ); + sendWebViewMessage( player, { + type: 'message', + event: 'videopress_ready', + } ); + expect( player ).toBeVisible(); + + // At this point the player should be showing the conversion state. + // Hence, let's notify the loaded state. + sendWebViewMessage( player, { + type: 'message', + event: 'videopress_loading_state', + state: 'loaded', + } ); + + // At this point the player should be ready to be used. + expect( within( block ).queryByText( 'Loading' ) ).toBeNull(); + expect( getEditorHtml() ).toMatchSnapshot( 'video ready' ); + } ); + + it( 'takes a video and uploads it', async () => { + const media = { + type: 'video', + localId: 3, + localUrl: 'file:///local-video-3.mp4', + serverId: 3000, + serverUrl: 'https://videopress.wordpress.com/local-video-3.mp4', + }; + + const { notifyUploadingState, notifySucceedState } = setupMediaUpload(); + const { + expectMediaPickerCall, + mediaPickerCallback, + } = setupMediaPicker(); + + const screen = await initializeEditor( { + initialHtml, + } ); + const { getByText, getByTestId } = screen; + const { selectOption } = setupPicker( screen, MEDIA_OPTIONS ); + // Clear previous calls to `apiFetch` + apiFetch.mockClear(); + + // Block is visible + const block = await getBlock( screen, 'VideoPress' ); + expect( block ).toBeVisible(); + + // Take a video and upload it + fireEvent.press( getByText( 'ADD VIDEO' ) ); + selectOption( 'Take a Video' ); + expectMediaPickerCall( 'DEVICE_CAMERA', [ 'video' ], false ); + + // Block is uploading the video + await mediaPickerCallback( media ); + expect( getByTestId( 'videopress-uploading-video' ) ).toBeVisible(); + expect( getEditorHtml() ).toMatchSnapshot( 'loading state' ); + + // During upload progress we keep displaying the loading state + await notifyUploadingState( media ); + expect( getByTestId( 'videopress-uploading-video' ) ).toBeVisible(); + + // Upload finish + await notifySucceedState( { + ...media, + metadata: Platform.select( { + android: { + videopressGUID: VIDEOPRESS_GUID, + }, + ios: { + id: VIDEOPRESS_GUID, + }, + } ), + } ); + + // Requests: + // - Token request + // - Metadata request + // - Check ownership request + // - Oembed request + FETCH_ITEMS.forEach( ( fetch ) => + expect( apiFetch ).toHaveBeenCalledWith( fetch.request ) + ); + + // Check loading overlay is displayed before the player is ready + expect( within( block ).getByText( 'Loading' ) ).toBeVisible(); + + // Notify the player is ready + const player = getByTestId( 'videopress-player' ); + sendWebViewMessage( player, { + type: 'message', + event: 'videopress_ready', + } ); + expect( player ).toBeVisible(); + + // At this point the player should be showing the conversion state. + // Hence, let's notify the loaded state. + sendWebViewMessage( player, { + type: 'message', + event: 'videopress_loading_state', + state: 'loaded', + } ); + + // At this point the player should be ready to be used. + expect( within( block ).queryByText( 'Loading' ) ).toBeNull(); + expect( getEditorHtml() ).toMatchSnapshot( 'video ready' ); + } ); + + it( 'adds video by inserting URL', async () => { + let promptApply; + prompt.mockImplementation( ( title, message, [ , apply ] ) => { + promptApply = apply.onPress; + } ); + + const screen = await initializeEditor( { + initialHtml, + } ); + const { getByText, getByTestId } = screen; + const { selectOption } = setupPicker( screen, MEDIA_OPTIONS ); + // Clear previous calls to `apiFetch` + apiFetch.mockClear(); + + // Block is visible + const block = await getBlock( screen, 'VideoPress' ); + expect( block ).toBeVisible(); + + // Add video from WordPress media library + fireEvent.press( getByText( 'ADD VIDEO' ) ); + selectOption( 'Insert from URL' ); + expect( prompt ).toHaveBeenCalled(); + + // Mock prompt dialog + await act( () => + promptApply( `https://videopress.com/v/${ VIDEOPRESS_GUID }` ) + ); + + // Requests: + // - Token request + // - Metadata request + // - Check ownership request + // - Oembed request + FETCH_ITEMS.forEach( ( fetch ) => + expect( apiFetch ).toHaveBeenCalledWith( fetch.request ) + ); + + // Check loading overlay is displayed before the player is ready + expect( within( block ).getByText( 'Loading' ) ).toBeVisible(); + + // Notify the player is ready + const player = getByTestId( 'videopress-player' ); + sendWebViewMessage( player, { + type: 'message', + event: 'videopress_ready', + } ); + expect( player ).toBeVisible(); + + // At this point the player should be showing the conversion state. + // Hence, let's notify the loaded state. + sendWebViewMessage( player, { + type: 'message', + event: 'videopress_loading_state', + state: 'loaded', + } ); + + // At this point the player should be ready to be used. + expect( within( block ).queryByText( 'Loading' ) ).toBeNull(); + expect( getEditorHtml() ).toMatchSnapshot( 'video ready' ); + } ); + + it( 'finishes pending uploads upon opening the editor', async () => { + const media = { + type: 'video', + localId: 4, + localUrl: 'file:///local-video-4.mp4', + serverId: 4000, + serverUrl: 'https://videopress.wordpress.com/local-video-4.mp4', + }; + const { notifyUploadingState, notifySucceedState } = setupMediaUpload(); + + const screen = await initializeEditor( { + initialHtml: ``, + } ); + const { getByTestId } = screen; + + // Block is visible + const block = await getBlock( screen, 'VideoPress' ); + expect( block ).toBeVisible(); + + // Notify that the media items are uploading + await notifyUploadingState( media ); + await notifyUploadingState( media ); + + // During upload progress we keep displaying the loading state + expect( getByTestId( 'videopress-uploading-video' ) ).toBeVisible(); + + // Upload finish + await notifySucceedState( { + ...media, + metadata: Platform.select( { + android: { + videopressGUID: VIDEOPRESS_GUID, + }, + ios: { + id: VIDEOPRESS_GUID, + }, + } ), + } ); + + // Requests: + // - Token request + // - Metadata request + // - Check ownership request + // - Oembed request + FETCH_ITEMS.forEach( ( fetch ) => + expect( apiFetch ).toHaveBeenCalledWith( fetch.request ) + ); + + // Check loading overlay is displayed before the player is ready + expect( within( block ).getByText( 'Loading' ) ).toBeVisible(); + + // Notify the player is ready + const player = getByTestId( 'videopress-player' ); + sendWebViewMessage( player, { + type: 'message', + event: 'videopress_ready', + } ); + expect( player ).toBeVisible(); + + // At this point the player should be showing the conversion state. + // Hence, let's notify the loaded state. + sendWebViewMessage( player, { + type: 'message', + event: 'videopress_loading_state', + state: 'loaded', + } ); + + // At this point the player should be ready to be used. + expect( within( block ).queryByText( 'Loading' ) ).toBeNull(); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + + it( 'handles upload failure', async () => { + const media = { + type: 'video', + localId: 1, + localUrl: 'file:///local-video-1.mp4', + serverId: 1000, + serverUrl: 'https://videopress.wordpress.com/local-video-1.mp4', + }; + + const { notifyUploadingState, notifyFailedState } = setupMediaUpload(); + const { + expectMediaPickerCall, + mediaPickerCallback, + } = setupMediaPicker(); + + const screen = await initializeEditor( { + initialHtml, + } ); + const { getByText, getByTestId } = screen; + const { selectOption } = setupPicker( screen, MEDIA_OPTIONS ); + + // Block is visible + const block = await getBlock( screen, 'VideoPress' ); + expect( block ).toBeVisible(); + + // Upload video from device + fireEvent.press( getByText( 'ADD VIDEO' ) ); + selectOption( 'Choose from device' ); + expectMediaPickerCall( 'DEVICE_MEDIA_LIBRARY', [ 'video' ], false ); + + // Block is uploading the video + await mediaPickerCallback( media ); + await notifyUploadingState( media ); + + // During upload progress we keep displaying the loading state + expect( getByTestId( 'videopress-uploading-video' ) ).toBeVisible(); + + // Notify that the upload failed + await notifyFailedState( media ); + const uploadFailText = getByText( /Failed to insert media/ ); + expect( uploadFailText ).toBeVisible(); + + // Retry option available + fireEvent.press( uploadFailText ); + expect( requestImageFailedRetryDialog ).toHaveBeenCalledWith( + media.localId + ); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + + it( 'cancel upload', async () => { + const media = { + type: 'video', + localId: 1, + localUrl: 'file:///local-video-1.mp4', + serverId: 1000, + serverUrl: 'https://videopress.wordpress.com/local-video-1.mp4', + }; + + const { notifyUploadingState, notifyResetState } = setupMediaUpload(); + const { + expectMediaPickerCall, + mediaPickerCallback, + } = setupMediaPicker(); + + const screen = await initializeEditor( { + initialHtml, + } ); + const { getByText, getByTestId } = screen; + const { selectOption } = setupPicker( screen, MEDIA_OPTIONS ); + + // Block is visible + const block = await getBlock( screen, 'VideoPress' ); + expect( block ).toBeVisible(); + + // Upload video from device + fireEvent.press( getByText( 'ADD VIDEO' ) ); + selectOption( 'Choose from device' ); + expectMediaPickerCall( 'DEVICE_MEDIA_LIBRARY', [ 'video' ], false ); + + // Block is uploading the video + await mediaPickerCallback( media ); + await notifyUploadingState( media ); + + // During upload progress we keep displaying the loading state + const uploadingVideoView = getByTestId( 'videopress-uploading-video' ); + expect( uploadingVideoView ).toBeVisible(); + + // Cancel upload + fireEvent.press( uploadingVideoView ); + expect( requestImageUploadCancelDialog ).toHaveBeenCalledWith( + media.localId + ); + await notifyResetState( media ); + + expect( within( block ).queryByText( 'Loading' ) ).toBeNull(); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); +} );