Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jest-environment-jsdom should export jsdom globally #11001

Closed
phawxby opened this issue Jan 7, 2021 · 4 comments
Closed

jest-environment-jsdom should export jsdom globally #11001

phawxby opened this issue Jan 7, 2021 · 4 comments

Comments

@phawxby
Copy link
Contributor

phawxby commented Jan 7, 2021

🚀 Feature Proposal

jest-environment-jsdom should export jsdom globally.

Motivation

Essentially because of all the hacks discussed in this thread and this thread to modify window.location. Being able to use the jsdom API's directly would be much more sensible that deleting location, object defining properties, etc.

Example

jsdom.reconfigure({
  url: "https://www.foo.com/bar",
});

Pitch

Although typically changes to jsdom test environments are not normally approved this is a safe an innocuous change which would allow tests to use jsdom API correctly rather than having to hack in changes or use a custom environment for something which should already be exposed.

@cpojer has suggested this would be a sensible addition in the past.

Workaround

jest-environment-jsdom-global already exists, but this should be part of core.

@sanek306
Copy link

sanek306 commented Mar 8, 2021

Before it, maybe, it's help anyone.

function shallowClone(obj, assignObj) {
  const clone = Object.create(Object.getPrototypeOf(obj));
  const descriptors = Object.getOwnPropertyDescriptors(obj);
  const assignObjDescriptors = Object.getOwnPropertyDescriptors(
    Object.getPrototypeOf(assignObj),
  );
  Object.defineProperties(clone, { ...descriptors, ...assignObjDescriptors });
  Object.keys(assignObj).forEach((key) => {
    Object.defineProperty(clone, key, {
      configurable: true, value: assignObj[key], writable: true,
    });
  });
  return clone;
}

const safelyStubAndThenCleanup = (target, method, value = {}) => {
  const original = target[method];

  beforeEach(() => {
    Object.defineProperty(target, method, {
      configurable: true, value: shallowClone(original, value),
    });
  });
  afterEach(() => {
    delete target[method];
    target[method] = original;
  });
};

const safelyStubAndThenCleanupWindow = () => {
  MockLocation.mockHistoryState();
  safelyStubAndThenCleanup(window, 'location', new MockLocation(window.location.href));
};

const getAbsoluteUrl = (relativeUrl) => {
  const { location: { origin } } = window;

  if (relativeUrl) {
    const preparedRelativeUrl = relativeUrl[0] === '/'
      ? relativeUrl.substring(1, relativeUrl.length)
      : relativeUrl;

    return relativeUrl ? `${origin}/${preparedRelativeUrl}` : origin;
  }

  return origin;
}

class MockLocation {
  constructor(url) {
    const {
      hash,
      hostname,
      origin,
      pathname,
      port,
      protocol,
      search,
    } = new URL(url);

    this.hash = hash;
    this.hostname = hostname;
    this.protocol = protocol;
    this.pathname = pathname;
    this.search = search;
    this.origin = origin || hostname;
    this.port = port;
  }

  static mockHistoryState() {
    const { pushState, replaceState } = window.history;

    window.history.pushState = (...arg) => {
      const url = arg[2];
      window.location.href = url;
      pushState.apply(window.history, arg);
    };

    window.history.replaceState = (...arg) => {
      const url = arg[2];
      window.location.href = url;
      replaceState.apply(window.history, arg);
    };
  }

  get href() {
    return `${this._protocol}//${this.host}${this._pathname}${this._search}`;
  }

  set href(value) {
    const isRelativeUrl = !value.includes(this.protocol);
    const {
      hash,
      href,
      hostname,
      origin,
      pathname,
      port,
      protocol,
      search,
    } = new URL(isRelativeUrl ? getAbsoluteUrl(value) : value);
    this.hash = hash;
    this.hostname = hostname;
    this.protocol = protocol;
    this.pathname = pathname;
    this.search = search;
    this.origin = origin || hostname;
    this.port = port;

    return href;
  }

  get protocol() {
    return this._protocol;
  }

  set protocol(value) {
    this._protocol = value;
  }

  get host() {
    return `${this._hostname}${this.port ? ':' : ''}${this.port}`;
  }

  get hostname() {
    return this._hostname;
  }

  set hostname(value) {
    this._hostname = value;
  }

  get port() {
    return this._port;
  }

  set port(value) {
    this._port = value;
  }

  get hash() {
    return this._hash;
  }

  set hash(value) {
    this._hash = value;
  }

  get pathname() {
    return this._pathname;
  }

  set pathname(value) {
    this._pathname = value;
  }

  get search() {
    return this._search;
  }

  set search(value) {
    this._search = value;
  }

  get origin() {
    return this._origin;
  }

  set origin(value) {
    this._origin = value;
  }

  assign(value) {
    this.href = value;
  }

  replace(value) {
    this.href = value;
  }

  toString() {
    return this.href;
  }
}

You just need write in setupTests.js

safelyStubAndThenCleanupWindow();

@jcrben
Copy link

jcrben commented Apr 25, 2021

jest-environment-jsdom-global already exists, but this should be part of core.

This can't be used in an unejected CRA which is another unfortunate current circumstance

@SimenB
Copy link
Member

SimenB commented Apr 25, 2021

jsdom is an implementation detail. If you want to expose it, you can create your own environment, I don't think it should be exposed by default.

I agree it's a bit of a pain for navigation, but IMO not enough to leak Jest's implementation details.


If CRA doesn't allow you to use a custom env, I'd open up an issue with them

@SimenB SimenB closed this as completed Apr 25, 2021
@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 26, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants