<commit_before> /**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
'use strict';

const AsyncTaskGroup = require('../lib/AsyncTaskGroup');
const MapWithDefaults = require('../lib/MapWithDefaults');
const debug = require('debug')('ReactNativePackager:DependencyGraph');
const util = require('util');
const path = require('../fastpath');
const realPath = require('path');
const isAbsolutePath = require('absolute-path');
const getAssetDataFromName = require('../lib/getAssetDataFromName');

const emptyModule = require.resolve('./assets/empty-module.js');

class ResolutionRequest {
  constructor({
    platform,
    platforms,
    preferNativePlatform,
    entryPath,
    hasteMap,
    deprecatedAssetMap,
    helpers,
    moduleCache,
    fastfs,
    shouldThrowOnUnresolvedErrors,
    extraNodeModules,
  }) {
    this._platform = platform;
    this._platforms = platforms;
    this._preferNativePlatform = preferNativePlatform;
    this._entryPath = entryPath;
    this._hasteMap = hasteMap;
    this._deprecatedAssetMap = deprecatedAssetMap;
    this._helpers = helpers;
    this._moduleCache = moduleCache;
    this._fastfs = fastfs;
    this._shouldThrowOnUnresolvedErrors = shouldThrowOnUnresolvedErrors;
    this._extraNodeModules = extraNodeModules;
    this._resetResolutionCache();
  }

  _tryResolve(action, secondaryAction) {
    return action().catch((error) => {
      if (error.type !== 'UnableToResolveError') {
        throw error;
      }
      return secondaryAction();
    });
  }

  resolveDependency(fromModule, toModuleName) {
    const resHash = resolutionHash(fromModule.path, toModuleName);

    if (this._immediateResolutionCache[resHash]) {
      return Promise.resolve(this._immediateResolutionCache[resHash]);
    }

    const asset_DEPRECATED = this._deprecatedAssetMap.resolve(
      fromModule,
      toModuleName
    );
    if (asset_DEPRECATED) {
      return Promise.resolve(asset_DEPRECATED);
    }

    const cacheResult = (result) => {
      this._immediateResolutionCache[resHash] = result;
      return result;
    };

    const forgive = (error) => {
      if (
        error.type !== 'UnableToResolveError' ||
        this._shouldThrowOnUnresolvedErrors(this._entryPath, this._platform)
      ) {
        throw error;
      }

      debug(
        'Unable to resolve module %s from %s',
        toModuleName,
        fromModule.path
      );
      return null;
    };

    if (!this._helpers.isNodeModulesDir(fromModule.path)
        && !(isRelativeImport(toModuleName) || isAbsolutePath(toModuleName))) {
      return this._tryResolve(
        () => this._resolveHasteDependency(fromModule, toModuleName),
        () => this._resolveNodeDependency(fromModule, toModuleName)
      ).then(
        cacheResult,
        forgive,
      );
    }

    return this._resolveNodeDependency(fromModule, toModuleName)
      .then(
        cacheResult,
        forgive,
      );
  }

  getOrderedDependencies({
    response,
    mocksPattern,
    transformOptions,
    onProgress,
    recursive = true,
  }) {
    return this._getAllMocks(mocksPattern).then(allMocks => {
      const entry = this._moduleCache.getModule(this._entryPath);
      const mocks = Object.create(null);

      response.pushDependency(entry);
      let totalModules = 1;
      let finishedModules = 0;

      const resolveDependencies = module =>
        module.getDependencies(transformOptions)
          .then(dependencyNames =>
            Promise.all(
              dependencyNames.map(name => this.resolveDependency(module, name))
            ).then(dependencies => [dependencyNames, dependencies])
          );

      const addMockDependencies = !allMocks
        ? (module, result) => result
        : (module, [dependencyNames, dependencies]) => {
          const list = [module.getName()];
          const pkg = module.getPackage();
          if (pkg) {
            list.push(pkg.getName());
          }
          return Promise.all(list).then(names => {
            names.forEach(name => {
              if (allMocks[name] && !mocks[name]) {
                const mockModule = this._moduleCache.getModule(allMocks[name]);
                dependencyNames.push(name);
                dependencies.push(mockModule);
                mocks[name] = allMocks[name];
              }
            });
            return [dependencyNames, dependencies];
          });
        };

      const collectedDependencies = new MapWithDefaults(module => collect(module));
      const crawlDependencies = (mod, [depNames, dependencies]) => {
        const filteredPairs = [];

        dependencies.forEach((modDep, i) => {
          const name = depNames[i];
          if (modDep == null) {
            // It is possible to require mocks that don't have a real
            // module backing them. If a dependency cannot be found but there
            // exists a mock with the desired ID, resolve it and add it as
            // a dependency.
            if (allMocks && allMocks[name] && !mocks[name]) {
              const mockModule = this._moduleCache.getModule(allMocks[name]);
              mocks[name] = allMocks[name];
              return filteredPairs.push([name, mockModule]);
            }

            debug(
              'WARNING: Cannot find required module `%s` from module `%s`',
              name,
              mod.path
            );
            return false;
          }
          return filteredPairs.push([name, modDep]);
        });

        response.setResolvedDependencyPairs(mod, filteredPairs);

        const dependencyModules = filteredPairs.map(([, m]) => m);
        const newDependencies =
          dependencyModules.filter(m => !collectedDependencies.has(m));

        if (onProgress) {
          finishedModules += 1;
          totalModules += newDependencies.length;
          onProgress(finishedModules, totalModules);
        }

        if (recursive) {
          // doesn't block the return of this function invocation, but defers
          // the resulution of collectionsInProgress.done.then(...)
          dependencyModules
            .forEach(dependency => collectedDependencies.get(dependency));
        }
        return dependencyModules;
      };

      const collectionsInProgress = new AsyncTaskGroup();
      function collect(module) {
        collectionsInProgress.start(module);
        const result = resolveDependencies(module)
          .then(result => addMockDependencies(module, result))
          .then(result => crawlDependencies(module, result));
        const end = () => collectionsInProgress.end(module);
        result.then(end, end);
        return result;
      }

      return Promise.all([
        // kicks off recursive dependency discovery, but doesn't block until it's done
        collectedDependencies.get(entry),

        // resolves when there are no more modules resolving dependencies
        collectionsInProgress.done,
      ]).then(([rootDependencies]) => {
        return Promise.all(
          Array.from(collectedDependencies, resolveKeyWithPromise)
        ).then(moduleToDependenciesPairs =>
          [rootDependencies, new MapWithDefaults(() => [], moduleToDependenciesPairs)]
        );
      }).then(([rootDependencies, moduleDependencies]) => {
        // serialize dependencies, and make sure that every single one is only
        // included once
        const seen = new Set([entry]);
        function traverse(dependencies) {
          dependencies.forEach(dependency => {
            if (seen.has(dependency)) { return; }

            seen.add(dependency);
            response.pushDependency(dependency);
            traverse(moduleDependencies.get(dependency));
          });
        }

        traverse(rootDependencies);
        response.setMocks(mocks);
      });
    });
  }

  _getAllMocks(pattern) {
    // Take all mocks in all the roots into account. This is necessary
    // because currently mocks are global: any module can be mocked by
    // any mock in the system.
    let mocks = null;
    if (pattern) {
      mocks = Object.create(null);
      this._fastfs.matchFilesByPattern(pattern).forEach(file =>
        mocks[path.basename(file, path.extname(file))] = file
      );
    }
    return Promise.resolve(mocks);
  }

  _resolveHasteDependency(fromModule, toModuleName) {
    toModuleName = normalizePath(toModuleName);

    let p = fromModule.getPackage();
    if (p) {
      p = p.redirectRequire(toModuleName);
    } else {
      p = Promise.resolve(toModuleName);
    }

    return p.then((realModuleName) => {
      let dep = this._hasteMap.getModule(realModuleName, this._platform);
      if (dep && dep.type === 'Module') {
        return dep;
      }

      let packageName = realModuleName;
      while (packageName && packageName !== '.') {
        dep = this._hasteMap.getModule(packageName, this._platform);
        if (dep && dep.type === 'Package') {
          break;
        }
        packageName = path.dirname(packageName);
      }

      if (dep && dep.type === 'Package') {
        const potentialModulePath = path.join(
          dep.root,
          path.relative(packageName, realModuleName)
        );
        return this._tryResolve(
          () => this._loadAsFile(
            potentialModulePath,
            fromModule,
            toModuleName,
          ),
          () => this._loadAsDir(potentialModulePath, fromModule, toModuleName),
        );
      }

      throw new UnableToResolveError(
        fromModule,
        toModuleName,
        'Unable to resolve dependency',
      );
    });
  }

  _redirectRequire(fromModule, modulePath) {
    return Promise.resolve(fromModule.getPackage()).then(p => {
      if (p) {
        return p.redirectRequire(modulePath);
      }
      return modulePath;
    });
  }

  _resolveFileOrDir(fromModule, toModuleName) {
    const potentialModulePath = isAbsolutePath(toModuleName) ?
        toModuleName :
        path.join(path.dirname(fromModule.path), toModuleName);

    return this._redirectRequire(fromModule, potentialModulePath).then(
      realModuleName => {
        if (realModuleName === false) {
          return this._loadAsFile(emptyModule, fromModule, toModuleName);
        }

        return this._tryResolve(
          () => this._loadAsFile(realModuleName, fromModule, toModuleName),
          () => this._loadAsDir(realModuleName, fromModule, toModuleName)
        );
      }
    );
  }

  _resolveNodeDependency(fromModule, toModuleName) {
    if (isRelativeImport(toModuleName) || isAbsolutePath(toModuleName)) {
      return this._resolveFileOrDir(fromModule, toModuleName);
    } else {
      return this._redirectRequire(fromModule, toModuleName).then(
        realModuleName => {
          // exclude
          if (realModuleName === false) {
            return this._loadAsFile(emptyModule, fromModule, toModuleName);
          }

          if (isRelativeImport(realModuleName) || isAbsolutePath(realModuleName)) {
            // derive absolute path /.../node_modules/fromModuleDir/realModuleName
            const fromModuleParentIdx = fromModule.path.lastIndexOf('node_modules/') + 13;
            const fromModuleDir = fromModule.path.slice(0, fromModule.path.indexOf('/', fromModuleParentIdx));
            const absPath = path.join(fromModuleDir, realModuleName);
            return this._resolveFileOrDir(fromModule, absPath);
          }

          const searchQueue = [];
          for (let currDir = path.dirname(fromModule.path);
               currDir !== realPath.parse(fromModule.path).root;
               currDir = path.dirname(currDir)) {
            let searchPath = path.join(currDir, 'node_modules');
            if (this._fastfs.dirExists(searchPath)) {
              searchQueue.push(
                path.join(searchPath, realModuleName)
              );
            }
          }

          if (this._extraNodeModules) {
            const bits = toModuleName.split('/');
            const packageName = bits[0];
            if (this._extraNodeModules[packageName]) {
              bits[0] = this._extraNodeModules[packageName];
              searchQueue.push(path.join.apply(path, bits));
            }
          }

          let p = Promise.reject(new UnableToResolveError(
            fromModule,
            toModuleName,
            'Node module not found',
          ));
          searchQueue.forEach(potentialModulePath => {
            p = this._tryResolve(
              () => this._tryResolve(
                () => p,
                () => this._loadAsFile(potentialModulePath, fromModule, toModuleName),
              ),
              () => this._loadAsDir(potentialModulePath, fromModule, toModuleName)
            );
          });

          return p;
        });
    }
  }

  _loadAsFile(potentialModulePath, fromModule, toModule) {
    return Promise.resolve().then(() => {
      if (this._helpers.isAssetFile(potentialModulePath)) {
        const dirname = path.dirname(potentialModulePath);
        if (!this._fastfs.dirExists(dirname)) {
          throw new UnableToResolveError(
            fromModule,
            toModule,
            `Directory ${dirname} doesn't exist`,
          );
        }

        const {name, type} = getAssetDataFromName(potentialModulePath, this._platforms);

        let pattern = '^' + name + '(@[\\d\\.]+x)?';
        if (this._platform != null) {
          pattern += '(\\.' + this._platform + ')?';
        }
        pattern += '\\.' + type;

        // We arbitrarly grab the first one, because scale selection
        // will happen somewhere
        const [assetFile] = this._fastfs.matches(
          dirname,
          new RegExp(pattern)
        );

        if (assetFile) {
          return this._moduleCache.getAssetModule(assetFile);
        }
      }

      let file;
      if (this._fastfs.fileExists(potentialModulePath)) {
        file = potentialModulePath;
      } else if (this._platform != null &&
                 this._fastfs.fileExists(potentialModulePath + '.' + this._platform + '.js')) {
        file = potentialModulePath + '.' + this._platform + '.js';
      } else if (this._preferNativePlatform &&
                 this._fastfs.fileExists(potentialModulePath + '.native.js')) {
        file = potentialModulePath + '.native.js';
      } else if (this._fastfs.fileExists(potentialModulePath + '.js')) {
        file = potentialModulePath + '.js';
      } else if (this._fastfs.fileExists(potentialModulePath + '.json')) {
        file = potentialModulePath + '.json';
      } else {
        throw new UnableToResolveError(
          fromModule,
          toModule,
          `File ${potentialModulePath} doesnt exist`,
        );
      }

      return this._moduleCache.getModule(file);
    });
  }

  _loadAsDir(potentialDirPath, fromModule, toModule) {
    return Promise.resolve().then(() => {
      if (!this._fastfs.dirExists(potentialDirPath)) {
        throw new UnableToResolveError(
          fromModule,
          toModule,
`Unable to find this module in its module map or any of the node_modules directories under ${potentialDirPath} and its parent directories

This might be related to https://github.com/facebook/react-native/issues/4968
To resolve try the following:
  1. Clear watchman watches: \`watchman watch-del-all\`.
  2. Delete the \`node_modules\` folder: \`rm -rf node_modules && npm install\`.
  3. Reset packager cache: \`rm -fr $TMPDIR/react-*\` or \`npm start -- --reset-cache\`.`,
        );
      }

      const packageJsonPath = path.join(potentialDirPath, 'package.json');
      if (this._fastfs.fileExists(packageJsonPath)) {
        return this._moduleCache.getPackage(packageJsonPath)
          .getMain().then(
            (main) => this._tryResolve(
              () => this._loadAsFile(main, fromModule, toModule),
              () => this._loadAsDir(main, fromModule, toModule)
            )
          );
      }

      return this._loadAsFile(
        path.join(potentialDirPath, 'index'),
        fromModule,
        toModule,
      );
    });
  }

  _resetResolutionCache() {
    this._immediateResolutionCache = Object.create(null);
  }

}

function resolutionHash(modulePath, depName) {
  return `${path.resolve(modulePath)}:${depName}`;
}

function UnableToResolveError(fromModule, toModule, message) {
  Error.call(this);
  Error.captureStackTrace(this, this.constructor);
  this.message = util.format(
    'Unable to resolve module %s from %s: %s',
    toModule,
    fromModule.path,
    message,
  );
  this.type = this.name = 'UnableToResolveError';
}

util.inherits(UnableToResolveError, Error);

function normalizePath(modulePath) {
  if (path.sep === '/') {
    modulePath = path.normalize(modulePath);
  } else if (path.posix) {
    modulePath = path.posix.normalize(modulePath);
  }

  return modulePath.replace(/\/$/, '');
}

function resolveKeyWithPromise([key, promise]) {
  return promise.then(value => [key, value]);
}

function isRelativeImport(path) {
  return /^[.][.]?[/]/.test(path);
}

module.exports = ResolutionRequest;
<commit_msg>Fix node_modules resolution error messages<commit_after> /**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
'use strict';

const AsyncTaskGroup = require('../lib/AsyncTaskGroup');
const MapWithDefaults = require('../lib/MapWithDefaults');
const debug = require('debug')('ReactNativePackager:DependencyGraph');
const util = require('util');
const path = require('../fastpath');
const realPath = require('path');
const isAbsolutePath = require('absolute-path');
const getAssetDataFromName = require('../lib/getAssetDataFromName');

const emptyModule = require.resolve('./assets/empty-module.js');

class ResolutionRequest {
  constructor({
    platform,
    platforms,
    preferNativePlatform,
    entryPath,
    hasteMap,
    deprecatedAssetMap,
    helpers,
    moduleCache,
    fastfs,
    shouldThrowOnUnresolvedErrors,
    extraNodeModules,
  }) {
    this._platform = platform;
    this._platforms = platforms;
    this._preferNativePlatform = preferNativePlatform;
    this._entryPath = entryPath;
    this._hasteMap = hasteMap;
    this._deprecatedAssetMap = deprecatedAssetMap;
    this._helpers = helpers;
    this._moduleCache = moduleCache;
    this._fastfs = fastfs;
    this._shouldThrowOnUnresolvedErrors = shouldThrowOnUnresolvedErrors;
    this._extraNodeModules = extraNodeModules;
    this._resetResolutionCache();
  }

  _tryResolve(action, secondaryAction) {
    return action().catch((error) => {
      if (error.type !== 'UnableToResolveError') {
        throw error;
      }
      return secondaryAction();
    });
  }

  resolveDependency(fromModule, toModuleName) {
    const resHash = resolutionHash(fromModule.path, toModuleName);

    if (this._immediateResolutionCache[resHash]) {
      return Promise.resolve(this._immediateResolutionCache[resHash]);
    }

    const asset_DEPRECATED = this._deprecatedAssetMap.resolve(
      fromModule,
      toModuleName
    );
    if (asset_DEPRECATED) {
      return Promise.resolve(asset_DEPRECATED);
    }

    const cacheResult = (result) => {
      this._immediateResolutionCache[resHash] = result;
      return result;
    };

    const forgive = (error) => {
      if (
        error.type !== 'UnableToResolveError' ||
        this._shouldThrowOnUnresolvedErrors(this._entryPath, this._platform)
      ) {
        throw error;
      }

      debug(
        'Unable to resolve module %s from %s',
        toModuleName,
        fromModule.path
      );
      return null;
    };

    if (!this._helpers.isNodeModulesDir(fromModule.path)
        && !(isRelativeImport(toModuleName) || isAbsolutePath(toModuleName))) {
      return this._tryResolve(
        () => this._resolveHasteDependency(fromModule, toModuleName),
        () => this._resolveNodeDependency(fromModule, toModuleName)
      ).then(
        cacheResult,
        forgive,
      );
    }

    return this._resolveNodeDependency(fromModule, toModuleName)
      .then(
        cacheResult,
        forgive,
      );
  }

  getOrderedDependencies({
    response,
    mocksPattern,
    transformOptions,
    onProgress,
    recursive = true,
  }) {
    return this._getAllMocks(mocksPattern).then(allMocks => {
      const entry = this._moduleCache.getModule(this._entryPath);
      const mocks = Object.create(null);

      response.pushDependency(entry);
      let totalModules = 1;
      let finishedModules = 0;

      const resolveDependencies = module =>
        module.getDependencies(transformOptions)
          .then(dependencyNames =>
            Promise.all(
              dependencyNames.map(name => this.resolveDependency(module, name))
            ).then(dependencies => [dependencyNames, dependencies])
          );

      const addMockDependencies = !allMocks
        ? (module, result) => result
        : (module, [dependencyNames, dependencies]) => {
          const list = [module.getName()];
          const pkg = module.getPackage();
          if (pkg) {
            list.push(pkg.getName());
          }
          return Promise.all(list).then(names => {
            names.forEach(name => {
              if (allMocks[name] && !mocks[name]) {
                const mockModule = this._moduleCache.getModule(allMocks[name]);
                dependencyNames.push(name);
                dependencies.push(mockModule);
                mocks[name] = allMocks[name];
              }
            });
            return [dependencyNames, dependencies];
          });
        };

      const collectedDependencies = new MapWithDefaults(module => collect(module));
      const crawlDependencies = (mod, [depNames, dependencies]) => {
        const filteredPairs = [];

        dependencies.forEach((modDep, i) => {
          const name = depNames[i];
          if (modDep == null) {
            // It is possible to require mocks that don't have a real
            // module backing them. If a dependency cannot be found but there
            // exists a mock with the desired ID, resolve it and add it as
            // a dependency.
            if (allMocks && allMocks[name] && !mocks[name]) {
              const mockModule = this._moduleCache.getModule(allMocks[name]);
              mocks[name] = allMocks[name];
              return filteredPairs.push([name, mockModule]);
            }

            debug(
              'WARNING: Cannot find required module `%s` from module `%s`',
              name,
              mod.path
            );
            return false;
          }
          return filteredPairs.push([name, modDep]);
        });

        response.setResolvedDependencyPairs(mod, filteredPairs);

        const dependencyModules = filteredPairs.map(([, m]) => m);
        const newDependencies =
          dependencyModules.filter(m => !collectedDependencies.has(m));

        if (onProgress) {
          finishedModules += 1;
          totalModules += newDependencies.length;
          onProgress(finishedModules, totalModules);
        }

        if (recursive) {
          // doesn't block the return of this function invocation, but defers
          // the resulution of collectionsInProgress.done.then(...)
          dependencyModules
            .forEach(dependency => collectedDependencies.get(dependency));
        }
        return dependencyModules;
      };

      const collectionsInProgress = new AsyncTaskGroup();
      function collect(module) {
        collectionsInProgress.start(module);
        const result = resolveDependencies(module)
          .then(result => addMockDependencies(module, result))
          .then(result => crawlDependencies(module, result));
        const end = () => collectionsInProgress.end(module);
        result.then(end, end);
        return result;
      }

      return Promise.all([
        // kicks off recursive dependency discovery, but doesn't block until it's done
        collectedDependencies.get(entry),

        // resolves when there are no more modules resolving dependencies
        collectionsInProgress.done,
      ]).then(([rootDependencies]) => {
        return Promise.all(
          Array.from(collectedDependencies, resolveKeyWithPromise)
        ).then(moduleToDependenciesPairs =>
          [rootDependencies, new MapWithDefaults(() => [], moduleToDependenciesPairs)]
        );
      }).then(([rootDependencies, moduleDependencies]) => {
        // serialize dependencies, and make sure that every single one is only
        // included once
        const seen = new Set([entry]);
        function traverse(dependencies) {
          dependencies.forEach(dependency => {
            if (seen.has(dependency)) { return; }

            seen.add(dependency);
            response.pushDependency(dependency);
            traverse(moduleDependencies.get(dependency));
          });
        }

        traverse(rootDependencies);
        response.setMocks(mocks);
      });
    });
  }

  _getAllMocks(pattern) {
    // Take all mocks in all the roots into account. This is necessary
    // because currently mocks are global: any module can be mocked by
    // any mock in the system.
    let mocks = null;
    if (pattern) {
      mocks = Object.create(null);
      this._fastfs.matchFilesByPattern(pattern).forEach(file =>
        mocks[path.basename(file, path.extname(file))] = file
      );
    }
    return Promise.resolve(mocks);
  }

  _resolveHasteDependency(fromModule, toModuleName) {
    toModuleName = normalizePath(toModuleName);

    let p = fromModule.getPackage();
    if (p) {
      p = p.redirectRequire(toModuleName);
    } else {
      p = Promise.resolve(toModuleName);
    }

    return p.then((realModuleName) => {
      let dep = this._hasteMap.getModule(realModuleName, this._platform);
      if (dep && dep.type === 'Module') {
        return dep;
      }

      let packageName = realModuleName;
      while (packageName && packageName !== '.') {
        dep = this._hasteMap.getModule(packageName, this._platform);
        if (dep && dep.type === 'Package') {
          break;
        }
        packageName = path.dirname(packageName);
      }

      if (dep && dep.type === 'Package') {
        const potentialModulePath = path.join(
          dep.root,
          path.relative(packageName, realModuleName)
        );
        return this._tryResolve(
          () => this._loadAsFile(
            potentialModulePath,
            fromModule,
            toModuleName,
          ),
          () => this._loadAsDir(potentialModulePath, fromModule, toModuleName),
        );
      }

      throw new UnableToResolveError(
        fromModule,
        toModuleName,
        'Unable to resolve dependency',
      );
    });
  }

  _redirectRequire(fromModule, modulePath) {
    return Promise.resolve(fromModule.getPackage()).then(p => {
      if (p) {
        return p.redirectRequire(modulePath);
      }
      return modulePath;
    });
  }

  _resolveFileOrDir(fromModule, toModuleName) {
    const potentialModulePath = isAbsolutePath(toModuleName) ?
        toModuleName :
        path.join(path.dirname(fromModule.path), toModuleName);

    return this._redirectRequire(fromModule, potentialModulePath).then(
      realModuleName => {
        if (realModuleName === false) {
          return this._loadAsFile(emptyModule, fromModule, toModuleName);
        }

        return this._tryResolve(
          () => this._loadAsFile(realModuleName, fromModule, toModuleName),
          () => this._loadAsDir(realModuleName, fromModule, toModuleName)
        );
      }
    );
  }

  _resolveNodeDependency(fromModule, toModuleName) {
    if (isRelativeImport(toModuleName) || isAbsolutePath(toModuleName)) {
      return this._resolveFileOrDir(fromModule, toModuleName);
    } else {
      return this._redirectRequire(fromModule, toModuleName).then(
        realModuleName => {
          // exclude
          if (realModuleName === false) {
            return this._loadAsFile(emptyModule, fromModule, toModuleName);
          }

          if (isRelativeImport(realModuleName) || isAbsolutePath(realModuleName)) {
            // derive absolute path /.../node_modules/fromModuleDir/realModuleName
            const fromModuleParentIdx = fromModule.path.lastIndexOf('node_modules/') + 13;
            const fromModuleDir = fromModule.path.slice(0, fromModule.path.indexOf('/', fromModuleParentIdx));
            const absPath = path.join(fromModuleDir, realModuleName);
            return this._resolveFileOrDir(fromModule, absPath);
          }

          const searchQueue = [];
          for (let currDir = path.dirname(fromModule.path);
               currDir !== realPath.parse(fromModule.path).root;
               currDir = path.dirname(currDir)) {
            let searchPath = path.join(currDir, 'node_modules');
            if (this._fastfs.dirExists(searchPath)) {
              searchQueue.push(
                path.join(searchPath, realModuleName)
              );
            }
          }

          if (this._extraNodeModules) {
            const bits = toModuleName.split('/');
            const packageName = bits[0];
            if (this._extraNodeModules[packageName]) {
              bits[0] = this._extraNodeModules[packageName];
              searchQueue.push(path.join.apply(path, bits));
            }
          }

          let p = Promise.reject(new UnableToResolveError(fromModule, toModuleName));
          searchQueue.forEach(potentialModulePath => {
            p = this._tryResolve(
              () => this._tryResolve(
                () => p,
                () => this._loadAsFile(potentialModulePath, fromModule, toModuleName),
              ),
              () => this._loadAsDir(potentialModulePath, fromModule, toModuleName)
            );
          });

          return p.catch(error => {
            if (error.type !== 'UnableToResolveError') {
              throw error;
            }
            throw new UnableToResolveError(
              fromModule,
              toModuleName,
              `Module does not exist in the module map ${searchQueue.length ? 'or in these directories:' : ''}\n` +
                searchQueue.map(searchPath => `  ${path.dirname(searchPath)}\n`) + '\n' +
              `This might be related to https://github.com/facebook/react-native/issues/4968\n` +
              `To resolve try the following:\n` +
              `  1. Clear watchman watches: \`watchman watch-del-all\`.\n` +
              `  2. Delete the \`node_modules\` folder: \`rm -rf node_modules && npm install\`.\n` +
              `  3. Reset packager cache: \`rm -fr $TMPDIR/react-*\` or \`npm start -- --reset-cache\`.`
            );
          });
        });
    }
  }

  _loadAsFile(potentialModulePath, fromModule, toModule) {
    return Promise.resolve().then(() => {
      if (this._helpers.isAssetFile(potentialModulePath)) {
        const dirname = path.dirname(potentialModulePath);
        if (!this._fastfs.dirExists(dirname)) {
          throw new UnableToResolveError(
            fromModule,
            toModule,
            `Directory ${dirname} doesn't exist`,
          );
        }

        const {name, type} = getAssetDataFromName(potentialModulePath, this._platforms);

        let pattern = '^' + name + '(@[\\d\\.]+x)?';
        if (this._platform != null) {
          pattern += '(\\.' + this._platform + ')?';
        }
        pattern += '\\.' + type;

        // We arbitrarly grab the first one, because scale selection
        // will happen somewhere
        const [assetFile] = this._fastfs.matches(
          dirname,
          new RegExp(pattern)
        );

        if (assetFile) {
          return this._moduleCache.getAssetModule(assetFile);
        }
      }

      let file;
      if (this._fastfs.fileExists(potentialModulePath)) {
        file = potentialModulePath;
      } else if (this._platform != null &&
                 this._fastfs.fileExists(potentialModulePath + '.' + this._platform + '.js')) {
        file = potentialModulePath + '.' + this._platform + '.js';
      } else if (this._preferNativePlatform &&
                 this._fastfs.fileExists(potentialModulePath + '.native.js')) {
        file = potentialModulePath + '.native.js';
      } else if (this._fastfs.fileExists(potentialModulePath + '.js')) {
        file = potentialModulePath + '.js';
      } else if (this._fastfs.fileExists(potentialModulePath + '.json')) {
        file = potentialModulePath + '.json';
      } else {
        throw new UnableToResolveError(
          fromModule,
          toModule,
          `File ${potentialModulePath} doesnt exist`,
        );
      }

      return this._moduleCache.getModule(file);
    });
  }

  _loadAsDir(potentialDirPath, fromModule, toModule) {
    return Promise.resolve().then(() => {
      if (!this._fastfs.dirExists(potentialDirPath)) {
        throw new UnableToResolveError(
          fromModule,
          toModule,
          `Directory ${potentialDirPath} doesnt exist`,
        );
      }

      const packageJsonPath = path.join(potentialDirPath, 'package.json');
      if (this._fastfs.fileExists(packageJsonPath)) {
        return this._moduleCache.getPackage(packageJsonPath)
          .getMain().then(
            (main) => this._tryResolve(
              () => this._loadAsFile(main, fromModule, toModule),
              () => this._loadAsDir(main, fromModule, toModule)
            )
          );
      }

      return this._loadAsFile(
        path.join(potentialDirPath, 'index'),
        fromModule,
        toModule,
      );
    });
  }

  _resetResolutionCache() {
    this._immediateResolutionCache = Object.create(null);
  }

}

function resolutionHash(modulePath, depName) {
  return `${path.resolve(modulePath)}:${depName}`;
}

function UnableToResolveError(fromModule, toModule, message) {
  Error.call(this);
  Error.captureStackTrace(this, this.constructor);
  this.message = util.format(
    'Unable to resolve module %s from %s: %s',
    toModule,
    fromModule.path,
    message,
  );
  this.type = this.name = 'UnableToResolveError';
}

util.inherits(UnableToResolveError, Error);

function normalizePath(modulePath) {
  if (path.sep === '/') {
    modulePath = path.normalize(modulePath);
  } else if (path.posix) {
    modulePath = path.posix.normalize(modulePath);
  }

  return modulePath.replace(/\/$/, '');
}

function resolveKeyWithPromise([key, promise]) {
  return promise.then(value => [key, value]);
}

function isRelativeImport(path) {
  return /^[.][.]?[/]/.test(path);
}

module.exports = ResolutionRequest;
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { c } from 'ttag';
import {
    Alert,
    SubTitle,
    SmallButton,
    Loader,
    MozillaInfoPanel,
    Progress,
    useModals,
    useSubscription,
    useOrganization,
    useUser
} from 'react-components';
import { CYCLE, PLAN_NAMES, COUPON_CODES } from 'proton-shared/lib/constants';
import humanSize from 'proton-shared/lib/helpers/humanSize';

import { formatPlans, toPlanNames, isBundleEligible } from './helpers';
import CouponDiscountBadge from '../CouponDiscountBadge';
import SubscriptionModal from './SubscriptionModal';

const { MONTHLY, YEARLY, TWO_YEARS } = CYCLE;
const { BUNDLE } = COUPON_CODES;

const getCyclesi18n = () => ({
    [MONTHLY]: c('Billing cycle').t`Monthly`,
    [YEARLY]: c('Billing cycle').t`Yearly`,
    [TWO_YEARS]: c('Billing cycle').t`2-year`
});

const SubscriptionSection = ({ permission }) => {
    const [{ hasPaidMail, hasPaidVpn, isPaid }] = useUser();
    const [subscription, loadingSubscription] = useSubscription();
    const { createModal } = useModals();
    const [
        {
            UsedDomains,
            MaxDomains,
            UsedSpace,
            MaxSpace,
            UsedAddresses,
            MaxAddresses,
            UsedMembers,
            MaxMembers,
            UsedVPN,
            MaxVPN
        } = {},
        loadingOrganization
    ] = useOrganization();

    if (!permission) {
        return (
            <>
                <SubTitle>{c('Title').t`Subscription`}</SubTitle>
                <Alert>{c('Info').t`No subscription yet`}</Alert>
            </>
        );
    }

    const { Plans = [], Cycle, CouponCode, Currency } = subscription;
    const bundleEligible = isBundleEligible(subscription);

    if (subscription.isManagedByMozilla) {
        return (
            <>
                <SubTitle>{c('Title').t`Subscription`}</SubTitle>
                <MozillaInfoPanel />
            </>
        );
    }

    if (loadingSubscription || loadingOrganization) {
        return (
            <>
                <SubTitle>{c('Title').t`Subscription`}</SubTitle>
                <Loader />
            </>
        );
    }

    const { mailPlan, vpnPlan } = formatPlans(Plans);
    const { Name: mailPlanName } = mailPlan || {};
    const canRemoveCoupon = CouponCode && CouponCode !== BUNDLE;
    const i18n = getCyclesi18n();

    const handleModal = (action = '') => () => {
        const coupon = action === 'remove-coupon' ? '' : CouponCode ? CouponCode : undefined; // CouponCode can equals null
        const cycle = action === 'yearly' ? YEARLY : Cycle;
        const plansMap = isPaid ? toPlanNames(subscription.Plans) : { plus: 1, vpnplus: 1 };

        createModal(<SubscriptionModal plansMap={plansMap} coupon={coupon} currency={Currency} cycle={cycle} />);
    };

    return (
        <>
            <SubTitle>{c('Title').t`Subscription`}</SubTitle>

ProtonMail plan

                            {hasPaidMail ? PLAN_NAMES[mailPlanName] : c('Plan').t`Free`}

                            {bundleEligible &&
                                c('Info').t`Combine paid ProtonMail and ProtonVPN and get a 20% discount on both`}

                            <SmallButton onClick={handleModal()}>
                                {hasPaidMail ? c('Action').t`Update` : c('Action').t`Upgrade`}
                            </SmallButton>

                    {hasPaidMail ? (
                        <>

{c('Label').t`Users`}

                                    {`${UsedMembers}/${MaxMembers}`}

                                    <Progress value={(UsedMembers * 100) / MaxMembers} />

{c('Label').t`Email addresses`}

                                    {`${UsedAddresses}/${MaxAddresses}`}

                                    <Progress value={(UsedAddresses * 100) / MaxAddresses} />

{c('Label').t`Storage capacity`}

                                    {`${humanSize(UsedSpace, 'GB', true)}/${humanSize(
                                        MaxSpace,
                                        'GB'
                                    )}`}

                                    <Progress value={(UsedSpace * 100) / MaxSpace} />

{c('Label').t`Custom domains`}

                                    {`${UsedDomains}/${MaxDomains}`}

                                    <Progress value={(UsedDomains * 100) / MaxDomains} />

                            {mailPlanName === 'visionary' ? (

{c('Label').t`VPN connections`}

                                        {`${UsedVPN}/${MaxVPN}`}

                                        <Progress value={(UsedVPN * 100) / MaxVPN} />

                            ) : null}
                        </>
                    ) : null}
                    {mailPlanName === 'visionary' ? null : (

ProtonVPN plan

                                {hasPaidVpn ? PLAN_NAMES[vpnPlan.Name] : c('Plan').t`Free`}

                                {bundleEligible &&
                                    c('Info').t`Combine paid ProtonMail and ProtonVPN and get a 20% discount on both`}

                                <SmallButton onClick={handleModal()}>
                                    {hasPaidVpn ? c('Action').t`Update` : c('Action').t`Upgrade`}
                                </SmallButton>

                    )}
                    {hasPaidVpn ? (

{c('Label').t`VPN connections`}

                                {`${UsedVPN}/${MaxVPN}`}

                                <Progress value={(UsedVPN * 100) / MaxVPN} />

                    ) : null}

{c('Label').t`Billing cycle`}

                            {i18n[Cycle]}

                        {Cycle === MONTHLY && (

{c('Info')
                                .t`Switch to annual billing for a 20% discount`}

                        )}
                        {Cycle === YEARLY && (

{c('Info')
                                .t`20% rebate applied to your subscription`}

                        )}
                        {Cycle === TWO_YEARS && (

{c('Info')
                                .t`33% rebate applied to your subscription`}

                        )}

                            {Cycle === MONTHLY ? (
                                <SmallButton className="pm-button--primary" onClick={handleModal('yearly')}>{c('Action')
                                    .t`Pay yearly`}</SmallButton>
                            ) : null}

                    {CouponCode ? (

{c('Label').t`Coupon`}

                                {CouponCode}

                                <CouponDiscountBadge code={CouponCode} />

                                {canRemoveCoupon ? (
                                    <SmallButton onClick={handleModal('remove-coupon')}>{c('Action')
                                        .t`Remove coupon`}</SmallButton>
                                ) : null}

                    ) : null}

        </>
    );
};

SubscriptionSection.propTypes = {
    permission: PropTypes.bool
};

export default SubscriptionSection;
<commit_msg>Fix loader on subscription section<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { c } from 'ttag';
import {
    Alert,
    SubTitle,
    SmallButton,
    Loader,
    MozillaInfoPanel,
    Progress,
    useModals,
    useSubscription,
    useOrganization,
    useUser
} from 'react-components';
import { CYCLE, PLAN_NAMES, COUPON_CODES } from 'proton-shared/lib/constants';
import humanSize from 'proton-shared/lib/helpers/humanSize';

import { formatPlans, toPlanNames, isBundleEligible } from './helpers';
import CouponDiscountBadge from '../CouponDiscountBadge';
import SubscriptionModal from './SubscriptionModal';

const { MONTHLY, YEARLY, TWO_YEARS } = CYCLE;
const { BUNDLE } = COUPON_CODES;

const getCyclesi18n = () => ({
    [MONTHLY]: c('Billing cycle').t`Monthly`,
    [YEARLY]: c('Billing cycle').t`Yearly`,
    [TWO_YEARS]: c('Billing cycle').t`2-year`
});

const SubscriptionSection = ({ permission }) => {
    const [{ hasPaidMail, hasPaidVpn, isPaid }] = useUser();
    const [subscription, loadingSubscription] = useSubscription();
    const { createModal } = useModals();
    const [
        {
            UsedDomains,
            MaxDomains,
            UsedSpace,
            MaxSpace,
            UsedAddresses,
            MaxAddresses,
            UsedMembers,
            MaxMembers,
            UsedVPN,
            MaxVPN
        } = {},
        loadingOrganization
    ] = useOrganization();

    const subTitle = <SubTitle>{c('Title').t`Subscription`}</SubTitle>;

    if (!permission) {
        return (
            <>
                {subTitle}
                <Alert>{c('Info').t`No subscription yet`}</Alert>
            </>
        );
    }

    if (loadingSubscription || loadingOrganization) {
        return (
            <>
                {subTitle}
                <Loader />
            </>
        );
    }

    const { Plans = [], Cycle, CouponCode, Currency } = subscription;
    const bundleEligible = isBundleEligible(subscription);

    if (subscription.isManagedByMozilla) {
        return (
            <>
                {subTitle}
                <MozillaInfoPanel />
            </>
        );
    }

    const { mailPlan, vpnPlan } = formatPlans(Plans);
    const { Name: mailPlanName } = mailPlan || {};
    const canRemoveCoupon = CouponCode && CouponCode !== BUNDLE;
    const i18n = getCyclesi18n();

    const handleModal = (action = '') => () => {
        const coupon = action === 'remove-coupon' ? '' : CouponCode ? CouponCode : undefined; // CouponCode can equals null
        const cycle = action === 'yearly' ? YEARLY : Cycle;
        const plansMap = isPaid ? toPlanNames(subscription.Plans) : { plus: 1, vpnplus: 1 };

        createModal(<SubscriptionModal plansMap={plansMap} coupon={coupon} currency={Currency} cycle={cycle} />);
    };

    return (
        <>
            {subTitle}

ProtonMail plan

                            {hasPaidMail ? PLAN_NAMES[mailPlanName] : c('Plan').t`Free`}

                            {bundleEligible &&
                                c('Info').t`Combine paid ProtonMail and ProtonVPN and get a 20% discount on both`}

                            <SmallButton onClick={handleModal()}>
                                {hasPaidMail ? c('Action').t`Update` : c('Action').t`Upgrade`}
                            </SmallButton>

                    {hasPaidMail ? (
                        <>

{c('Label').t`Users`}

                                    {`${UsedMembers}/${MaxMembers}`}

                                    <Progress value={(UsedMembers * 100) / MaxMembers} />

{c('Label').t`Email addresses`}

                                    {`${UsedAddresses}/${MaxAddresses}`}

                                    <Progress value={(UsedAddresses * 100) / MaxAddresses} />

{c('Label').t`Storage capacity`}

                                    {`${humanSize(UsedSpace, 'GB', true)}/${humanSize(
                                        MaxSpace,
                                        'GB'
                                    )}`}

                                    <Progress value={(UsedSpace * 100) / MaxSpace} />

{c('Label').t`Custom domains`}

                                    {`${UsedDomains}/${MaxDomains}`}

                                    <Progress value={(UsedDomains * 100) / MaxDomains} />

                            {mailPlanName === 'visionary' ? (

{c('Label').t`VPN connections`}

                                        {`${UsedVPN}/${MaxVPN}`}

                                        <Progress value={(UsedVPN * 100) / MaxVPN} />

                            ) : null}
                        </>
                    ) : null}
                    {mailPlanName === 'visionary' ? null : (

ProtonVPN plan

                                {hasPaidVpn ? PLAN_NAMES[vpnPlan.Name] : c('Plan').t`Free`}

                                {bundleEligible &&
                                    c('Info').t`Combine paid ProtonMail and ProtonVPN and get a 20% discount on both`}

                                <SmallButton onClick={handleModal()}>
                                    {hasPaidVpn ? c('Action').t`Update` : c('Action').t`Upgrade`}
                                </SmallButton>

                    )}
                    {hasPaidVpn ? (

{c('Label').t`VPN connections`}

                                {`${UsedVPN}/${MaxVPN}`}

                                <Progress value={(UsedVPN * 100) / MaxVPN} />

                    ) : null}

{c('Label').t`Billing cycle`}

                            {i18n[Cycle]}

                        {Cycle === MONTHLY && (

{c('Info')
                                .t`Switch to annual billing for a 20% discount`}

                        )}
                        {Cycle === YEARLY && (

{c('Info')
                                .t`20% rebate applied to your subscription`}

                        )}
                        {Cycle === TWO_YEARS && (

{c('Info')
                                .t`33% rebate applied to your subscription`}

                        )}

                            {Cycle === MONTHLY ? (
                                <SmallButton className="pm-button--primary" onClick={handleModal('yearly')}>{c('Action')
                                    .t`Pay yearly`}</SmallButton>
                            ) : null}

                    {CouponCode ? (

{c('Label').t`Coupon`}

                                {CouponCode}

                                <CouponDiscountBadge code={CouponCode} />

                                {canRemoveCoupon ? (
                                    <SmallButton onClick={handleModal('remove-coupon')}>{c('Action')
                                        .t`Remove coupon`}</SmallButton>
                                ) : null}

                    ) : null}

        </>
    );
};

SubscriptionSection.propTypes = {
    permission: PropTypes.bool
};

export default SubscriptionSection;
<|endoftext|>


<commit_before>import React from 'react'
import {shallow} from 'enzyme'

import SearchPanel from './SearchPanel'

const EMPTY_FUNC = () => {}

describe('resource-scheduler', () => {
  describe('components', () => {
    describe('SearchPanel', () => {
      it('should render', () => {
        const wrapper = shallow(
          <SearchPanel
            addCalendarsOfType={EMPTY_FUNC}
            updateRequestedCalendars={EMPTY_FUNC}
            calendarTypes={[]}
          />
        )
        expect(wrapper.find('div')).to.have.length(1)
      })

      // Can not test asynchonously added icons (wrapper.find(Icon) reveals <i/> instead of <Icon />).
      //
      // it('should show colored icon if color is defined', () => {
      //   const color = '#f44242'
      //   const calendarTypes = [
      //     {
      //       name: 'lecturer',
      //       targetEntity: 'User',
      //       formBase: 'UserCalendar',
      //       label: 'Dozent',
      //       color
      //     },
      //     {
      //       name: 'noColor',
      //       targetEntity: 'User',
      //       formBase: 'UserCalendar',
      //       label: 'No Color'
      //     }
      //   ]

      //   const wrapper = render(
      //     <SearchPanel
      //       addCalendarsOfType={EMPTY_FUNC}
      //       updateRequestedCalendars={EMPTY_FUNC}
      //       calendarTypes={calendarTypes}
      //     />
      //   )

      //   expect(wrapper.find('.color-icon')).to.have.length(2)
      //   expect(wrapper.find('.color-icon')).to.have.style('color', color)
      //   expect(wrapper.find('.color-icon').last()).to.not.have.property('style')
      // })
    })
  })
})
<commit_msg>test(resource-scheduler): delete unused test<commit_after>import React from 'react'
import {shallow} from 'enzyme'

import SearchPanel from './SearchPanel'

const EMPTY_FUNC = () => {}

describe('resource-scheduler', () => {
  describe('components', () => {
    describe('SearchPanel', () => {
      it('should render', () => {
        const wrapper = shallow(
          <SearchPanel
            addCalendarsOfType={EMPTY_FUNC}
            updateRequestedCalendars={EMPTY_FUNC}
            calendarTypes={[]}
          />
        )
        expect(wrapper.find('div')).to.have.length(1)
      })
    })
  })
})
<|endoftext|>


<commit_before>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * External dependencies
 */
