Coder Social home page Coder Social logo

crowdstrike / ember-browser-services Goto Github PK

View Code? Open in Web Editor NEW
46.0 12.0 12.0 2.56 MB

Services for interacting with browser APIs so that you can have fine-grained control in tests.

License: MIT License

JavaScript 22.06% HTML 4.90% Handlebars 0.09% TypeScript 72.95%
ember pwa services testing

ember-browser-services's Introduction

ember-browser-services

CI npm version

ember-browser-services is a collection of Ember Services that allow for consistent interaction with browser APIs.

When all browser APIs are accessed via services, browser behavior is now stubbable in unit tests!

This addon is written in TypeScript so that your editor will provide intellisense hints to guide you through usage so that you don't have to spend as much time looking at the documentation.

Installation

pnpm add ember-browser-services
# or
yarn add ember-browser-services
# or
npm install ember-browser-services
# or
ember install ember-browser-services

Compatibility

  • Ember.js v3.12 or above
  • ember-auto-import v2 or above
  • typescript v4.5 or above
  • embroider max-compat and strict modes

Usage

Whenever you would reach for window, or any other browser API, inject the service instead.

export default class MyComponent extends Component {
  @service('browser/window') window;

  @action
  externalRedirect() {
    this.window.location.href = 'https://crowdstrike.com';
  }
}

Testing

for fuller examples, see the tests directory

There are two types of stubbing you may be interested in when working with browser services

  • service overriding

    As with any service, if the default implementation is not suitable for testing, it may be swapped out during the test.

    import Service from '@ember/service';
    
    module('Scenario Name', function (hooks) {
      test('rare browser API', function (assert) {
        let called = false;
    
        this.owner.register(
          'service:browser/window',
          class TestWindow extends Service {
            rareBrowserApi() {
              called = true;
            }
          },
        );
    
        this.owner.lookup('service:browser/window').rareBrowserApi();
    
        assert.ok(called, 'the browser api was called');
      });
    });
  • direct assignment

    This approach may be useful for deep-objects are complex interactions that otherwise would be hard to reproduce via normal UI interaction.

    module('Scenario Name', function (hooks) {
      test('rare browser API', function (assert) {
        let service = this.owner.lookup('service:browser/window');
        let called = false;
    
        service.rareBrowserApi = () => (called = true);
    
        service.rareBrowserApi();
    
        assert.ok(called, 'the browser api was called');
      });
    });

There is also a shorthand for grouped "modules" in your tests:

Window

import { setupBrowserFakes } from 'ember-browser-services/test-support';

module('Scenario Name', function (hooks) {
  setupBrowserFakes(hooks, { window: true });

  test('is at crowdstrike.com', function (assert) {
    let service = this.owner.lookup('service:browser/window');

    // somewhere in a component or route or service
    // windowService.location = '/';
    assert.equal(service.location.href, '/'); // => succeeds
  });
});

Alternatively, specific APIs of the window can be stubbed with an object

import { setupBrowserFakes } from 'ember-browser-services/test-support';

module('Scenario Name', function (hooks) {
  setupBrowserFakes(hooks, {
    window: { location: { href: 'https://crowdstrike.com' } },
  });

  test('is at crowdstrike.com', function (assert) {
    let service = this.owner.lookup('service:browser/window');

    assert.equal(service.location.href, 'https://crowdstrike.com'); // => succeeds
  });
});

localStorage

import { setupBrowserFakes } from 'ember-browser-services/test-support';

module('Scenario Name', function (hooks) {
  setupBrowserFakes(hooks, { localStorage: true });

  test('local storage service works', function (assert) {
    let service = this.owner.lookup('service:browser/local-storage');

    assert.equal(service.getItem('foo'), null);

    service.setItem('foo', 'bar');
    assert.equal(service.getItem('foo'), 'bar');
    assert.equal(localStorage.getItem('foo'), null);
  });
});

sessionStorage

import { setupBrowserFakes } from 'ember-browser-services/test-support';

module('Scenario Name', function (hooks) {
  setupBrowserFakes(hooks, { sessionStorage: true });

  test('session storage service works', function (assert) {
    let service = this.owner.lookup('service:browser/session-storage');

    assert.equal(service.getItem('foo'), null);

    service.setItem('foo', 'bar');
    assert.equal(service.getItem('foo'), 'bar');
    assert.equal(sessionStorage.getItem('foo'), null);
  });
});

