import React from 'react'; import { waitFor, act } from '@testing-library/react'; import { ResourceBrowser, ResourceBrowserProps } from './index'; import { mockSource, mockResource } from './__mocks__/MockModels'; import { renderWithContext } from './__mocks__/renderWithContext'; import { ResourceBrowserPlugin, ResourceBrowserSource, ResourceBrowserSourceWithPlugin, PluginLaunchModeType } from './types'; import * as RBI from './ResourceBrowserInput/ResourceBrowserInput'; jest.spyOn(RBI, 'ResourceBrowserInput'); import * as Plugin from './Plugin/Plugin'; import * as useSources from './Hooks/useSources'; jest.spyOn(useSources, 'useSources'); var useSourceReloadMock = jest.fn(); jest.mock('./Hooks/useSources', () => { const actual = jest.requireActual('./Hooks/useSources'); return { useSources: jest.fn((...args) => { const actualResult = actual.useSources(...args); return { ...actualResult, reload: useSourceReloadMock, }; }), }; }); describe('Resource browser input', () => { const mockChange = jest.fn(); const mockOnClear = jest.fn(); const mockRequestSources = jest.fn().mockResolvedValue([]); const mockRenderSelectedResource = jest.fn(); const mockSourceBrowserComponent = jest.fn().mockReturnValue(() =>
Source UI has been rendered
); const mockResolveResource = jest.fn(); const mockUseResolveResource = jest.fn().mockReturnValue({ data: null, error: null, isLoading: false, }); const mockDamPlugin = { type: 'dam', resolveResource: mockResolveResource, renderSelectedResource: mockRenderSelectedResource, sourceBrowserComponent: mockSourceBrowserComponent, useResolveResource: mockUseResolveResource, sourceSearchComponent: jest.fn(), renderResourceLauncher: jest.fn(), } as unknown as ResourceBrowserPlugin; const mockMatrixPlugin = { type: 'matrix', resolveResource: mockResolveResource, renderSelectedResource: mockRenderSelectedResource, sourceBrowserComponent: mockSourceBrowserComponent, useResolveResource: mockUseResolveResource, sourceSearchComponent: jest.fn(), renderResourceLauncher: jest.fn(), } as ResourceBrowserPlugin; const plugins = [mockMatrixPlugin, mockDamPlugin]; const renderComponent = (props: Partial = {}, searchEnabled?: boolean) => { return renderWithContext( , { onRequestSources: mockRequestSources, plugins, searchEnabled: !!searchEnabled, }, ); }; // Internally the plugin is attached to the source so handle that for tests expectation const calculateExpectedSource = (source: ResourceBrowserSource): ResourceBrowserSourceWithPlugin => { return { ...source, plugin: mockDamPlugin, }; }; it('allowedPlugins will restrict which plugins are used', async () => { // Works as expected with no restriction (useSources.useSources as jest.Mock).mockClear(); renderComponent(); let expectedPlugins = plugins; await waitFor(() => { expect(useSources.useSources).toHaveBeenLastCalledWith( expect.objectContaining({ plugins: expectedPlugins, }), ); }); // Works as expected with restrictions (useSources.useSources as jest.Mock).mockClear(); renderComponent({ allowedPlugins: ['matrix'] }); expectedPlugins = plugins.filter((plugin) => plugin.type === 'matrix'); await waitFor(() => { expect(useSources.useSources).toHaveBeenLastCalledWith( expect.objectContaining({ plugins: expectedPlugins, }), ); }); }); it('If only one valid source is provided will default to its Source and Plugin', async () => { const source = mockSource({ type: 'dam' }); mockRequestSources.mockResolvedValueOnce([source]); renderComponent(); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ source: calculateExpectedSource(source), plugin: mockDamPlugin, }), {}, ); }); }); it('If a resource is provided will default to its Source and Plugin to match', async () => { const source = mockSource({ type: 'dam' }); mockRequestSources.mockResolvedValueOnce([source, mockSource({ type: 'matrix' })]); mockResolveResource.mockResolvedValueOnce(mockResource({ source })); renderComponent({ value: { sourceId: source.id, resourceId: '100' } }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ source: calculateExpectedSource(source), plugin: mockDamPlugin, pluginMode: null, }), {}, ); }); }); it('If an aliased resource is provided will default to its true Source and Plugin to match', async () => { const source = mockSource({ type: 'dam', aliases: ['alias-source-id'] }); mockRequestSources.mockResolvedValueOnce([source, mockSource({ type: 'matrix' })]); mockResolveResource.mockResolvedValueOnce(mockResource({ source })); renderComponent({ value: { sourceId: 'alias-source-id', resourceId: '100' } }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ source: calculateExpectedSource(source), plugin: mockDamPlugin, pluginMode: null, }), {}, ); }); }); it('If a resource is provided but its Source cannot be found it will error', async () => { const source = mockSource({ type: 'dam' }); mockRequestSources.mockResolvedValueOnce([source, mockSource({ type: 'matrix' })]); mockResolveResource.mockResolvedValueOnce(mockResource({ source })); renderComponent({ value: { sourceId: 'not a real id', resourceId: '100' } }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ error: expect.any(Error), source: null, plugin: null, }), {}, ); }); }); it('onSourceSelect will alter source passed to ResourceBrowserInput', async () => { const sourcesInput = [mockSource({ type: 'dam' }), mockSource()]; const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source)); mockRequestSources.mockResolvedValueOnce(sourcesInput); renderComponent(); // Expect no source or plugin await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ sources: calculatedSources, source: null, plugin: null, }), {}, ); }); // Get the provided callback const { setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; // Invoke it act(() => { setSource(calculatedSources[0]); }); // Expect the source and plugin to be loaded await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ source: calculatedSources[0], plugin: mockDamPlugin, pluginMode: null, }), {}, ); }); }); it('onSourceSelect will set mode if provided', async () => { const sourcesInput = [mockSource({ type: 'dam' }), mockSource()]; const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source)); mockRequestSources.mockResolvedValueOnce(sourcesInput); renderComponent(); // Expect no source or plugin await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ sources: calculatedSources, source: null, plugin: null, }), {}, ); }); // Get the provided callback const { setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; // Invoke it act(() => { setSource(calculatedSources[0], { type: PluginLaunchModeType.Search, args: { query: 'myQuery' } }); }); // Expect the source and plugin to be loaded await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ source: calculatedSources[0], plugin: mockDamPlugin, pluginMode: { type: PluginLaunchModeType.Search, args: { query: 'myQuery' } }, }), {}, ); }); }); it("When source is selected it will pass down its plugin's useResolveResource to maintain hook rules", async () => { const source = mockSource({ type: 'dam' }); const resource = { resourceId: '12', sourceId: source.id }; mockRequestSources.mockResolvedValueOnce([source]); renderComponent({ value: resource }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ value: resource, source: calculateExpectedSource(source), plugin: mockDamPlugin, }), {}, ); }); await waitFor(() => { expect(mockUseResolveResource).toHaveBeenCalledWith(resource, calculateExpectedSource(source)); }); }); it('Value will only be passed to ResourceBrowserInput if value source matches currently selected source', async () => { const sourcesInput = [mockSource({ id: '1' }), mockSource({ id: '2' })]; const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source)); mockRequestSources.mockResolvedValueOnce(sourcesInput); const value = { sourceId: sourcesInput[0].id, resourceId: '123456' }; renderComponent({ value }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenLastCalledWith( expect.objectContaining({ value, isOtherSourceValue: false, source: calculatedSources[0], }), {}, ); }); const { onModalStateChange, setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; // Invoke open the modal act(() => { onModalStateChange(true); }); // Change the source act(() => { setSource(calculatedSources[1]); }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenLastCalledWith( expect.objectContaining({ value: null, isOtherSourceValue: true, source: calculatedSources[1], }), {}, ); }); }); it('onModalStateChange called with false will reset the Source and Plugin', async () => { const sourcesInput = [mockSource({ type: 'dam' }), mockSource()]; const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source)); mockRequestSources.mockResolvedValueOnce(sourcesInput); renderComponent(); // Expect no source or plugin await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ sources: calculatedSources, source: null, plugin: null, }), {}, ); }); // Get the provided callback const { setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; // Invoke it act(() => { setSource(calculatedSources[0]); }); // Expect the source and plugin to be loaded await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ source: calculatedSources[0], plugin: mockDamPlugin, }), {}, ); }); // Invoke modal close callback const { onModalStateChange } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; // Invoke open and close the modal act(() => { onModalStateChange(true); }); act(() => { onModalStateChange(false); }); // Expect no source or plugin await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ sources: calculatedSources, source: null, plugin: null, pluginMode: null, }), {}, ); }); }); it('onModalStateChange called with false will not reset the Source and Plugin if only one source exists', async () => { const sources = [mockSource({ type: 'dam' })]; mockRequestSources.mockResolvedValueOnce(sources); renderComponent(); // Expect the source and plugin to be loaded await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ source: calculateExpectedSource(sources[0]), plugin: mockDamPlugin, }), {}, ); }); // Invoke modal close callback const { onModalStateChange } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; act(() => { onModalStateChange(true); }); act(() => { onModalStateChange(false); }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ source: calculateExpectedSource(sources[0]), plugin: mockDamPlugin, }), {}, ); }); }); it('onModalStateChange called with false will reset Source and Plugin if only one source exists AND search is enabled', async () => { const sourcesInput = [mockSource({ type: 'dam' })]; const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source)); mockRequestSources.mockResolvedValueOnce(sourcesInput); renderComponent({}, true); // Expect no source or plugin await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ sources: calculatedSources, source: null, plugin: null, }), {}, ); }); // Get the provided callback const { setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; // Invoke it act(() => { setSource(calculatedSources[0]); }); // Expect the source and plugin to be loaded await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ source: calculatedSources[0], plugin: mockDamPlugin, }), {}, ); }); // Invoke modal close callback const { onModalStateChange } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; // Invoke open and close the modal act(() => { onModalStateChange(true); }); act(() => { onModalStateChange(false); }); // Expect no source or plugin await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ sources: calculatedSources, source: null, plugin: null, pluginMode: null, }), {}, ); }); }); it('onModalStateChange called with false will reset the Source and Plugin to the value defaults if a value exists', async () => { const sourcesInput = [mockSource({ id: '1' }), mockSource({ id: '2' })]; const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source)); mockRequestSources.mockResolvedValueOnce(sourcesInput); renderComponent({ value: { sourceId: sourcesInput[0].id, resourceId: '123456' } }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ sources: calculatedSources, source: calculatedSources[0], plugin: mockDamPlugin, }), {}, ); }); const { onModalStateChange, setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; // Invoke open and close the modal act(() => { onModalStateChange(true); }); // Change the source act(() => { setSource(calculatedSources[1]); }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ source: calculatedSources[1], plugin: mockDamPlugin, }), {}, ); }); act(() => { onModalStateChange(false); }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenLastCalledWith( expect.objectContaining({ source: calculatedSources[0], plugin: mockDamPlugin, }), {}, ); }); }); it('onModalStateChange called with false will reset the Source and Plugin to the aliases true source if the value uses an alias', async () => { const originalSource = mockSource({ id: 'original-source-id', name: 'Original Source', type: 'dam', aliases: ['alias-source-id'], }); const aliasSourceId = 'alias-source-id'; const valueWithAlias = { sourceId: aliasSourceId, resourceId: '123456', }; const sourcesInput = [originalSource, mockSource({ id: '2' })]; const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source)); mockRequestSources.mockResolvedValueOnce(sourcesInput); renderComponent({ value: valueWithAlias }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ value: valueWithAlias, sources: calculatedSources, source: calculatedSources[0], plugin: mockDamPlugin, }), {}, ); }); const { onModalStateChange, setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; // Invoke open and close the modal act(() => { onModalStateChange(true); }); // Change the source act(() => { setSource(calculatedSources[1]); }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ source: calculatedSources[1], plugin: mockDamPlugin, }), {}, ); }); act(() => { onModalStateChange(false); }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenLastCalledWith( expect.objectContaining({ source: calculatedSources[0], plugin: mockDamPlugin, }), {}, ); }); }); it('onModalStateChange called with false will reset the Mode', async () => { const sourcesInput = [mockSource({ type: 'dam' }), mockSource()]; mockRequestSources.mockResolvedValueOnce(sourcesInput); renderComponent({ value: { sourceId: sourcesInput[0].id, resourceId: '123456' } }); // Select a component and plugin mode (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0].setSource(sourcesInput[0], { type: 'browse' }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ pluginMode: { type: 'browse', }, }), {}, ); }); // Invoke modal close callback const { onModalStateChange } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; act(() => { onModalStateChange(true); }); act(() => { onModalStateChange(false); }); // Expect the mode to be reset await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenLastCalledWith( expect.objectContaining({ pluginMode: null, }), {}, ); }); }); it('onRetry reloads sources', async () => { const sourcesInput = [mockSource({ type: 'dam' }), mockSource()]; mockRequestSources.mockResolvedValueOnce(sourcesInput); renderComponent({ value: { sourceId: sourcesInput[0].id, resourceId: '123456' } }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalled(); }); const { onRetry } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; act(() => { onRetry(); }); await waitFor(() => { expect(useSourceReloadMock).toHaveBeenCalled(); }); }); it('onModalStateChange calls onModalStateChangeExternalNotification when provided', async () => { const mockOnModalStateChange = jest.fn(); const sourcesInput = [mockSource({ type: 'dam' })]; mockRequestSources.mockResolvedValueOnce(sourcesInput); renderComponent({ onModalStateChange: mockOnModalStateChange }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalled(); }); const { onModalStateChange } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; // Test opening modal act(() => { onModalStateChange(true); }); await waitFor(() => { expect(mockOnModalStateChange).toHaveBeenCalledWith(true); }); // Test closing modal act(() => { onModalStateChange(false); }); await waitFor(() => { expect(mockOnModalStateChange).toHaveBeenCalledWith(false); }); expect(mockOnModalStateChange).toHaveBeenCalledTimes(2); }); describe('Resource browser plugin', () => { let PluginRenderSpy: jest.SpyInstance; beforeEach(() => { //@ts-ignore PluginRenderSpy = jest.spyOn(Plugin.PluginRender, 'render'); }); afterEach(() => { PluginRenderSpy.mockRestore(); }); it('Will default to a non plugin based render for initial load and selection of first source', async () => { const sourcesInput = [mockSource({ type: 'dam' }), mockSource({ type: 'matrix' })]; mockRequestSources.mockResolvedValue(sourcesInput); renderComponent(); // Will render a default with no selected source await waitFor(() => { expect(PluginRenderSpy).toHaveBeenCalledWith( expect.objectContaining({ render: true, type: null, plugin: null, }), null, ); expect(PluginRenderSpy).toHaveBeenCalledWith( expect.objectContaining({ render: false, type: 'dam', }), null, ); expect(PluginRenderSpy).toHaveBeenCalledWith( expect.objectContaining({ render: false, type: 'matrix', }), null, ); }); }); it('Will only send render=true to the Plugin for the currently selected source', async () => { const sourcesInput = [mockSource({ type: 'dam' }), mockSource({ type: 'matrix' })]; mockRequestSources.mockResolvedValue(sourcesInput); // Render with an input so it will default a source renderComponent({ value: { sourceId: sourcesInput[0].id, resourceId: '123456' } }); // Will render a default with no selected source await waitFor(() => { expect(PluginRenderSpy).toHaveBeenCalledWith( expect.objectContaining({ render: false, type: null, }), null, ); expect(PluginRenderSpy).toHaveBeenCalledWith( expect.objectContaining({ render: true, type: 'dam', }), null, ); expect(PluginRenderSpy).toHaveBeenCalledWith( expect.objectContaining({ render: false, type: 'matrix', }), null, ); }); // renderComponent({ value: { sourceId: sourcesInput[0].id, resourceId: '123456' } }); }); it('Will match plugin to render based on type', async () => { const sourcesInput = [mockSource({ type: 'dam' }), mockSource({ type: 'matrix' })]; const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source)); mockRequestSources.mockResolvedValue(sourcesInput); renderComponent(); // Modify the source so the plugin object comparison doesnt match calculatedSources[0] = { ...calculatedSources[0], plugin: { // @ts-ignore test: 'test', ...calculatedSources[0].plugin, }, }; // Get the provided callback const { setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0]; // Invoke it act(() => { setSource(calculatedSources[0]); }); // Will render a default with no selected source await waitFor(() => { expect(PluginRenderSpy).toHaveBeenCalledWith( expect.objectContaining({ render: true, type: null, }), null, ); expect(PluginRenderSpy).toHaveBeenCalledWith( expect.objectContaining({ render: true, type: 'dam', }), null, ); expect(PluginRenderSpy).toHaveBeenCalledWith( expect.objectContaining({ render: false, type: 'matrix', }), null, ); }); }); it('switching source updates plugin and does not use the old plugin', async () => { const damSource = mockSource({ type: 'dam', id: '1' }); const matrixSource = mockSource({ type: 'matrix', id: '2' }); const pluginA = { ...mockDamPlugin, type: 'dam' }; const pluginB = { ...mockDamPlugin, type: 'matrix' }; mockRequestSources.mockResolvedValueOnce([damSource, matrixSource]); renderComponent(); const { setSource } = (RBI.ResourceBrowserInput as jest.Mock).mock.calls[0][0]; act(() => { setSource({ ...damSource, plugin: pluginA }); }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ plugin: pluginA, }), expect.any(Object), ); }); (PluginRenderSpy as unknown as jest.Mock).mockClear(); act(() => { setSource({ ...matrixSource, plugin: pluginB }); }); await waitFor(() => { expect(PluginRenderSpy).toHaveBeenCalledWith( expect.objectContaining({ plugin: pluginB, }), expect.any(Object), ); expect(PluginRenderSpy).not.toHaveBeenCalledWith( expect.objectContaining({ plugin: pluginA, }), expect.any(Object), ); }); }); it('When resource browser is called with a value containing an alias source, Plugin is called with the original source not the alias value', async () => { const originalSource = mockSource({ id: 'original-source-id', name: 'Original Source', type: 'dam', aliases: ['alias-source-id'], }); const aliasSourceId = 'alias-source-id'; mockRequestSources.mockResolvedValueOnce([originalSource]); // Create a value that uses the alias source ID const valueWithAlias = { sourceId: aliasSourceId, resourceId: '123456', }; renderComponent({ value: valueWithAlias }); await waitFor(() => { expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith( expect.objectContaining({ value: valueWithAlias, source: calculateExpectedSource(originalSource), // Should be the original source, not alias plugin: mockDamPlugin, }), {}, ); }); // Verify that the PluginRender is called with the original source await waitFor(() => { expect(PluginRenderSpy).toHaveBeenCalledWith( expect.objectContaining({ render: true, type: 'dam', source: calculateExpectedSource(originalSource), // Should be the original source isOtherSourceValue: false, }), null, ); }); }); }); });