import { waitFor } from '@testing-library/react';
import { createSolidFromString } from '@web-stories-wp/patterns';

/**
 * Internal dependencies
 */
import { Fixture } from '../../../../../karma';
import useInsertElement from '../../../../canvas/useInsertElement';
import { TEXT_ELEMENT_DEFAULT_FONT } from '../../../../../app/font/defaultFonts';

describe('Link Panel', () => {
  let fixture;
  let linkPanel;
  let safezone;

  beforeEach(async () => {
    fixture = new Fixture();
    await fixture.render();
  });

  afterEach(() => {
    fixture.restore();
  });

  const moveElementToBottom = async (frame) => {
    const safezoneHeight = safezone.getBoundingClientRect().height;
    const frameHeight = frame.getBoundingClientRect().height;
    await fixture.events.mouse.seq(({ moveRel, moveBy, down, up }) => [
      moveRel(frame, 10, 10),
      down(),
      moveBy(0, safezoneHeight - frameHeight - 5, { steps: 10 }),
      up(),
    ]);
  };

  const setPageAttachmentLink = async (link) => {
    const input = fixture.screen.getByLabelText(
      'Type an address to add a page attachment link'
    );
    await fixture.events.click(input, { clickCount: 3 });
    await fixture.events.keyboard.type(link);
    await fixture.events.keyboard.press('tab');
  };

  async function clickOnTarget(target, key = false) {
    const { x, y, width, height } = target.getBoundingClientRect();
    if (key) {
      await fixture.events.keyboard.down(key);
    }
    await fixture.events.mouse.click(x + width / 2, y + height / 2);
    if (key) {
      await fixture.events.keyboard.up(key);
    }
  }

  describe('CUJ: Creator Can Add A Link: Apply a link to any element', () => {
    beforeEach(async () => {
      await fixture.events.click(fixture.editor.library.textAdd);
      await waitFor(() => fixture.editor.canvas.framesLayer.frames[1].node);
      linkPanel = fixture.editor.inspector.designPanel.link;
    });

    it('should correctly show focus border both when using keyboard and mouse', async () => {
      // Click input
      await fixture.events.click(linkPanel.address);

      // Verify input has focus
      expect(linkPanel.address).toHaveFocus();

      // Screenshot it
      await fixture.snapshot('Link input has focus from mouse');

      // shift-tab, then tab to re-focus
      await fixture.events.keyboard.shortcut('shift+tab');
      await fixture.events.keyboard.press('tab');

      // Verify input still has focus
      expect(linkPanel.address).toHaveFocus();

      // Screenshot it
      await fixture.snapshot('Link input has focus from keyboard');
    });

    it('should add protocol automatically on blurring', async () => {
      await fixture.events.click(linkPanel.address);
      await fixture.events.keyboard.type('example.com');

      await fixture.events.keyboard.press('tab');
      expect(linkPanel.address.value).toBe('https://example.com');
    });

    it('should not add additional protocol if already present', async () => {
      await fixture.events.click(linkPanel.address);
      await fixture.events.keyboard.type('https://example.com');

      await fixture.events.keyboard.press('tab');
      expect(linkPanel.address.value).toBe('https://example.com');
    });

    it('should display the link tooltip correctly', async () => {
      const linkDescription = 'Example description';
      // make sure address input exists
      await waitFor(() => linkPanel.address);

      await fixture.events.click(linkPanel.address);
      await fixture.events.keyboard.type('example.com');

      // Debounce time for populating meta-data.
      await fixture.events.keyboard.press('tab');
      await fixture.events.sleep(1200);
      // make sure description input exists
      await waitFor(() => linkPanel.description);
      await fixture.events.click(linkPanel.description, { clickCount: 3 });
      await fixture.events.keyboard.type(linkDescription);
      await fixture.events.keyboard.press('tab');
      // Unselect element.
      const fullbleed = fixture.container.querySelector(
        '[data-testid="fullbleed"]'
      );
      const { left, top } = fullbleed.getBoundingClientRect();
      await fixture.events.mouse.click(left - 5, top - 5);

      // Move mouse to hover over the element.
      const frame = fixture.editor.canvas.framesLayer.frames[1].node;
      await fixture.events.mouse.moveRel(frame, 10, 10);

      expect(fixture.screen.getByText(linkDescription)).toBeTruthy();
      await fixture.snapshot(
        'Element is hovered on. The link tooltip is visible'
      );

      // Select the element again.
      await fixture.events.click(frame);
      await waitFor(() => fixture.editor.inspector.designPanel.link.address);
      await fixture.events.click(
        fixture.editor.inspector.designPanel.link.address,
        { clickCount: 3 }
      );
      await fixture.events.keyboard.press('del');

      // Verify that the description is not displayed when hovering without url.
      await fixture.events.mouse.click(left - 5, top - 5);
      await fixture.events.mouse.moveRel(frame, 10, 10);
      const removedDescription = fixture.screen.queryByText(linkDescription);
      expect(removedDescription).toBeNull();
      await fixture.snapshot(
        'Element is hovered on. The link tooltip is not visible'
      );
    });

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should invoke API when looking up link');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should display error when API errors');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should display link details when API succeeds');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should be able to apply a link to a shape element');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should be able to apply a link to a image element');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should be able to apply a link to a video element');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should not be able to apply a link to a background shape element');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should not be able to apply a link to a background media element');
  });

  describe('CUJ: Creator Can Add A Link: Link with Page Attachment', () => {
    beforeEach(async () => {
      // Select Page.
      safezone = fixture.querySelector('[data-testid="safezone"]');
      await clickOnTarget(safezone);

      // Add Page Attachment
      await setPageAttachmentLink('http://pageattachment.com');
    });

    it('should not allow adding link in Page Attachment area', async () => {
      const insertElement = await fixture.renderHook(() => useInsertElement());
      const element = await fixture.act(() =>
        insertElement('shape', {
          backgroundColor: createSolidFromString('#ff00ff'),
          mask: { type: 'rectangle' },
          x: 10,
          y: 10,
          width: 50,
          height: 50,
        })
      );
      const frame = fixture.editor.canvas.framesLayer.frame(element.id).node;
      await moveElementToBottom(frame);

      linkPanel = fixture.editor.inspector.designPanel.link;
      await fixture.events.click(linkPanel.address);

      await fixture.snapshot('Page Attachment warning & dashed line visible');

      const warning = fixture.screen.getByText(
        'Link can not reside below the dashed line when a page attachment is present'
      );
      expect(warning).toBeDefined();
    });

    it('should not allow adding link to multi-selection in Page Attachment area', async () => {
      const insertElement = await fixture.renderHook(() => useInsertElement());
      const element1 = await fixture.act(() =>
        insertElement('text', {
          font: TEXT_ELEMENT_DEFAULT_FONT,
          content: 'Hello World!',
          x: 40,
          y: 100,
          width: 250,
        })
      );
      const frame = fixture.editor.canvas.framesLayer.frame(element1.id).node;
      await fixture.act(() =>
        insertElement('shape', {
          backgroundColor: createSolidFromString('#ff00ff'),
          mask: { type: 'rectangle' },
          x: 40,
          y: 0,
          width: 50,
          height: 50,
        })
      );
      // Select the first element as well.
      await clickOnTarget(frame, 'Shift');

      await moveElementToBottom(frame);

      linkPanel = fixture.editor.inspector.designPanel.link;
      await fixture.events.click(linkPanel.address);

      await fixture.snapshot(
        'Warning & dashed line visible with multi-selection'
      );

      const warning = fixture.screen.getByText(
        'Link can not reside below the dashed line when a page attachment is present'
      );
      expect(warning).toBeDefined();
    });
  });

  describe('CUJ: Creator Can Add A Link: Apply a link to multi-selection', () => {
    let frame1;
    let frame2;

    beforeEach(async () => {
      safezone = fixture.querySelector('[data-testid="safezone"]');
      // Add two elements.
      const insertElement = await fixture.renderHook(() => useInsertElement());
      // First one with link.
      const element1 = await fixture.act(() =>
        insertElement('text', {
          font: TEXT_ELEMENT_DEFAULT_FONT,
          content: 'Hello World!',
          x: 40,
          y: 0,
          width: 250,
          link: {
            url: 'https://example.com',
          },
        })
      );
      frame1 = fixture.editor.canvas.framesLayer.frame(element1.id).node;
      const element2 = await fixture.act(() =>
        insertElement('text', {
          font: TEXT_ELEMENT_DEFAULT_FONT,
          content: 'Hello again!',
          x: 40,
          y: 100,
          width: 250,
        })
      );
      frame2 = fixture.editor.canvas.framesLayer.frame(element2.id).node;

      // Select both elements.
      await clickOnTarget(frame1, 'Shift');
    });

    it('should allow changing link for two elements at the same time', async () => {
      linkPanel = fixture.editor.inspector.designPanel.link;
      await fixture.events.click(linkPanel.address);

      expect(linkPanel.address.value).toBe('');

      await fixture.events.keyboard.type('http://google.com');

      await fixture.events.keyboard.press('tab');

      // Click the elements separately to verify having the new link set.
      await clickOnTarget(frame1);
      expect(linkPanel.address.value).toBe('http://google.com');

      await clickOnTarget(frame2);
      expect(linkPanel.address.value).toBe('http://google.com');
    });
  });

  describe('CUJ: Creator Can Add A Link: Remove applied link', () => {
    beforeEach(async () => {
      await fixture.events.click(fixture.editor.library.textAdd);
      await waitFor(() => fixture.editor.canvas.framesLayer.frames[1].node);
      linkPanel = fixture.editor.inspector.designPanel.link;
      await fixture.events.click(linkPanel.address);
      await fixture.events.keyboard.type('http://google.com');
    });

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should be able to removed an applied a link');
  });

  // Disable reason: tests not implemented yet
  // eslint-disable-next-line jasmine/no-disabled-tests
  xdescribe('CUJ: Creator Can Add A Link: Edit brand icon', () => {
    it('should be able to edit brand icon');

    it('should be able to remove brand icon');
  });

  // Disable reason: tests not implemented yet
  // eslint-disable-next-line jasmine/no-disabled-tests
  xdescribe('CUJ: Creator Can Add A Link: Edit description', () => {
    it('should be able to edit description');
  });
});
<commit_msg>Flakey test:  should not allow adding link in Page Attachment area (#8714)<commit_after>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * External dependencies
 */
import { waitFor } from '@testing-library/react';
import { createSolidFromString } from '@web-stories-wp/patterns';

/**
 * Internal dependencies
 */
import { Fixture } from '../../../../../karma';
import useInsertElement from '../../../../canvas/useInsertElement';
import { TEXT_ELEMENT_DEFAULT_FONT } from '../../../../../app/font/defaultFonts';

describe('Link Panel', () => {
  let fixture;
  let linkPanel;
  let safezone;

  beforeEach(async () => {
    fixture = new Fixture();
    await fixture.render();
  });

  afterEach(() => {
    fixture.restore();
  });

  const moveElementToBottom = async (frame, frameY = 0) => {
    const safezoneHeight = safezone.getBoundingClientRect().height;
    const frameHeight = frame.getBoundingClientRect().height;
    await fixture.events.mouse.seq(({ moveRel, moveBy, down, up }) => [
      moveRel(frame, 10, 10),
      down(),
      moveBy(0, safezoneHeight - frameHeight - frameY - 50, { steps: 10 }),
      up(),
    ]);
  };

  const setPageAttachmentLink = async (link) => {
    const input = fixture.screen.getByLabelText(
      'Type an address to add a page attachment link'
    );
    await fixture.events.click(input, { clickCount: 3 });
    await fixture.events.keyboard.type(link);
    await fixture.events.keyboard.press('tab');
  };

  async function clickOnTarget(target, key = false) {
    const { x, y, width, height } = target.getBoundingClientRect();
    if (key) {
      await fixture.events.keyboard.down(key);
    }
    await fixture.events.mouse.click(x + width / 2, y + height / 2);
    if (key) {
      await fixture.events.keyboard.up(key);
    }
  }

  async function closePanel(name) {
    const btn = fixture.screen.getByRole('button', { name });
    await fixture.events.click(btn);
  }

  describe('CUJ: Creator Can Add A Link: Apply a link to any element', () => {
    beforeEach(async () => {
      await fixture.events.click(fixture.editor.library.textAdd);
      await waitFor(() => fixture.editor.canvas.framesLayer.frames[1].node);
      linkPanel = fixture.editor.inspector.designPanel.link;
    });

    it('should correctly show focus border both when using keyboard and mouse', async () => {
      // Click input
      await fixture.events.click(linkPanel.address);

      // Verify input has focus
      expect(linkPanel.address).toHaveFocus();

      // Screenshot it
      await fixture.snapshot('Link input has focus from mouse');

      // shift-tab, then tab to re-focus
      await fixture.events.keyboard.shortcut('shift+tab');
      await fixture.events.keyboard.press('tab');

      // Verify input still has focus
      expect(linkPanel.address).toHaveFocus();

      // Screenshot it
      await fixture.snapshot('Link input has focus from keyboard');
    });

    it('should add protocol automatically on blurring', async () => {
      await fixture.events.click(linkPanel.address);
      await fixture.events.keyboard.type('example.com');

      await fixture.events.keyboard.press('tab');
      expect(linkPanel.address.value).toBe('https://example.com');
    });

    it('should not add additional protocol if already present', async () => {
      await fixture.events.click(linkPanel.address);
      await fixture.events.keyboard.type('https://example.com');

      await fixture.events.keyboard.press('tab');
      expect(linkPanel.address.value).toBe('https://example.com');
    });

    it('should display the link tooltip correctly', async () => {
      const linkDescription = 'Example description';
      // make sure address input exists
      await waitFor(() => linkPanel.address);

      await fixture.events.click(linkPanel.address);
      await fixture.events.keyboard.type('example.com');

      // Debounce time for populating meta-data.
      await fixture.events.keyboard.press('tab');
      await fixture.events.sleep(1200);
      // make sure description input exists
      await waitFor(() => linkPanel.description);
      await fixture.events.click(linkPanel.description, { clickCount: 3 });
      await fixture.events.keyboard.type(linkDescription);
      await fixture.events.keyboard.press('tab');
      // Unselect element.
      const fullbleed = fixture.container.querySelector(
        '[data-testid="fullbleed"]'
      );
      const { left, top } = fullbleed.getBoundingClientRect();
      await fixture.events.mouse.click(left - 5, top - 5);

      // Move mouse to hover over the element.
      const frame = fixture.editor.canvas.framesLayer.frames[1].node;
      await fixture.events.mouse.moveRel(frame, 10, 10);

      expect(fixture.screen.getByText(linkDescription)).toBeTruthy();
      await fixture.snapshot(
        'Element is hovered on. The link tooltip is visible'
      );

      // Select the element again.
      await fixture.events.click(frame);
      await waitFor(() => fixture.editor.inspector.designPanel.link.address);
      await fixture.events.click(
        fixture.editor.inspector.designPanel.link.address,
        { clickCount: 3 }
      );
      await fixture.events.keyboard.press('del');

      // Verify that the description is not displayed when hovering without url.
      await fixture.events.mouse.click(left - 5, top - 5);
      await fixture.events.mouse.moveRel(frame, 10, 10);
      const removedDescription = fixture.screen.queryByText(linkDescription);
      expect(removedDescription).toBeNull();
      await fixture.snapshot(
        'Element is hovered on. The link tooltip is not visible'
      );
    });

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should invoke API when looking up link');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should display error when API errors');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should display link details when API succeeds');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should be able to apply a link to a shape element');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should be able to apply a link to a image element');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should be able to apply a link to a video element');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should not be able to apply a link to a background shape element');

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should not be able to apply a link to a background media element');
  });

  describe('CUJ: Creator Can Add A Link: Link with Page Attachment', () => {
    beforeEach(async () => {
      // Select Page.
      safezone = fixture.querySelector('[data-testid="safezone"]');
      await clickOnTarget(safezone);

      // Add Page Attachment
      await setPageAttachmentLink('http://pageattachment.com');
    });

    it('should not allow adding link in Page Attachment area', async () => {
      const insertElement = await fixture.renderHook(() => useInsertElement());
      const element = await fixture.act(() =>
        insertElement('shape', {
          backgroundColor: createSolidFromString('#ff00ff'),
          mask: { type: 'rectangle' },
          x: 100,
          y: 100,
          width: 50,
          height: 50,
        })
      );
      const frame = fixture.editor.canvas.framesLayer.frame(element.id).node;
      await moveElementToBottom(frame, 100);

      await closePanel('Size & Position');
      await closePanel('Saved Colors');
      await closePanel('Layer');

      linkPanel = fixture.editor.inspector.designPanel.link;
      await fixture.events.click(linkPanel.address);

      await fixture.snapshot('Page Attachment warning & dashed line visible');

      const warning = fixture.screen.getByText(
        'Link can not reside below the dashed line when a page attachment is present'
      );
      expect(warning).toBeDefined();
    });

    it('should not allow adding link to multi-selection in Page Attachment area', async () => {
      const insertElement = await fixture.renderHook(() => useInsertElement());
      const element1 = await fixture.act(() =>
        insertElement('text', {
          font: TEXT_ELEMENT_DEFAULT_FONT,
          content: 'Hello World!',
          x: 40,
          y: 140,
          width: 250,
        })
      );
      const frame = fixture.editor.canvas.framesLayer.frame(element1.id).node;
      await fixture.act(() =>
        insertElement('shape', {
          backgroundColor: createSolidFromString('#ff00ff'),
          mask: { type: 'rectangle' },
          x: 40,
          y: 0,
          width: 50,
          height: 50,
        })
      );
      // Select the first element as well.
      await clickOnTarget(frame, 'Shift');

      await moveElementToBottom(frame);

      linkPanel = fixture.editor.inspector.designPanel.link;
      await fixture.events.click(linkPanel.address);

      await fixture.snapshot(
        'Warning & dashed line visible with multi-selection'
      );

      const warning = fixture.screen.getByText(
        'Link can not reside below the dashed line when a page attachment is present'
      );
      expect(warning).toBeDefined();
    });
  });

  describe('CUJ: Creator Can Add A Link: Apply a link to multi-selection', () => {
    let frame1;
    let frame2;

    beforeEach(async () => {
      safezone = fixture.querySelector('[data-testid="safezone"]');
      // Add two elements.
      const insertElement = await fixture.renderHook(() => useInsertElement());
      // First one with link.
      const element1 = await fixture.act(() =>
        insertElement('text', {
          font: TEXT_ELEMENT_DEFAULT_FONT,
          content: 'Hello World!',
          x: 40,
          y: 0,
          width: 250,
          link: {
            url: 'https://example.com',
          },
        })
      );
      frame1 = fixture.editor.canvas.framesLayer.frame(element1.id).node;
      const element2 = await fixture.act(() =>
        insertElement('text', {
          font: TEXT_ELEMENT_DEFAULT_FONT,
          content: 'Hello again!',
          x: 40,
          y: 100,
          width: 250,
        })
      );
      frame2 = fixture.editor.canvas.framesLayer.frame(element2.id).node;

      // Select both elements.
      await clickOnTarget(frame1, 'Shift');
    });

    it('should allow changing link for two elements at the same time', async () => {
      linkPanel = fixture.editor.inspector.designPanel.link;
      await fixture.events.click(linkPanel.address);

      expect(linkPanel.address.value).toBe('');

      await fixture.events.keyboard.type('http://google.com');

      await fixture.events.keyboard.press('tab');

      // Click the elements separately to verify having the new link set.
      await clickOnTarget(frame1);
      expect(linkPanel.address.value).toBe('http://google.com');

      await clickOnTarget(frame2);
      expect(linkPanel.address.value).toBe('http://google.com');
    });
  });

  describe('CUJ: Creator Can Add A Link: Remove applied link', () => {
    beforeEach(async () => {
      await fixture.events.click(fixture.editor.library.textAdd);
      await waitFor(() => fixture.editor.canvas.framesLayer.frames[1].node);
      linkPanel = fixture.editor.inspector.designPanel.link;
      await fixture.events.click(linkPanel.address);
      await fixture.events.keyboard.type('http://google.com');
    });

    // Disable reason: tests not implemented yet
    // eslint-disable-next-line jasmine/no-disabled-tests
    xit('should be able to removed an applied a link');
  });

  // Disable reason: tests not implemented yet
  // eslint-disable-next-line jasmine/no-disabled-tests
  xdescribe('CUJ: Creator Can Add A Link: Edit brand icon', () => {
    it('should be able to edit brand icon');

    it('should be able to remove brand icon');
  });

  // Disable reason: tests not implemented yet
  // eslint-disable-next-line jasmine/no-disabled-tests
  xdescribe('CUJ: Creator Can Add A Link: Edit description', () => {
    it('should be able to edit description');
  });
});
<|endoftext|>