navigator

// An example test from ember-jsqr's tests
module('Scenario Name', function (hooks) {
  setupApplicationTest(hooks);
  setupBrowserFakes(hooks, {
    navigator: {
      mediaDevices: {
        getUserMedia: () => ({ getTracks: () => [] }),
      },
    },
  });

  test('the camera can be turned on and then off', async function (assert) {
    let selector = '[data-test-single-camera-demo] button';

    await visit('/docs/single-camera');
    await click(selector);

    assert.dom(selector).hasText('Stop Camera', 'the camera is now on');

    await click(selector);

    assert.dom(selector).hasText('Start Camera', 'the camera has been turned off');
  });
});

document

import { setupBrowserFakes } from 'ember-browser-services/test-support';

module('Examples: How to use the browser/document service', function (hooks) {
  setupBrowserFakes(hooks, {
    document: {
      title: 'Foo',
    },
  });

  test('title interacts separately from the real document', function (assert) {
    let service = this.owner.lookup('service:browser/document');

    assert.equal(service.title, 'Foo');
    assert.notEqual(service.title, document.title);

    service.title = 'Bar';
    assert.equal(service.title, 'Bar');
    assert.notEqual(service.title, document.title);
  });
});

What about ember-window-mock?

ember-window-mock offers much of the same feature set as ember-browser-services.

ember-browser-services builds on top of ember-window-mock and the two libraries can be used together.

The main differences being:

  • ember-window-mock

    • smaller API surface

    • uses imports for window instead of a service

    • all browser APIs must be accessed from the imported window to be mocked / stubbed

    • adding additional behavior to the test version of an object requires something like:

      import window from 'ember-window-mock';
      
      // ....
      window.location = new TestLocation();
      window.parent.location = window.location;
  • ember-browser-services

    • uses services instead of imports

    • multiple top-level browser APIs, instead of just window

    • setting behavior on services can be done by simply assigning, thanks to ember-window-mock

      let service = this.owner.lookup('service:browser/navigator');
      
      service.someApi = someValue;
    • or adding additional behavior to the test version of an object can be done via familiar service extension like:

      this.owner.register(
        'service:browser/window',
        class extends Service {
          location = new TestLocation();
      
          parent = this;
        },
      );
    • because of the ability to register custom services during tests, if app authors want to customize their own implementation of test services, that can be done without a PR to the addon

    • there is an object short-hand notation for customizing browser APIs via setupBrowserFakes (demonstrated in the above examples)

Similarities / both addons:

  • use proxies to fallback to default browser API behavior
  • provide default stubs for commonly tested behavior (location, localStorage)
  • all state reset between tests

Contributing

See the Contributing guide for details.

License

This project is licensed under the MIT License.

ember-browser-services's People

Contributors

dependabot[bot] avatar ember-tomster avatar github-actions[bot] avatar nelstrom avatar nullvoxpopuli avatar renovate-bot avatar renovate[bot] avatar semantic-release-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ember-browser-services's Issues

Issue with getComputedStyle / breaking tests

Using getComputedStyle with browser service breaks tests, this is cause by the browser service proxying values, since getComputedStyle expects a HTMLElement and not a proxy it thows the following error:

Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.

with the following code

let cssVar = getComputedStyle(this.window.document.documentElement).getPropertyValue( '--css-var', );

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Warning

These dependencies are deprecated:

Datasource Name Replacement PR?
npm @babel/plugin-proposal-class-properties Unavailable
npm @types/ember-qunit Unavailable
npm @types/ember-resolver Unavailable
npm eslint-plugin-node Available

Rate-Limited

