Mirage tests : what to use instead of #clear, #pushObjects methods

I have the following model and afterModel hooks in the route:

export default Route.extend(AuthenticatedRouteMixin, {
  currentShop: service(),

  async model() {
    return {
      events:     await this.store.query('event', { country_id: this.get('currentShop.shop.country.id')}),
      shopEvents: await this.store.query('shop-event', { shop_identifier: this.get('currentShop.shop.identifier') })
    }
  },

  afterModel(model) {
    let currentShopEvents = this.get('currentShop.shop.shopEvents');
    console.log("currentShopEvents: " + currentShopEvents);
    if(currentShopEvents) {
      currentShopEvents.clear();      
    }
    this.get('currentShop.shop').set('shopEvents', model.shopEvents);
  },
...

It works as needed in the app. The problem comes in an acceptance test:

TypeError: currentShopEvents.clear is not a function

The difference seems to be in what kind of object is this.get('currentShop.shop.shopEvents');:

  • when running the test, it is collection:shop-event(1)
  • when running the app, it is <DS.ManyArray:ember2198>

After looking in the documentation of EC Mirage for Collection, it DOES not contain clear method compared to ED MutableArray one.

Does anybody have an idea what to use in this case to make it work both in tests and the app? Here is the failing test:

module('Acceptance | Country events', function(hooks) {
  setupWindowMock(hooks);
  setupApplicationTest(hooks);
  setupMirage(hooks);
  
  hooks.beforeEach(function() {
    let country = this.server.create('country');
    let event = this.server.create('event', { country });  
      
    let currentShop = this.owner.lookup('service:current-shop');
    let shop = this.server.create('shop', { country });
    
    this.server.create('shopEvent', {
      shop: shop,
      event: event
    });
    currentShop.setShop(shop);
  });

  test('Authenticated users can visit /country-events', async function(assert) {
    this.server.create('user');
    await authenticateSession({
      access_token: 'azerty',
      token_type: 'Bearer'
    });

    await visit('/country-events');
    assert.equal(currentURL(), '/country-events', 'user is on the Events page');
  });
});

I tried to change little bit to not use clear method:

afterModel(model) {
    let currentShopEvents = this.get('currentShop.shop.shopEvents');
    if(currentShopEvents) {
      currentShopEvents.set('shopEvents', []);      
    }
    currentShopEvents.pushObjects(model.shopEvents);
  },

but time EC Mirage raises TypeError: currentShopEvents.set is not a function. I think it will not like pushObjects either :confused:

Thank you!

I think the problem is in your test. When you use this.server.create it doesn’t return an Ember Data model, it returns a Mirage model, which is totally different. It helps to always think of mirage as if it is your backend. Suppose you had a rails backend, and an Ember frontend, it wouldn’t make sense to “give” Ember a Rails model. Similarly Ember doesn’t really know how to handle a mirage model.

Typically an acceptance test is written in such a way that the app should behave basically the same as it does in real-world usage. Here you’re doing some magic in the background… like setting the current shop via the service, but typically in an acceptance test you mostly create backend records (typically the minimum amount required to test what you need), then mock authentication, and let the client request all the records it needs (thus “fetching” records from mirage => ember data, which solves the problem you’re seeing). Then you would use the test helpers to operate the app as if it was a user making interactions. That means if you’re writing an acceptance test you should probably set the current shop via the UI instead of through the shortcut method. What you’re attempting is more common in integration/rendering tests.

Anyway hope that helps, let me know if you have any follow up questions to that.

@dknutsen: Thank you for your response. I still can’t figure out the way to do the following thing:

  • navigate to the country-events page and check some data.

To display data on that page, the corresponding route should load:

  • all the country events
  • all the shop events
  • set shop events on the currentShop(service).

Sure, in an acceptance tests I’ll have to create some data to be able to display it on the page. The problem is whatever method I use (set*, push*, etc.), none of them is available in EC Mirage Collection. How to do in this case ? Should I trick something in EC Mirage factories? I already have event, shop, shop-event factories:

# event.js

import { Factory } from 'ember-cli-mirage';
import faker from 'faker';

export default Factory.extend({
  name: faker.company.companyName()
});

# shop-event.js

import { Factory, association } from 'ember-cli-mirage';

export default Factory.extend({
  shop:  association(),
  event: association()
});

# shop.js

import { Factory, association } from 'ember-cli-mirage';
import faker from 'faker';

export default Factory.extend({
  category:   faker.commerce.department(),
  country:    association(),
  fax:        faker.phone.phoneNumber(),
  identifier: faker.random.number(),
  name:       faker.company.companyName(),
  phone:      faker.phone.phoneNumber()
});
...

And Mirage docs say

If you're using Ember Data, Mirage's ORM will automatically register your Ember Data models for you at run time, so you don't have to duplicate your domain information in two places.

The problem is whatever method I use ( set* , push* , etc.), none of them is available in EC Mirage Collection

So what I was getting at is that this doesn’t matter because your actual front-end Ember code should never touch a Mirage collection. You create mirage models in Mirage using the server.create API, and then (usually) just visit the page in your test to let your Ember app fetch all the data from mirage.

You should remove these lines from your test:

    let currentShop = this.owner.lookup('service:current-shop');
    ...
    currentShop.setShop(shop);

Then let the app fetch the shop(s) and set the current shop however that happens if you’re running the app in development/production mode.

Removed it:

module('Acceptance | Country events', function(hooks) {
  setupWindowMock(hooks);
  setupApplicationTest(hooks);
  setupMirage(hooks);

  hooks.beforeEach(function() {
    let country = this.server.create('country');
    let event = this.server.create('event', { country });

    let shop = this.server.create('shop', { country });

    this.server.create('shopEvent', {
      shop: shop,
      event: event
    });
  });

  test('Authenticated users can visit /country-events', async function(assert) {
    this.server.create('user');
    await authenticateSession({
      access_token: 'azerty',
      token_type: 'Bearer'
    });

    await visit('/country-events');
    assert.equal(currentURL(), '/country-events', 'user is on the Events page');
  });
});

and it raise another error:

Acceptance | Country events: Authenticated users can visit /country-events
    ✘ Promise rejected during "Authenticated users can visit /country-events": Cannot read property 'belongsTo' of null
        TypeError: Cannot read property 'belongsTo' of null
            at Class.countryId (http://localhost:7357/assets/decastore-front.js:242:25)

A problem with a factory ? I defined a shop factory as follows:

#factories/shop.js

import { Factory, association } from 'ember-cli-mirage';
import faker from 'faker';

export default Factory.extend({
  category:   faker.commerce.department(),
  country:    association(),
  fax:        faker.phone.phoneNumber(),
  identifier: faker.random.number(),
  name:       faker.company.companyName(),
  phone:      faker.phone.phoneNumber()
});

and shop-event.js factory like this:

import { Factory, association } from 'ember-cli-mirage';

export default Factory.extend({
  shop:  association(),
  event: association()
});

event.js factory:

import { Factory, association } from 'ember-cli-mirage';
import faker from 'faker';

export default Factory.extend({
  name: faker.company.companyName(),
  country: association()
});

What’s wrong here? I think the erros comes from the route model hook when fetching events:

events:     await this.store.query('event', { country_id: this.get('currentShop.shop.country.id')}),
...

that’s why I had to set up the currentShop.shop value. No?

The proof is that when I comment out afterModel hook, the above error is no more there, but another 3, much more weird are there (with on test passing, yay !):

The displayed assertion is really weird:

expecting [object Object], got [object Object]

I also use ember-data-url-templates and the adapter for events is defined as follows:

# adapters/event.js

export default ApplicationAdapter.extend(UrlTemplates, {
  urlTemplate: '{+host}/countries/{countryId}/events',
  findAllUrlTemplate: '{+host}/countries/{countryId}/events',
  createRecordUrlTemplate: '{+host}/countries/{countryId}/events',
  findRecordUrlTemplate: '{+host}/countries/{countryId}/events/{id}',
  queryRecordUrlTemplate: '{+host}/countries/{countryId}/events/{id}',
  updateRecordUrlTemplate: '{+host}/countries/{countryId}/events/{id}',
  deleteRecordUrlTemplate: '{+host}/countries/{countryId}/events/{id}',

  urlSegments: {
    countryId: function(type, id, snapshot, query) {
      if (query && query.country_id) {
        return query.country_id;
      }
      return snapshot.belongsTo('country', { id: true });
    },

    id: function(type, id, snapshot) {
      return snapshot.id;
    }
  }

May be the line return snapshot.belongsTo('country', { id: true }); causes the error related to belongs_to null …

Finally, it seems like it was a code organization problem. I hadn’t use the afterModel hook at all:

async afterModel(model) {
    let currentShopEvents = await this.get('currentShop.shop.shopEvents');
    await currentShopEvents.clear();
    currentShopEvents.pushObjects(model.shopEvents);
  }

because I’ve already got the events associated with a current shop in the model hook:

async model() {
    return {
      events:     await this.store.query('event', { country_id: this.get('currentShop.shop.country.id')}),
      shopEvents: await this.store.query('shop-event', { shop_identifier: this.get('currentShop.shop.identifier') })
    }
  },

The only place where I used the current.shop.shopEvents was in the corresponding controller CP:

# controllers/country-events/index.js

export default Controller.extend({
  currentShop:   service(),
...
shopEventsIds: computed('currentShop.shop.shopEvents.[]', function() {
    let events = this.get('currentShop.shop.shopEvents');
    return events.map(e => e.get('event.id'));
  }),
...

So, as the route model is available in the controller, i juts replaced it with the following:

shopEventsIds: computed('model.shopEvents.[]', function() {
    let events = this.get('model.shopEvents');
    return events.map(e => e.get('event.id'));
  }),

and left the test code as it was, - it worked :slight_smile:.

1 Like

Awesome! Glad you got it working. Debugging those weird stack traces in acceptance tests can be frustrating sometimes but the cause is usually fairly simple even if figuring out where isn’t so much