diff --git a/.editorconfig b/.editorconfig index 6b1aa0c8..72823be4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,5 @@ end_of_line=lf charset=utf-8 trim_trailing_whitespace=true insert_final_newline=true - -[*.js] indent_style=space indent_size=2 diff --git a/.gitignore b/.gitignore index 4600e634..94492937 100644 --- a/.gitignore +++ b/.gitignore @@ -57,5 +57,5 @@ node_modules # codeceptjs e2e/output -# cypress -cypress \ No newline at end of file +# cypress +cypress diff --git a/.travis.yml b/.travis.yml index 5014c5a4..34f5210d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ os: linux -dist: trusty +dist: xenial language: node_js @@ -10,6 +10,7 @@ python: - "3.6" addons: +# https://stackoverflow.com/questions/57903415/travis-ci-chrome-62-instead-of-77 chrome: stable firefox: "67.0" diff --git a/README.md b/README.md index 797200d0..9e00ac53 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ typeformEmbed.makeWidget(element, url, options) | hideFooter | Hide typeform footer, that appears showing the progress bar and the navigation buttons. | `Boolean` | false | | hideHeaders | Hide typeform header, that appears when you have a Question group, or a long question that you need to scroll through to answer, like a Multiple Choice block. | `Boolean` | false | | onSubmit | Callback function that will be executed right after the typeform is successfully submitted. | `Function` | - | + | onReady | Callback function that will be executed once the typeform is ready. | `Function` | - | #### Example: @@ -73,6 +74,9 @@ typeformEmbed.makeWidget(element, url, options) hideScrollbars: true, onSubmit: function () { console.log('Typeform successfully submitted') + }, + onReady: function () { + console.log('Typeform is ready') } } ) @@ -99,6 +103,10 @@ typeformEmbed.makePopup(url, options) | hideHeaders | Hide typeform header, that appears when you have a Question group, or a long question that you need to scroll through to answer, like a Multiple Choice block. | `Boolean` | false | | drawerWidth | Specify the width of the drawer (only applies if using `mode` `"drawer_left"` or `"drawer_right"`). | `Number` (pixels) | 800 | | onSubmit | Callback function that will be executed right after the typeform is successfully submitted. | `Function` | - | + | onReady | Callback function that will be executed once the typeform is ready. | + `Function` | - | + | onClose | Callback function that will be executed once the typeform is closed. | + `Function` | - | #### Example: @@ -112,6 +120,12 @@ typeformEmbed.makePopup(url, options) hideScrollbars: true, onSubmit: function () { console.log('Typeform successfully submitted') + }, + onReady: function () { + console.log('Typeform is ready') + }, + onClose: function () { + console.log('Typeform is closed') } } ) @@ -140,15 +154,15 @@ Although we have no hard limit, we recommend having a height of at least 350px. We use `position: fixed` to position our modal relative to its containing block established by the viewport. If one of the modal ancestors has a `transform`, `perspective`, or `filter` css property set to something other than `none` the positioning will be relative to it and probably not visible by the user. ## Tests -In order to run visual tests, it is need an applitools key. -- Add a `.env` file in your root, you can look at the `.env.example` +In order to run visual tests, you need an applitools key. +- Add a `.env` file in your root, you can look at the `.env.example` - Add your api key `EYES_API_KEY=HERE_GOES_YOUR_KEY` - (Optional) You can add the url, by default the url is `http://localhost:8080` - Start the server `yarn start` - Run the tests with `yarn test` - - This command will run all the tests. That means **unit tests**, **integration tests** and **visual tests** -This is the list of all the test commands, if you want to run them one by one: +This is the list of all the test commands, if you want to run them one by one: - `yarn test:unit` --> Runs unit tests - `yarn test:functional` --> Runs cross browser functional tests with Cypress - `yarn test:visual` --> Runs visual tests with CodeceptJs, WebDriver, and Applitools. diff --git a/package.json b/package.json index e284049e..d044d5e1 100644 --- a/package.json +++ b/package.json @@ -21,29 +21,29 @@ "clean:lib": "rm -rf lib", "start": "NODE_ENV=development webpack-dev-server -d --config webpack.config.dist.js", "test": "yarn lint && yarn test:unit && yarn test:visual && yarn test:functional", - "lint": "eslint ./src --ext .js --ignore-path .eslintignore", - "test:unit": "jest", - "test:unit:watch": "jest --watch", - "test:unit:coverage": "jest --coverage", - "test:visual": "yarn test:visual:install && yarn test:visual:chrome && yarn test:visual:firefox && yarn test:visual:mobile", + "lint": "yarn eslint ./src --ext .js --ignore-path .eslintignore", + "test:unit": "yarn jest", + "test:unit:watch": "yarn jest --watch", + "test:unit:coverage": "yarn jest --coverage", + "test:visual": "yarn run test:visual:install && yarn run test:visual:chrome && yarn run test:visual:firefox && yarn run test:visual:mobile", "test:visual:install": "yarn selenium-standalone install --silent", "test:visual:chrome": "yarn codeceptjs --config codecept-visual.conf.js run --steps --grep @desktop", "test:visual:firefox": "yarn codeceptjs --config codecept-visual.conf.js run --steps --profile firefox --grep @desktop", "test:visual:mobile": "yarn codeceptjs --config codecept-visual.conf.js run --steps --grep @mobile", "test:functional:debug": "yarn cypress open", - "test:functional": "yarn test:functional:chrome && yarn test:functional:firefox", + "test:functional": "yarn run test:functional:chrome && yarn run test:functional:firefox", "test:functional:firefox": "yarn cypress run --browser firefox", "test:functional:chrome": "yarn cypress run --browser chrome", "prepublish": "yarn run lib", - "semantic-release": "semantic-release --branch release", - "travis-deploy-once": "travis-deploy-once --pro", + "semantic-release": "yarn semantic-release --branch release", + "travis-deploy-once": "yarn travis-deploy-once --pro", "copy": "yarn run copy:assets && yarn run copy:helpcenter && yarn run copy:demo", - "copy:assets": "copyfiles -f assets/* dist", - "copy:helpcenter": "copyfiles -f helpcenter/* dist", - "copy:demo": "copyfiles -f demo/* dist", + "copy:assets": "yarn copyfiles -f assets/* dist", + "copy:helpcenter": "yarn copyfiles -f helpcenter/* dist", + "copy:demo": "yarn copyfiles -f demo/* dist", "build": "yarn run dist && yarn run lib && yarn run copy", - "dist": "yarn run clean:dist && webpack --config webpack.config.dist.js", - "lib": "yarn run clean:lib && webpack --config webpack.config.lib.js" + "dist": "yarn run clean:dist && yarn webpack --config webpack.config.dist.js", + "lib": "yarn run clean:lib && yarn webpack --config webpack.config.lib.js" }, "devDependencies": { "@applitools/eyes-webdriverio": "^5.7.2", @@ -72,6 +72,7 @@ "jsdom": "^11.6.2", "react": "16.12.0", "react-dom": "16.12.0", + "regenerator-runtime": "^0.13.5", "selenium-standalone": "^6.17.0", "semantic-release": "^12.2.5", "travis-deploy-once": "^4.3.3", diff --git a/src/core/make-popup.js b/src/core/make-popup.js index 7dd0506d..3e6a7f9c 100644 --- a/src/core/make-popup.js +++ b/src/core/make-popup.js @@ -18,6 +18,7 @@ import Popup, { DEFAULT_AUTOCLOSE_TIMEOUT } from './views/popup' import MobileModal from './views/mobile-modal' +import { getPostMessageHandler } from './utils/get-post-message-handler' const DEFAULT_DRAWER_WIDTH = 800 @@ -41,7 +42,8 @@ const queryStringKeys = { disableTracking: 'disable-tracking' } -const renderComponent = (url, domNode, options, onClose) => { +const renderComponent = (params, options) => { + const { url, domNode, close } = params const { autoClose, buttonText, @@ -59,7 +61,7 @@ const renderComponent = (url, domNode, options, onClose) => { render( , @@ -73,7 +75,7 @@ const renderComponent = (url, domNode, options, onClose) => { buttonText={buttonText} embedId={embedId} isAutoCloseEnabled={isAutoCloseEnabled} - onClose={onClose} + onClose={close} onSubmit={onSubmit} open url={urlWithQueryString} @@ -84,6 +86,9 @@ const renderComponent = (url, domNode, options, onClose) => { } export default function makePopup (url, options) { + window.addEventListener('message', getPostMessageHandler('form-ready', options.onReady)) + window.addEventListener('message', getPostMessageHandler('form-closed', options.onClose)) + const embedId = randomString() options = { @@ -109,10 +114,17 @@ export default function makePopup (url, options) { open (event) { const { currentTarget } = event || {} const currentUrl = currentTarget && currentTarget.href ? currentTarget.href : url - renderComponent(currentUrl, domNode, options, this.close) + const params = { + domNode, + url: currentUrl, + close: this.close + } + + renderComponent(params, options) }, close () { window.postMessage({ type: 'form-closed', embedId }, '*') + unmountComponentAtNode(domNode) } } diff --git a/src/core/make-popup.spec.js b/src/core/make-popup.spec.js index 9bb2337d..6aab0651 100644 --- a/src/core/make-popup.spec.js +++ b/src/core/make-popup.spec.js @@ -104,4 +104,27 @@ describe('makePopup', () => { expect(component.type.name).toEqual('Popup') expect(component.props.options).toEqual(expect.objectContaining(options)) }) + + it(`onReady is called during initialization`, async () => { + const options = { onReady: jest.fn() } + + makePopup(URL, options) + + window.postMessage({ type: 'form-ready' }, '*') + await new Promise((resolve) => setTimeout(resolve)) + expect(options.onReady).toHaveBeenCalledTimes(1) + expect(options.onReady).toHaveBeenCalledWith() + }) + + it(`onClose is called when form closes`, async () => { + const options = { onClose: jest.fn() } + + makePopup(URL, options) + + window.postMessage({ type: 'form-closed' }, '*') + await new Promise((resolve) => setTimeout(resolve)) + + expect(options.onClose).toHaveBeenCalledTimes(1) + expect(options.onClose).toHaveBeenCalledWith() + }) }) diff --git a/src/core/make-widget.js b/src/core/make-widget.js index e70477e6..c56689ed 100644 --- a/src/core/make-widget.js +++ b/src/core/make-widget.js @@ -11,6 +11,7 @@ import { isMobile } from './utils/mobile-detection' import Widget from './views/widget' +import { getPostMessageHandler } from './utils/get-post-message-handler' const defaultOptions = { mode: 'embed-widget', @@ -32,6 +33,8 @@ const queryStringKeys = { export default function makeWidget (element, url, options) { options = { ...defaultOptions, ...options } + window.addEventListener('message', getPostMessageHandler('form-ready', options.onReady)) + const enabledFullscreen = isMobile(navigator.userAgent) let queryStrings = replaceExistingKeys(options, queryStringKeys) diff --git a/src/core/make-widget.spec.js b/src/core/make-widget.spec.js index 92eec7a3..36c3ab2a 100644 --- a/src/core/make-widget.spec.js +++ b/src/core/make-widget.spec.js @@ -58,4 +58,16 @@ describe('makeWidget', () => { expect(component.type.name).toEqual('Widget') }) + + it(`onReady is called during initialization`, async () => { + const element = document.createElement('div') + const options = { onReady: jest.fn() } + + makeWidget(element, URL, options) + + window.postMessage({ type: 'form-ready' }, '*') + await new Promise((resolve) => setTimeout(resolve)) + expect(options.onReady).toHaveBeenCalledTimes(1) + expect(options.onReady).toHaveBeenCalledWith() + }) }) diff --git a/src/core/utils/get-post-message-handler.js b/src/core/utils/get-post-message-handler.js new file mode 100644 index 00000000..b68a0402 --- /dev/null +++ b/src/core/utils/get-post-message-handler.js @@ -0,0 +1,11 @@ +export const getPostMessageHandler = (type, handler, options = {}) => (event) => { + try { + if (event.data.type !== type) { return } + + if (options.includePayload) { + handler(event) + } else { + handler() + } + } catch (e) { } +} diff --git a/test/setup.js b/test/setup.js index 41f98f95..78faaf5a 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,3 +1,4 @@ +require('regenerator-runtime/runtime') const { JSDOM } = require('jsdom') // This setup creates a fake DOM with JSDOM and set globally @@ -8,5 +9,5 @@ const { window } = new JSDOM('