These updates are currently rate-limited. Click on a checkbox below to force their creation now.

  • chore(deps): update type definitions (@types/ember__error, @types/ember__owner, @types/ember__polyfills, @types/ember__string, @types/ember__template, @types/htmlbars-inline-precompile, @types/qunit, @types/rsvp)
  • chore(deps): update dependency @changesets/changelog-github to ^0.5.0
  • chore(deps): update dependency @changesets/cli to v2.27.7
  • chore(deps): update dependency @embroider/addon-dev to v3.2.0
  • chore(deps): update dependency @nullvoxpopuli/eslint-configs to v3.2.2
  • chore(deps): update dependency ember-cli-htmlbars to v6.3.0
  • chore(deps): update dependency ember-cli-typescript to v5.3.0
  • chore(deps): update dependency qunit to v2.21.0
  • chore(deps): update dependency rollup-plugin-copy to v3.5.0
  • chore(deps): update dependency rollup-plugin-ts to v3.4.5
  • chore(deps): update pnpm/action-setup action to v2.4.0
  • chore(deps): update actions/checkout action to v4
  • chore(deps): update actions/setup-node action to v4
  • chore(deps): update dependency @ember/test-helpers to v3
  • chore(deps): update dependency @nullvoxpopuli/eslint-configs to v4
  • chore(deps): update dependency ember-cli to v5
  • chore(deps): update dependency ember-cli-app-version to v7
  • chore(deps): update dependency ember-cli-babel to v8
  • chore(deps): update dependency ember-page-title to v8
  • chore(deps): update dependency rollup to v4
  • chore(deps): update embroider monorepo (major) (@embroider/addon-dev, @embroider/test-setup)
  • chore(deps): update framework dependencies (major) (ember-resolver, ember-source)
  • chore(deps): update github artifact actions to v4 (major) (actions/download-artifact, actions/upload-artifact)
  • chore(deps): update lint dependencies (major) (@typescript-eslint/eslint-plugin, @typescript-eslint/parser, ember-template-lint, eslint, eslint-plugin-ember, eslint-plugin-json, eslint-plugin-markdown, eslint-plugin-qunit, eslint-plugin-simple-import-sort, prettier, remark-cli, remark-lint, remark-preset-lint-recommended)
  • chore(deps): update node.js to v20
  • chore(deps): update pnpm/action-setup action to v4
  • chore(deps): update type definitions (major) (@types/ember-qunit, @types/ember-resolver)
  • ๐Ÿ” Create all rate-limited PRs at once ๐Ÿ”

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/actions/assert-build/action.yml
  • actions/upload-artifact v3
.github/actions/download-built-package/action.yml
  • actions/download-artifact v3
.github/actions/pnpm/action.yml
  • pnpm/action-setup v2.2.4
  • actions/setup-node v3
.github/workflows/ci.yml
  • actions/checkout v3
  • actions/checkout v3
  • actions/checkout v3
  • actions/checkout v3
  • actions/checkout v3
  • actions/checkout v3
  • actions/checkout v3
  • actions/checkout v3
  • changesets/action v1
npm
ember-browser-services/package.json
  • @babel/core 7.21.3
  • @babel/eslint-parser ^7.19.1
  • @babel/plugin-proposal-class-properties 7.18.6
  • @babel/plugin-proposal-decorators 7.21.0
  • @babel/plugin-syntax-decorators 7.21.0
  • @babel/plugin-transform-typescript 7.21.3
  • @babel/preset-typescript 7.21.0
  • @embroider/addon-dev 3.0.0
  • @nullvoxpopuli/eslint-configs 3.1.3
  • @types/ember__application ^4.0.0
  • @types/ember__engine ^4.0.0
  • @types/ember__object ^4.0.0
  • @types/ember__service ^4.0.0
  • @types/qunit ^2.11.3
  • @typescript-eslint/eslint-plugin 5.55.0
  • @typescript-eslint/parser ^5.50.0
  • concurrently 7.6.0
  • ember-source 3.28.11
  • eslint ^8.33.0
  • eslint-plugin-decorator-position 5.0.2
  • eslint-plugin-ember 11.4.8
  • eslint-plugin-import 2.27.5
  • eslint-plugin-json 3.1.0
  • eslint-plugin-node 11.1.0
  • eslint-plugin-simple-import-sort 10.0.0
  • prettier ^2.8.3
  • rollup 3.19.1
  • rollup-plugin-copy ^3.4.0
  • rollup-plugin-ts 3.2.0
  • typescript 4.9.5
package.json
  • @changesets/changelog-github ^0.4.8
  • @changesets/cli ^2.26.0
  • prettier ^2.8.3
  • node 18.15.0
  • @types/eslint ^8.0.0
