From 443f060ef94cdc5b0879fb8e3c3ec8b2d0ac47ce Mon Sep 17 00:00:00 2001 From: Shawn Erquhart Date: Fri, 7 Dec 2018 16:49:55 -0500 Subject: [PATCH] fix(media-library-cloudinary): fix options, add tests (#1938) --- package.json | 1 + .../src/__tests__/index.spec.js | 292 ++++++++++++++++++ .../src/index.js | 10 +- yarn.lock | 19 +- 4 files changed, 318 insertions(+), 4 deletions(-) create mode 100644 packages/netlify-cms-media-library-cloudinary/src/__tests__/index.spec.js diff --git a/package.json b/package.json index 99c52de2..44aca783 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "cache-me-outside": "^0.0.4", "cross-env": "^5.1.4", "cypress": "^3.0.3", + "dom-testing-library": "^3.13.0", "eslint": "^5.3.0", "eslint-plugin-react": "^7.10.0", "friendly-errors-webpack-plugin": "^1.7.0", diff --git a/packages/netlify-cms-media-library-cloudinary/src/__tests__/index.spec.js b/packages/netlify-cms-media-library-cloudinary/src/__tests__/index.spec.js new file mode 100644 index 00000000..d8bdda95 --- /dev/null +++ b/packages/netlify-cms-media-library-cloudinary/src/__tests__/index.spec.js @@ -0,0 +1,292 @@ +import { queryHelpers, waitForElement } from 'dom-testing-library'; +import cloudinary from '../index'; + +describe('cloudinary media library', () => { + let mediaLibrary; + let cloudinaryScript; + let cloudinaryConfig; + let cloudinaryInsertHandler; + + beforeEach(() => { + /** + * Mock of the Cloudinary library itself, which is otherwise created by + * their script (which isn't actually run during testing). + */ + window.cloudinary = { + createMediaLibrary: (config, { insertHandler }) => { + cloudinaryConfig = config; + cloudinaryInsertHandler = insertHandler; + return mediaLibrary; + }, + }; + + /** + * Mock of the object returned by the Cloudinary createMediaLibrary method. + */ + mediaLibrary = { + show: jest.fn(), + hide: jest.fn(), + }; + + /** + * Every time the integration is initialized, a script tag is dynamically + * generated and added to the page. The initialization is on hold until + * the `load` event is broadcast, but that doesn't happen during testing, + * so we wait for the script tag to be added to the dom and then manually + * call its `onreadystatechange` method, which resolves the promise and + * allows initialization to continue. + * + * This also ensures that the script is being added to the DOM, and in a way + * that is not tied to script loading implementation details. + */ + waitForElement(() => { + const url = 'https://media-library.cloudinary.com/global/all.js'; + return queryHelpers.queryByAttribute('src', document, url); + }).then(script => { + cloudinaryScript = script; + script.onreadystatechange(); + }); + }); + + afterEach(() => { + /** + * Remove the script element from the dom after each test. + */ + if (cloudinaryScript) { + document.head.removeChild(cloudinaryScript); + } + }); + + it('exports an object with expected properties', () => { + expect(cloudinary).toMatchInlineSnapshot(` +Object { + "init": [Function], + "name": "cloudinary", +} +`); + }); + + describe('configuration', () => { + const defaultCloudinaryConfig = { + button_class: undefined, + inline_container: undefined, + insert_transformation: false, + z_index: '99999', + multiple: false, + }; + + it('has defaults', async () => { + await cloudinary.init(); + expect(cloudinaryConfig).toEqual(defaultCloudinaryConfig); + }); + + it('does not allow enforced values to be overridden', async () => { + const options = { + config: { + button_class: 'foo', + inline_container: 'foo', + insert_transformation: 'foo', + z_index: 0, + }, + }; + await cloudinary.init({ options }); + expect(cloudinaryConfig).toEqual(defaultCloudinaryConfig); + }); + + it('allows non-enforced defaults to be overridden', async () => { + const options = { + config: { + multiple: true, + }, + }; + await cloudinary.init({ options }); + expect(cloudinaryConfig).toEqual({ ...defaultCloudinaryConfig, ...options.config }); + }); + + it('allows unknown values', async () => { + const options = { + config: { + foo: 'bar', + }, + }; + await cloudinary.init({ options }); + expect(cloudinaryConfig).toEqual({ ...defaultCloudinaryConfig, ...options.config }); + }); + }); + + describe('insertHandler', () => { + let handleInsert; + const asset = { + url: 'http://foo.bar/image.jpg', + secure_url: 'https://foo.bar/image.jpg', + public_id: 'image', + format: 'jpg', + }; + const assetWithDerived = { + ...asset, + derived: [ + { + secure_url: 'https://derived.foo.bar/image.jpg', + url: 'http://derived.foo.bar/image.jpg', + }, + ], + }; + + beforeEach(() => { + handleInsert = jest.fn(); + }); + + it('calls insert function with single asset', async () => { + await cloudinary.init({ handleInsert }); + cloudinaryInsertHandler({ assets: [asset] }); + expect(handleInsert).toHaveBeenCalledWith(expect.any(String)); + }); + + it('calls insert function with multiple assets', async () => { + const options = { + config: { + multiple: true, + }, + }; + await cloudinary.init({ options, handleInsert }); + cloudinaryInsertHandler({ assets: [asset, asset] }); + expect(handleInsert).toHaveBeenCalledWith(expect.any(Array)); + }); + + it('calls insert function with secure url', async () => { + await cloudinary.init({ handleInsert }); + cloudinaryInsertHandler({ assets: [asset] }); + expect(handleInsert).toHaveBeenCalledWith(asset.secure_url); + }); + + it('calls insert function with insecure url', async () => { + const options = { + use_secure_url: false, + }; + await cloudinary.init({ options, handleInsert }); + cloudinaryInsertHandler({ assets: [asset] }); + expect(handleInsert).toHaveBeenCalledWith(asset.url); + }); + + it('supports derived assets', async () => { + await cloudinary.init({ handleInsert }); + cloudinaryInsertHandler({ assets: [assetWithDerived] }); + expect(handleInsert).toHaveBeenCalledWith(assetWithDerived.derived[0].secure_url); + }); + + it('ignores derived assets when use_transformations is false', async () => { + const options = { + use_transformations: false, + }; + await cloudinary.init({ options, handleInsert }); + cloudinaryInsertHandler({ assets: [assetWithDerived] }); + expect(handleInsert).toHaveBeenCalledWith(assetWithDerived.secure_url); + }); + + it('supports outputting filename only', async () => { + const options = { + output_filename_only: true, + }; + await cloudinary.init({ options, handleInsert }); + cloudinaryInsertHandler({ assets: [asset] }); + expect(handleInsert.mock.calls[0][0]).toMatchInlineSnapshot(`"image.jpg"`); + }); + }); + + describe('show method', () => { + const defaultOptions = { + config: { + multiple: false, + }, + }; + + it('calls cloudinary instance show method with default options', async () => { + const integration = await cloudinary.init(); + integration.show(); + expect(mediaLibrary.show).toHaveBeenCalledWith(defaultOptions); + }); + + it('accepts unknown configuration keys', async () => { + const showOptions = { + config: { + ...defaultOptions.config, + foo: 'bar', + }, + }; + const integration = await cloudinary.init(); + integration.show(showOptions); + expect(mediaLibrary.show).toHaveBeenCalledWith(showOptions); + }); + + it('receives global configuration for behavior only', async () => { + const behaviorOptions = { + default_transformations: [{ foo: 'bar' }], + max_files: 2, + multiple: true, + }; + const nonBehaviorOptions = { + api_key: 123, + }; + const options = { + config: { + ...behaviorOptions, + ...nonBehaviorOptions, + }, + }; + const expectedOptions = { + config: behaviorOptions, + }; + const integration = await cloudinary.init({ options }); + integration.show(); + expect(mediaLibrary.show).toHaveBeenCalledWith(expectedOptions); + }); + + it('allows global/default configuration to be overridden', async () => { + const showOptions = { + config: { + multiple: true, + }, + }; + const integration = await cloudinary.init(); + integration.show(showOptions); + expect(mediaLibrary.show).toHaveBeenCalledWith(showOptions); + }); + + it('enforces multiple: false if allowMultiple is true', async () => { + const options = { + config: { + multiple: true, + }, + }; + const showOptions = { + config: { + multiple: true, + }, + allowMultiple: false, + }; + const expectedOptions = { + config: { + multiple: false, + }, + }; + const integration = await cloudinary.init(options); + integration.show(showOptions); + expect(mediaLibrary.show).toHaveBeenCalledWith(expectedOptions); + }); + }); + + describe('hide method', () => { + it('calls cloudinary instance hide method', async () => { + const integration = await cloudinary.init(); + integration.hide(); + expect(mediaLibrary.hide).toHaveBeenCalled(); + }); + }); + + describe('enableStandalone method', () => { + it('returns true', async () => { + const integration = await cloudinary.init(); + expect(integration.enableStandalone()).toEqual(true); + }); + }); +}); diff --git a/packages/netlify-cms-media-library-cloudinary/src/index.js b/packages/netlify-cms-media-library-cloudinary/src/index.js index be933490..23211098 100644 --- a/packages/netlify-cms-media-library-cloudinary/src/index.js +++ b/packages/netlify-cms-media-library-cloudinary/src/index.js @@ -45,7 +45,11 @@ function getAssetUrl(asset, { use_secure_url, use_transformations, output_filena return urlObject[urlKey]; } -async function init({ options, handleInsert }) { +async function init({ options = {}, handleInsert } = {}) { + /** + * Configuration is specific to Cloudinary, while options are specific to this + * integration. + */ const { config: providedConfig = {}, ...integrationOptions } = options; const resolvedOptions = { ...defaultOptions, ...integrationOptions }; const cloudinaryConfig = { ...defaultConfig, ...providedConfig, ...enforcedConfig }; @@ -62,7 +66,7 @@ async function init({ options, handleInsert }) { const mediaLibrary = window.cloudinary.createMediaLibrary(cloudinaryConfig, { insertHandler }); return { - show: ({ config: instanceConfig = {}, allowMultiple }) => { + show: ({ config: instanceConfig = {}, allowMultiple } = {}) => { /** * Ensure multiple selection is not available if the field is configured * to disallow it. @@ -70,7 +74,7 @@ async function init({ options, handleInsert }) { if (allowMultiple === false) { instanceConfig.multiple = false; } - return mediaLibrary.show({ config: { ...cloudinaryBehaviorConfig, instanceConfig } }); + return mediaLibrary.show({ config: { ...cloudinaryBehaviorConfig, ...instanceConfig } }); }, hide: () => mediaLibrary.hide(), enableStandalone: () => true, diff --git a/yarn.lock b/yarn.lock index d9e468f2..f2854823 100644 --- a/yarn.lock +++ b/yarn.lock @@ -753,6 +753,13 @@ dependencies: regenerator-runtime "^0.12.0" +"@babel/runtime@^7.1.5": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.2.0.tgz#b03e42eeddf5898e00646e4c840fa07ba8dcad7f" + integrity sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg== + dependencies: + regenerator-runtime "^0.12.0" + "@babel/template@7.0.0-beta.54": version "7.0.0-beta.54" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.54.tgz#d5b0d2d2d55c0e78b048c61a058f36cfd7d91af3" @@ -4171,6 +4178,16 @@ dom-testing-library@^3.12.0: pretty-format "^23.6.0" wait-for-expect "^1.0.0" +dom-testing-library@^3.13.0: + version "3.13.0" + resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.13.0.tgz#3d9c48db2bc4629f097571612d138bf2e8c42421" + integrity sha512-ImIZQrsEPQkmXNFzYmOsCJBjaBcZJe4vRJfP55DhYySD2LL56ACPaJATbXphLGred5efqGC1Q4H3UuqWCZ9Bqg== + dependencies: + "@babel/runtime" "^7.1.5" + "@sheerun/mutationobserver-shim" "^0.3.2" + pretty-format "^23.6.0" + wait-for-expect "^1.1.0" + dom-walk@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" @@ -12341,7 +12358,7 @@ w3c-hr-time@^1.0.1: dependencies: browser-process-hrtime "^0.1.2" -wait-for-expect@^1.0.0: +wait-for-expect@^1.0.0, wait-for-expect@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-1.1.0.tgz#6607375c3f79d32add35cd2c87ce13f351a3d453" integrity sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg==