diff --git a/.gitignore b/.gitignore index c6889fcd6245..c038b66451ca 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,9 @@ packages/server/test/support/fixtures/server/libs /npm/react/bin/* /npm/react/cypress/videos +# from runner-ct +/packages/runner-ct/cypress/screenshots + # Building app binary scripts/support package-lock.json diff --git a/circle.yml b/circle.yml index a0fd75c71d6d..5ccc1a8a7724 100644 --- a/circle.yml +++ b/circle.yml @@ -198,6 +198,15 @@ commands: PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_ID \ PERCY_PARALLEL_TOTAL=-1 \ $cmd yarn workspace @packages/runner-ct run cypress:run --browser <> + - run: + command: | + if [[ <> == 'true' ]]; then + PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_ID \ + PERCY_PARALLEL_TOTAL=-1 \ + yarn percy upload packages/runner-ct/cypress/screenshots/screenshot.spec.tsx/percy + else + echo "skipping percy screenshots uploading" + fi - store_test_results: path: /tmp/cypress - store_artifacts: diff --git a/npm/webpack-dev-server/src/measureWebpackPerformance.ts b/npm/webpack-dev-server/src/measureWebpackPerformance.ts index 9331062cffed..a227c072ab57 100644 --- a/npm/webpack-dev-server/src/measureWebpackPerformance.ts +++ b/npm/webpack-dev-server/src/measureWebpackPerformance.ts @@ -13,7 +13,7 @@ export function measureWebpackPerformance (webpackConfig: webpack.Configuration) const compareWithPrevious = process.env.WEBPACK_PERF_MEASURE_COMPARE function percentageDiff (a: number, b: number) { - return 100 * (a - b) / ((a + b) / 2) + return ((a - b) / a) * 100 } const compareOutput = (output: string) => { @@ -31,7 +31,10 @@ export function measureWebpackPerformance (webpackConfig: webpack.Configuration) const delimiter = new Array(process.stdout.columns).fill('═').join('') console.log(delimiter) - console.log(`${chalk.bold('WEBPACK_PERF_MEASURE:')} ${result}`) + console.log(`${chalk.bold('WEBPACK_PERF_MEASURE')}`) + console.log(`Before: ${chalk.bold(oldStats.misc.compileTime / 1000)}s`) + console.log(`After: ${chalk.bold(newStats.misc.compileTime / 1000)}s`) + console.log(result) console.log(delimiter) } diff --git a/package.json b/package.json index 3018493ac8b0..8f43a4a581ec 100644 --- a/package.json +++ b/package.json @@ -163,6 +163,7 @@ "mocha-junit-reporter": "2.0.0", "mocha-multi-reporters": "1.1.7", "mock-fs": "4.9.0", + "odiff-bin": "2.1.0", "parse-github-repo-url": "1.4.1", "patch-package": "6.2.2", "percy": "0.26.9", diff --git a/packages/reporter/src/main.tsx b/packages/reporter/src/main.tsx index 61e17a337760..1d6c900294bc 100644 --- a/packages/reporter/src/main.tsx +++ b/packages/reporter/src/main.tsx @@ -22,6 +22,7 @@ import Runnables from './runnables/runnables' interface BaseReporterProps { appState: AppState + className?: string runnablesStore: RunnablesStore runner: Runner scroller: Scroller @@ -78,6 +79,7 @@ class Reporter extends Component { render () { const { appState, + className, runMode, runnablesStore, scroller, @@ -89,7 +91,7 @@ class Reporter extends Component { } = this.props return ( -
{ + return ( +
+ +
Body
+ +
+ ) +} + +describe('screenshot', () => { + it('takes a standard screenshot', () => { + cy.viewport(500, 500) + mount(, { + styles, + }) + + cy.screenshot('percy/component_testing_takes_a_screenshot') + }) + + it('takes a screenshot with a custom viewport', () => { + cy.viewport(750, 750) + mount(, { + styles, + }) + + cy.screenshot('percy/component_testing_screenshot_custom_viewport_screenshot') + }) + + // TODO: This will technically pass, but the screenshot is not correct. + // AUT transform appears to be buggy for extreme viewports. + xit('screenshot with a really long viewport', () => { + cy.viewport(200, 2000) + mount(, { + styles, + }) + + cy.screenshot('percy/component_testing_screenshot_long_viewport') + }) +}) diff --git a/packages/runner-ct/cypress/plugins/index.js b/packages/runner-ct/cypress/plugins/index.js index 615067509d11..aba246603b7a 100644 --- a/packages/runner-ct/cypress/plugins/index.js +++ b/packages/runner-ct/cypress/plugins/index.js @@ -22,6 +22,7 @@ function injectStylesInlineForPercyInPlace (webpackConfig) { */ module.exports = (on, config) => { on('task', percyHealthCheck) + on('dev-server:start', (options) => { /** @type {import('webpack').Configuration} */ const { default: webpackConfig } = require(path.resolve(__dirname, '..', '..', 'webpack.config.ts')) diff --git a/packages/runner-ct/src/app/RunnerCt.scss b/packages/runner-ct/src/app/RunnerCt.scss index 0a997b285e0d..c74e5aa73dd9 100644 --- a/packages/runner-ct/src/app/RunnerCt.scss +++ b/packages/runner-ct/src/app/RunnerCt.scss @@ -12,6 +12,10 @@ main.app-ct { font-size: $font-size; } +.display-none { + display: none !important; +} + .menu-toggle { z-index: 2; :hover { @@ -70,6 +74,14 @@ main.app-ct { margin-inline: $specs-list-offset; } +.app-wrapper-screenshotting { + margin-inline: 0; +} + +.screenshotting { + box-shadow: none; +} + .runner-ct { left: 0; diff --git a/packages/runner-ct/src/app/RunnerCt.tsx b/packages/runner-ct/src/app/RunnerCt.tsx index 7e904e2507c8..b89b35e67e9d 100644 --- a/packages/runner-ct/src/app/RunnerCt.tsx +++ b/packages/runner-ct/src/app/RunnerCt.tsx @@ -1,6 +1,7 @@ import cs from 'classnames' import { observer } from 'mobx-react' import * as React from 'react' + import { Reporter } from '@packages/reporter/src/main' import errorMessages from '../errors/error-messages' @@ -21,6 +22,7 @@ import { useGlobalHotKey } from '../lib/useHotKey' import './RunnerCt.scss' import { KeyboardHelper, NoSpecSelected } from './NoSpecSelected' +import { useScreenshotHandler } from './useScreenshotHandler' // Cypress.ConfigOptions only appears to have internal options. // TODO: figure out where the "source of truth" should be for @@ -42,6 +44,7 @@ const VIEWPORT_SIDE_MARGIN = 40 + 17 const App: React.FC = observer( function App (props: AppProps) { const searchRef = React.useRef(null) + const splitPaneRef = React.useRef<{ splitPane: HTMLDivElement }>(null) const pluginRootContainer = React.useRef(null) const { state, eventManager, config } = props @@ -86,6 +89,12 @@ const App: React.FC = observer( monitorWindowResize() }, []) + useScreenshotHandler({ + state, + eventManager, + splitPaneRef, + }) + function focusSpecsList () { setIsSpecsListOpen(true) @@ -116,7 +125,12 @@ const App: React.FC = observer( <>
= observer( />
-
+
setIsResizing(true)} onDragFinished={() => setIsResizing(false)} onChange={onSplitPaneChange} @@ -171,10 +186,10 @@ const App: React.FC = observer( = observer( : state.isAnyPluginToShow ? 30 : 0 } > -
+
{!state.spec ? ( diff --git a/packages/runner-ct/src/app/useScreenshotHandler.tsx b/packages/runner-ct/src/app/useScreenshotHandler.tsx new file mode 100644 index 000000000000..7c98be719b7b --- /dev/null +++ b/packages/runner-ct/src/app/useScreenshotHandler.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { runInAction } from 'mobx' +import EventManager from '../lib/event-manager' +import State from '../lib/state' + +/** +* SplitPane hierarchy looks like this: +* ```jsx +*
+*
..
+* ... +*
+*``` +* we need to set these to display: none during cy.screenshot. +*/ +export function useScreenshotHandler ({ eventManager, state, splitPaneRef } : { + eventManager: typeof EventManager, state: State, splitPaneRef: React.MutableRefObject<{ splitPane: HTMLDivElement }> +}) { + const showPane = () => { + if (!splitPaneRef.current) { + return + } + + splitPaneRef.current.splitPane.firstElementChild.classList.remove('d-none') + splitPaneRef.current.splitPane.querySelector('[role="presentation"]').classList.remove('d-none') + } + + const hidePane = () => { + if (!splitPaneRef.current) { + return + } + + splitPaneRef.current.splitPane.firstElementChild.classList.add('d-none') + splitPaneRef.current.splitPane.querySelector('[role="presentation"]').classList.add('d-none') + } + + React.useEffect(() => { + eventManager.on('before:screenshot', (config) => { + runInAction(() => { + state.setScreenshotting(true) + hidePane() + }) + }) + + const revertFromScreenshotting = () => { + runInAction(() => { + state.setScreenshotting(false) + showPane() + }) + } + + eventManager.on('after:screenshot', (config) => { + revertFromScreenshotting() + }) + + eventManager.on('run:start', () => { + revertFromScreenshotting() + }) + }, []) +} diff --git a/packages/runner-ct/src/header/header.jsx b/packages/runner-ct/src/header/header.tsx similarity index 91% rename from packages/runner-ct/src/header/header.jsx rename to packages/runner-ct/src/header/header.tsx index ae9b9e71b182..00217633213c 100644 --- a/packages/runner-ct/src/header/header.jsx +++ b/packages/runner-ct/src/header/header.tsx @@ -4,17 +4,23 @@ import { action, observable } from 'mobx' import { observer } from 'mobx-react' import React, { Component } from 'react' import Tooltip from '@cypress/react-tooltip' -import { $ } from '@packages/driver' +import State from '../lib/state' import { configFileFormatted } from '../lib/config-file-formatted' import SelectorPlayground from '../selector-playground/selector-playground' import selectorPlaygroundModel from '../selector-playground/selector-playground-model' +import { ExtendedConfigOptions } from '../app/RunnerCt' + +interface HeaderProps { + state: State + config: ExtendedConfigOptions +} @observer -export default class Header extends Component { +export default class Header extends Component { headerRef = React.createRef() - @observable showingViewportMenu = false; + @observable showingViewportMenu = false render () { const { state, config } = this.props @@ -24,6 +30,7 @@ export default class Header extends Component { ref={this.headerRef} className={cs({ 'showing-selector-playground': selectorPlaygroundModel.isOpen, + 'display-none': state.screenshotting, })} >
diff --git a/packages/runner-ct/src/iframe/iframes.jsx b/packages/runner-ct/src/iframe/iframes.jsx index 30544c70f4c6..3a1d8c62903e 100644 --- a/packages/runner-ct/src/iframe/iframes.jsx +++ b/packages/runner-ct/src/iframe/iframes.jsx @@ -22,10 +22,13 @@ export default class Iframes extends Component { containerRef = null render () { - const { height, width, scriptError, scale } = this.props.state + const { height, width, scriptError, scale, screenshotting } = this.props.state return ( -
+
this.containerRef = container} className='size-container' diff --git a/packages/runner-ct/src/iframe/iframes.scss b/packages/runner-ct/src/iframe/iframes.scss index 711c29cb0cc3..d52f9a2939ec 100644 --- a/packages/runner-ct/src/iframe/iframes.scss +++ b/packages/runner-ct/src/iframe/iframes.scss @@ -2,6 +2,11 @@ margin: 8px; } +.iframes-ct-container-screenshotting { + margin: 0; +} + + .size-container { width: 100%; overflow: auto; diff --git a/packages/runner-ct/src/lib/state.ts b/packages/runner-ct/src/lib/state.ts index 88ec62fc1d50..f85316b34ebb 100644 --- a/packages/runner-ct/src/lib/state.ts +++ b/packages/runner-ct/src/lib/state.ts @@ -73,6 +73,8 @@ export default class State { @observable width = _defaults.width @observable height = _defaults.height + @observable screenshotting = false + // if null, the default CSS handles it // if non-null, the user has set it by resizing @observable reporterWidth = _defaults.reporterWidth @@ -151,6 +153,10 @@ export default class State { } } + @action setScreenshotting (screenshotting: boolean) { + this.screenshotting = screenshotting + } + @action setIsLoading (isLoading) { this.isLoading = isLoading } diff --git a/packages/server-ct/package.json b/packages/server-ct/package.json index 3792a6017361..c71ab0a33e6c 100644 --- a/packages/server-ct/package.json +++ b/packages/server-ct/package.json @@ -12,13 +12,14 @@ "test-unit": "mocha -r @packages/ts/register test/**/*.spec.ts --config ./test/.mocharc.js --exit" }, "dependencies": { - "bluebird": "^3.7.2", - "chokidar": "^3.4.3", - "debug": "^4.2.0", - "express": "^4.17.1", - "http-proxy": "^1.18.1", - "lodash": "^4.17.20", - "send": "^0.17.1" + "bluebird": "3.5.3", + "chalk": "2.4.2", + "chokidar": "3.2.2", + "debug": "4.3.2", + "express": "4.17.1", + "http-proxy": "1.18.1", + "lodash": "4.17.20", + "send": "0.17.1" }, "devDependencies": { "chai": "^4.2.0", diff --git a/packages/server/__snapshots__/cypress_spec.js b/packages/server/__snapshots__/cypress_spec.js index f25827837ee3..fc962c31457e 100644 --- a/packages/server/__snapshots__/cypress_spec.js +++ b/packages/server/__snapshots__/cypress_spec.js @@ -68,6 +68,7 @@ The ciBuildId is automatically detected if you are running Cypress in any of the - codeshipBasic - codeshipPro - concourse +- codeFresh - drone - githubActions - gitlab @@ -104,6 +105,7 @@ The ciBuildId is automatically detected if you are running Cypress in any of the - codeshipBasic - codeshipPro - concourse +- codeFresh - drone - githubActions - gitlab @@ -141,6 +143,7 @@ The ciBuildId is automatically detected if you are running Cypress in any of the - codeshipBasic - codeshipPro - concourse +- codeFresh - drone - githubActions - gitlab diff --git a/packages/server/lib/util/ci_provider.js b/packages/server/lib/util/ci_provider.js index 275b34cc5099..7c57b713244a 100644 --- a/packages/server/lib/util/ci_provider.js +++ b/packages/server/lib/util/ci_provider.js @@ -96,6 +96,7 @@ const CI_PROVIDERS = { 'codeshipBasic': isCodeshipBasic, 'codeshipPro': isCodeshipPro, 'concourse': isConcourse, + codeFresh: 'CF_BUILD_ID', 'drone': 'DRONE', githubActions: 'GITHUB_ACTIONS', 'gitlab': isGitlab, @@ -218,6 +219,20 @@ const _providerCiParams = () => { 'BUILD_TEAM_NAME', 'ATC_EXTERNAL_URL', ]), + // https://codefresh.io/docs/docs/codefresh-yaml/variables/ + codeFresh: extract([ + 'CF_BUILD_ID', + 'CF_BUILD_URL', + 'CF_CURRENT_ATTEMPT', + 'CF_STEP_NAME', + 'CF_PIPELINE_NAME', + 'CF_PIPELINE_TRIGGER_ID', + // variables added for pull requests + 'CF_PULL_REQUEST_ID', + 'CF_PULL_REQUEST_IS_FORK', + 'CF_PULL_REQUEST_NUMBER', + 'CF_PULL_REQUEST_TARGET', + ]), drone: extract([ 'DRONE_JOB_NUMBER', 'DRONE_BUILD_LINK', @@ -465,6 +480,12 @@ const _providerCommitParams = () => { // remoteOrigin: ??? // defaultBranch: ??? }, + codeFresh: { + sha: env.CF_REVISION, + branch: env.CF_BRANCH, + message: env.CF_COMMIT_MESSAGE, + authorName: env.CF_COMMIT_AUTHOR, + }, drone: { sha: env.DRONE_COMMIT_SHA, branch: env.DRONE_COMMIT_BRANCH, diff --git a/packages/server/test/unit/ci_provider_spec.js b/packages/server/test/unit/ci_provider_spec.js index 343b86a09d93..c350739ca12a 100644 --- a/packages/server/test/unit/ci_provider_spec.js +++ b/packages/server/test/unit/ci_provider_spec.js @@ -458,6 +458,52 @@ describe('lib/util/ci_provider', () => { return expectsCommitParams(null) }) + it('codeFresh', () => { + resetEnv = mockedEnv({ + // build information + 'CF_BUILD_ID': 'cfBuildId', + 'CF_BUILD_URL': 'cfBuildUrl', + 'CF_CURRENT_ATTEMPT': 'cfCurrentAttempt', + 'CF_STEP_NAME': 'cfStepName', + 'CF_PIPELINE_NAME': 'cfPipelineName', + 'CF_PIPELINE_TRIGGER_ID': 'cfPipelineTriggerId', + + // variables added for pull requests + 'CF_PULL_REQUEST_ID': 'cfPullRequestId', + 'CF_PULL_REQUEST_IS_FORK': 'cfPullRequestIsFork', + 'CF_PULL_REQUEST_NUMBER': 'cfPullRequestNumber', + 'CF_PULL_REQUEST_TARGET': 'cfPullRequestTarget', + + // git information + CF_REVISION: 'cfRevision', + CF_BRANCH: 'cfBranch', + CF_COMMIT_MESSAGE: 'cfCommitMessage', + CF_COMMIT_AUTHOR: 'cfCommitAuthor', + }, { clear: true }) + + expectsName('codeFresh') + expectsCiParams({ + cfBuildId: 'cfBuildId', + cfBuildUrl: 'cfBuildUrl', + cfCurrentAttempt: 'cfCurrentAttempt', + cfStepName: 'cfStepName', + cfPipelineName: 'cfPipelineName', + cfPipelineTriggerId: 'cfPipelineTriggerId', + // pull request variables + cfPullRequestId: 'cfPullRequestId', + cfPullRequestIsFork: 'cfPullRequestIsFork', + cfPullRequestNumber: 'cfPullRequestNumber', + cfPullRequestTarget: 'cfPullRequestTarget', + }) + + expectsCommitParams({ + sha: 'cfRevision', + branch: 'cfBranch', + message: 'cfCommitMessage', + authorName: 'cfCommitAuthor', + }) + }) + it('drone', () => { resetEnv = mockedEnv({ DRONE: 'true', diff --git a/yarn.lock b/yarn.lock index ddf9a9ff7924..d5c1cb20cb5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10765,7 +10765,7 @@ chokidar@3.4.3: optionalDependencies: fsevents "~2.1.2" -"chokidar@>=2.0.0 <4.0.0", chokidar@^3.3.0, chokidar@^3.4.1, chokidar@^3.4.3: +"chokidar@>=2.0.0 <4.0.0", chokidar@^3.3.0, chokidar@^3.4.1: version "3.5.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.0.tgz#458a4816a415e9d3b3caa4faec2b96a6935a9e65" integrity sha512-JgQM9JS92ZbFR4P90EvmzNpSGhpPBGBSj10PILeDyYFwp4h2/D9OM03wsJ4zW1fEp4ka2DGrnUeD7FuvQ2aZ2Q== @@ -12861,7 +12861,7 @@ de-indent@^1.0.2: resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= -debug@*, debug@4, debug@4.3.1, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.0, debug@~4.3.1: +debug@*, debug@4, debug@4.3.1, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.0, debug@~4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== @@ -18532,7 +18532,7 @@ http-proxy-middleware@^0.19.1: lodash "^4.17.11" micromatch "^3.1.10" -http-proxy@^1.17.0, http-proxy@^1.18.1: +http-proxy@1.18.1, http-proxy@^1.17.0, http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== @@ -22292,16 +22292,16 @@ lodash@4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== +lodash@4.17.20, "lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.2, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.2: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + lodash@4.17.4: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" integrity sha1-eCA6TRwyiuHYbcpkYONptX9AVa4= -"lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.2, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.2: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - log-symbols@2.2.0, log-symbols@^2.1.0, log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" @@ -25078,6 +25078,11 @@ octokit-pagination-methods@^1.1.0: resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== +odiff-bin@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/odiff-bin/-/odiff-bin-2.1.0.tgz#6ef727b44a1843d9215408b99774c05567176776" + integrity sha512-jN74kFP216ltssc72ig/48NtFO81AOvO5pJevDykVCA4hmyhzf5M/wyNFt0RxJZT8MWi2g/I4svp6VDjFhtuCQ== + omggif@^1.0.10, omggif@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19" @@ -30067,7 +30072,7 @@ semver@~5.0.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" integrity sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no= -send@0.17.1, send@^0.17.1: +send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==