test-app/package.json
  • ember-browser-services *
  • @babel/core 7.21.3
  • @babel/eslint-parser ^7.19.1
  • @ember/optional-features ^2.0.0
  • @ember/test-helpers ^2.8.1
  • @embroider/test-setup 2.1.1
  • @glimmer/component ^1.1.2
  • @glimmer/tracking ^1.1.2
  • @nullvoxpopuli/eslint-configs ^3.1.1
  • @types/ember ^4.0.0
  • @types/ember-qunit ^5.0.0
  • @types/ember-resolver ^5.0.11
  • @types/ember__application ^4.0.0
  • @types/ember__array ^4.0.1
  • @types/ember__component ^4.0.8
  • @types/ember__controller ^4.0.0
  • @types/ember__debug ^4.0.1
  • @types/ember__engine ^4.0.0
  • @types/ember__error ^4.0.0
  • @types/ember__object ^4.0.2
  • @types/ember__owner ^4.0.3
  • @types/ember__polyfills ^4.0.0
  • @types/ember__routing ^4.0.7
  • @types/ember__runloop ^4.0.1
  • @types/ember__service ^4.0.0
  • @types/ember__string ^3.0.9
  • @types/ember__template ^4.0.0
  • @types/ember__test ^4.0.0
  • @types/ember__utils ^4.0.0
  • @types/htmlbars-inline-precompile ^3.0.0
  • @types/qunit ^2.19.0
  • @types/rsvp ^4.0.4
  • @typescript-eslint/eslint-plugin 5.55.0
  • @typescript-eslint/parser ^5.50.0
  • broccoli-asset-rev ^3.0.0
  • concurrently 7.6.0
  • ember-auto-import ^2.4.2
  • ember-cli ~4.11.0
  • ember-cli-app-version ^5.0.0
  • ember-cli-babel ^7.26.3
  • ember-cli-dependency-checker ^3.3.1
  • ember-cli-htmlbars ^6.0.1
  • ember-cli-inject-live-reload ^2.0.2
  • ember-cli-sri ^2.1.1
  • ember-cli-typescript ^5.1.0
  • ember-disable-prototype-extensions ^1.1.3
  • ember-export-application-global ^2.0.1
  • ember-load-initializers ^2.1.2
  • ember-maybe-import-regenerator ^1.0.0
  • ember-page-title ^7.0.0
  • ember-qunit ^5.1.4
  • ember-resolver ^8.0.2
  • ember-source ~4.11.0
  • ember-source-channel-url ^3.0.0
  • ember-template-lint ^5.3.3
  • ember-try ^2.0.0
  • eslint ^8.33.0
  • eslint-plugin-decorator-position ^5.0.2
  • eslint-plugin-ember ^11.4.5
  • eslint-plugin-markdown ^3.0.0
  • eslint-plugin-node ^11.1.0
  • eslint-plugin-qunit ^7.3.4
  • loader.js ^4.7.0
  • prettier ^2.8.3
  • qunit ^2.19.1
  • qunit-console-grouper ^0.3.0
  • qunit-dom ^2.0.0
  • remark-cli ^11.0.0
  • remark-lint ^9.1.1
  • remark-preset-lint-recommended ^6.1.2
  • typescript ^4.7.3
  • webpack ^5.70.0

  • Check this box to trigger a request for Renovate to run again on this repository

Is there any way to set window.location and window.parent.location to different values?

We have a helper called is-running-in-iframe. It compares the value of window.location.href and window.parent.location.href. If these values are different, then (for our purposes) that means our Ember app is running inside an iframe.

In our implementation of the helper, we use @service('browser/window') to help with testing. Our test looks like this (simplified here for clarity):

import { render } from '@ember/test-helpers';
import { setupBrowserFakes } from 'ember-browser-services/test-support';
import { hbs } from 'ember-cli-htmlbars';
import { setupRenderingTest } from 'ember-qunit';
import { module, test } from 'qunit';

module('Integration | Helper | is-running-in-iframe', function (hooks) {
  setupRenderingTest(hooks);
  setupBrowserFakes(hooks, { window: true });

  test('it returns TRUE when the app is running in an IFrame', async function (assert) {
    let browserWindowMock = this.owner.lookup('service:browser/window');
    browserWindowMock.location.href = 'inner';
    browserWindowMock.parent.location.href = 'outer';

    await render(hbs`{{if (is-running-in-iframe) "TRUE" "FALSE"}}`);

    assert.dom(this.element).hasText('TRUE');
  });
});

This test worked with version 1.1.6 of ember-browser-services. After upgrading to version 2.1.0, this test started to fail.

After investigating, I've tracked down the problem to this line of code:

(If I comment out that line, then our test passes.)

I understand that ember-browser-services is intentionally keeping the value of window.location and window.parent.location in sync. I found the relevant tests. But here we have a case where we want to allow those values to diverge.

Do you have any thoughts about how to support this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.