From bbda1469370cda2b7e4ac804ecb3ce58584f1723 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Mon, 23 Jul 2018 11:21:00 -1000 Subject: [PATCH] [BUGFIX] fix and tests for belongs-to proxy not properly updating (#5533) * adds failing tests for #5511 and #5517 * add a test for #5525 that suspiciously passes * adds test for #5522 isEmpty issue * run prettier * fix issue with proxy * upgrade test for potential issue with create, still passes * fix emberobserver issue for missing data member in payloads --- addon/-legacy-private/system/model/states.js | 1 + .../system/relationships/state/belongs-to.js | 17 +- .../relationships/state/relationship.js | 8 +- addon/-legacy-private/system/store.js | 2 +- .../system/model/model-data.js | 2 +- .../system/model/states.js | 1 + .../relationships/state/relationship.js | 8 +- addon/-record-data-private/system/store.js | 5 +- .../integration/records/create-record-test.js | 84 +++++ tests/integration/records/edit-record-test.js | 344 ++++++++++++++++++ tests/integration/records/load-test.js | 205 +++++++++-- .../relationships/json-api-links-test.js | 90 +++++ tests/unit/store/push-test.js | 2 +- 13 files changed, 734 insertions(+), 35 deletions(-) create mode 100644 tests/integration/records/edit-record-test.js diff --git a/addon/-legacy-private/system/model/states.js b/addon/-legacy-private/system/model/states.js index 7697a1f1fd9..33aefc63e1b 100644 --- a/addon/-legacy-private/system/model/states.js +++ b/addon/-legacy-private/system/model/states.js @@ -521,6 +521,7 @@ const RootState = { loading: { // FLAGS isLoading: true, + isEmpty: true, exit(internalModel) { internalModel._promiseProxy = null; diff --git a/addon/-legacy-private/system/relationships/state/belongs-to.js b/addon/-legacy-private/system/relationships/state/belongs-to.js index 78733dbe78c..ddd07937733 100644 --- a/addon/-legacy-private/system/relationships/state/belongs-to.js +++ b/addon/-legacy-private/system/relationships/state/belongs-to.js @@ -34,6 +34,7 @@ export default class BelongsToRelationship extends Relationship { } else if (this.inverseInternalModel) { this.removeInternalModel(this.inverseInternalModel); } + this.setHasAnyRelationshipData(true); this.setRelationshipIsStale(false); this.setRelationshipIsEmpty(false); @@ -163,6 +164,12 @@ export default class BelongsToRelationship extends Relationship { } notifyBelongsToChange() { + if (this._promiseProxy !== null) { + let iM = this.inverseInternalModel; + + this._updateLoadingPromise(proxyRecord(iM), iM ? iM.getRecord() : null); + } + this.internalModel.notifyBelongsToChange(this.key); } @@ -242,9 +249,7 @@ export default class BelongsToRelationship extends Relationship { if (this.isAsync) { if (this._promiseProxy === null) { - let promise = resolve(this.inverseInternalModel).then(internalModel => { - return internalModel ? internalModel.getRecord() : null; - }); + let promise = proxyRecord(this.inverseInternalModel); this._updateLoadingPromise(promise, record); } @@ -282,6 +287,12 @@ export default class BelongsToRelationship extends Relationship { } } +function proxyRecord(internalModel) { + return resolve(internalModel).then(resolvedInternalModel => { + return resolvedInternalModel ? resolvedInternalModel.getRecord() : null; + }); +} + function handleCompletedFind(relationship, error) { let internalModel = relationship.inverseInternalModel; diff --git a/addon/-legacy-private/system/relationships/state/relationship.js b/addon/-legacy-private/system/relationships/state/relationship.js index 9f48256ee16..e53e2f1c76c 100644 --- a/addon/-legacy-private/system/relationships/state/relationship.js +++ b/addon/-legacy-private/system/relationships/state/relationship.js @@ -60,6 +60,7 @@ export default class Relationship { this.canonicalMembers = new OrderedSet(); this.store = store; this.key = relationshipMeta.key; + this.kind = relationshipMeta.kind; this.inverseKey = inverseKey; this.internalModel = internalModel; this.isAsync = typeof async === 'undefined' ? true : async; @@ -526,7 +527,7 @@ export default class Relationship { warn( `You pushed a record of type '${this.internalModel.modelName}' with a relationship '${ this.key - }' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload.`, + }' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, this.isAsync || this.hasAnyRelationshipData, { id: 'ds.store.push-link-for-sync-relationship', @@ -696,6 +697,11 @@ export default class Relationship { this.updateData(payload.data, initial); } else if (payload._partialData !== undefined) { this.updateData(payload._partialData, initial); + } else if (this.isAsync === false) { + hasRelationshipDataProperty = true; + let data = this.kind === 'hasMany' ? [] : null; + + this.updateData(data, initial); } if (payload.links && payload.links.related) { diff --git a/addon/-legacy-private/system/store.js b/addon/-legacy-private/system/store.js index e9612191bdd..793ed7d4712 100644 --- a/addon/-legacy-private/system/store.js +++ b/addon/-legacy-private/system/store.js @@ -3162,7 +3162,7 @@ function setupRelationships(store, internalModel, data, modelNameToInverseMap) { warn( `You pushed a record of type '${ internalModel.modelName - }' with a relationship '${relationshipName}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload.`, + }' with a relationship '${relationshipName}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, isAsync || relationshipData.data, { id: 'ds.store.push-link-for-sync-relationship', diff --git a/addon/-record-data-private/system/model/model-data.js b/addon/-record-data-private/system/model/model-data.js index 4b8aafe0d21..6c6e0262a71 100644 --- a/addon/-record-data-private/system/model/model-data.js +++ b/addon/-record-data-private/system/model/model-data.js @@ -106,7 +106,7 @@ export default class ModelData { warn( `You pushed a record of type '${ this.modelName - }' with a relationship '${relationshipName}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload.`, + }' with a relationship '${relationshipName}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, isAsync || relationshipData.data, { id: 'ds.store.push-link-for-sync-relationship', diff --git a/addon/-record-data-private/system/model/states.js b/addon/-record-data-private/system/model/states.js index 98cf914167e..035da6d7b47 100644 --- a/addon/-record-data-private/system/model/states.js +++ b/addon/-record-data-private/system/model/states.js @@ -518,6 +518,7 @@ const RootState = { // XHR to retrieve the data. loading: { // FLAGS + isEmpty: true, isLoading: true, exit(internalModel) { diff --git a/addon/-record-data-private/system/relationships/state/relationship.js b/addon/-record-data-private/system/relationships/state/relationship.js index fe4f5a933b6..5122d633e31 100644 --- a/addon/-record-data-private/system/relationships/state/relationship.js +++ b/addon/-record-data-private/system/relationships/state/relationship.js @@ -55,6 +55,7 @@ export default class Relationship { constructor(store, inverseKey, relationshipMeta, modelData, inverseIsAsync) { heimdall.increment(newRelationship); this.inverseIsAsync = inverseIsAsync; + this.kind = relationshipMeta.kind; let async = relationshipMeta.options.async; let polymorphic = relationshipMeta.options.polymorphic; this.modelData = modelData; @@ -572,7 +573,7 @@ export default class Relationship { warn( `You pushed a record of type '${this.modelData.modelName}' with a relationship '${ this.key - }' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload.`, + }' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`, this.isAsync || this.hasAnyRelationshipData, { id: 'ds.store.push-link-for-sync-relationship', @@ -647,6 +648,11 @@ export default class Relationship { if (payload.data !== undefined) { hasRelationshipDataProperty = true; this.updateData(payload.data, initial); + } else if (this.isAsync === false) { + hasRelationshipDataProperty = true; + let data = this.kind === 'hasMany' ? [] : null; + + this.updateData(data, initial); } if (payload.links && payload.links.related) { diff --git a/addon/-record-data-private/system/store.js b/addon/-record-data-private/system/store.js index 7b47948cba7..2ffecb36026 100644 --- a/addon/-record-data-private/system/store.js +++ b/addon/-record-data-private/system/store.js @@ -4,7 +4,7 @@ import { A } from '@ember/array'; import EmberError from '@ember/error'; import MapWithDefault from './map-with-default'; -import { run as emberRun } from '@ember/runloop'; +import { run as emberRunLoop } from '@ember/runloop'; import { set, get, computed } from '@ember/object'; import { assign } from '@ember/polyfills'; import { default as RSVP, Promise } from 'rsvp'; @@ -52,6 +52,7 @@ import ModelData from './model/model-data'; import edBackburner from './backburner'; const badIdFormatAssertion = '`id` passed to `findRecord()` has to be non-empty string or number'; +const emberRun = emberRunLoop.backburner; const { ENV } = Ember; let globalClientIdCounter = 1; @@ -2033,7 +2034,7 @@ Store = Service.extend({ snapshot: snapshot, resolver: resolver, }); - emberRun.once(this, this.flushPendingSave); + emberRun.scheduleOnce('actions', this, this.flushPendingSave); }, /** diff --git a/tests/integration/records/create-record-test.js b/tests/integration/records/create-record-test.js index 59b0040c433..1b11ba3da50 100644 --- a/tests/integration/records/create-record-test.js +++ b/tests/integration/records/create-record-test.js @@ -1,18 +1,25 @@ import { module, test } from 'qunit'; +import JSONAPIAdapter from 'ember-data/adapters/json-api'; +import JSONAPISerializer from 'ember-data/serializers/json-api'; import { setupTest } from 'ember-qunit'; import Store from 'ember-data/store'; import Model from 'ember-data/model'; +import { resolve } from 'rsvp'; import { attr, belongsTo, hasMany } from '@ember-decorators/data'; class Person extends Model { @hasMany('pet', { inverse: 'owner', async: false }) pets; + @belongsTo('pet', { inverse: 'bestHuman', async: true }) + bestDog; @attr name; } class Pet extends Model { @belongsTo('person', { inverse: 'pets', async: false }) owner; + @belongsTo('person', { inverse: 'bestDog', async: false }) + bestHuman; @attr name; } @@ -111,4 +118,81 @@ module('Store.createRecord() coverage', function(hooks) { .map(pet => pet.get('name')); assert.deepEqual(pets, [], 'Chris no longer has any pets'); }); + + test('creating and saving a record with relationships puts them into the correct state', async function(assert) { + this.owner.register( + 'serializer:application', + JSONAPISerializer.extend({ + normalizeResponse(_, __, data) { + return data; + }, + }) + ); + this.owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + shouldBackgroundReload() { + return false; + }, + findRecord() { + assert.ok(false, 'Adapter should not make any findRecord Requests'); + }, + findBelongsTo() { + assert.ok(false, 'Adapter should not make any findBelongsTo Requests'); + }, + createRecord() { + return resolve({ + data: { + type: 'pet', + id: '2', + attributes: { name: 'Shen' }, + relationships: { + bestHuman: { + data: { type: 'person', id: '1' }, + links: { self: './person', related: './person' }, + }, + }, + }, + }); + }, + }) + ); + + let chris = store.push({ + data: { + id: '1', + type: 'person', + attributes: { + name: 'Chris', + }, + relationships: { + bestDog: { + data: null, + links: { self: './dog', related: './dog' }, + }, + }, + }, + }); + + let shen = store.createRecord('pet', { + name: 'Shen', + bestHuman: chris, + }); + + let bestHuman = shen.get('bestHuman'); + let bestDog = await chris.get('bestDog'); + + // check that we are properly configured + assert.ok(bestHuman === chris, 'Precondition: Shen has bestHuman as Chris'); + assert.ok(bestDog === shen, 'Precondition: Chris has Shen as his bestDog'); + + await shen.save(); + + bestHuman = shen.get('bestHuman'); + bestDog = await chris.get('bestDog'); + + // check that the relationship has remained established + assert.ok(bestHuman === chris, 'Shen bestHuman is still Chris'); + assert.ok(bestDog === shen, 'Chris still has Shen as bestDog'); + }); }); diff --git a/tests/integration/records/edit-record-test.js b/tests/integration/records/edit-record-test.js new file mode 100644 index 00000000000..3a8e837cc63 --- /dev/null +++ b/tests/integration/records/edit-record-test.js @@ -0,0 +1,344 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import Store from 'ember-data/store'; +import Model from 'ember-data/model'; +import { attr, belongsTo, hasMany } from '@ember-decorators/data'; + +class Person extends Model { + @hasMany('pet', { inverse: 'owner', async: false }) + pets; + @hasMany('person', { inverse: 'friends', async: true }) + friends; + @belongsTo('person', { inverse: 'bestFriend', async: true }) + bestFriend; + @attr name; +} + +class Pet extends Model { + @belongsTo('person', { inverse: 'pets', async: false }) + owner; + @attr name; +} + +module('Editing a Record', function(hooks) { + let store; + setupTest(hooks); + + hooks.beforeEach(function() { + let { owner } = this; + owner.register('service:store', Store); + owner.register('model:person', Person); + owner.register('model:pet', Pet); + store = owner.lookup('service:store'); + }); + + module('Simple relationship addition case', function() { + module('Adding a sync belongsTo relationship to a record', function() { + test('We can add to a record', async function(assert) { + let chris = store.push({ + data: { + id: '1', + type: 'person', + attributes: { name: 'Chris' }, + relationships: { + pets: { + data: [], + }, + }, + }, + }); + + let pet = store.push({ + data: { + id: '1', + type: 'pet', + attributes: { name: 'Shen' }, + relationships: { + owner: { + data: null, + }, + }, + }, + }); + + // check that we are properly configured + assert.ok(pet.get('owner') === null, 'Precondition: Our owner is null'); + + let pets = chris + .get('pets') + .toArray() + .map(pet => pet.get('name')); + assert.deepEqual(pets, [], 'Precondition: Chris has no pets'); + + pet.set('owner', chris); + + assert.ok(pet.get('owner') === chris, 'Shen has Chris as an owner'); + + // check that the relationship has been established + pets = chris + .get('pets') + .toArray() + .map(pet => pet.get('name')); + assert.deepEqual(pets, ['Shen'], 'Chris has Shen as a pet'); + }); + + test('We can add a new record to a record', async function(assert) { + let chris = store.createRecord('person', { + name: 'Chris', + pets: [], + }); + + let pet = store.push({ + data: { + id: '1', + type: 'pet', + attributes: { name: 'Shen' }, + relationships: { + owner: { + data: null, + }, + }, + }, + }); + + // check that we are properly configured + assert.ok(pet.get('owner') === null, 'Precondition: Our owner is null'); + + let pets = chris + .get('pets') + .toArray() + .map(pet => pet.get('name')); + assert.deepEqual(pets, [], 'Precondition: Chris has no pets'); + + pet.set('owner', chris); + + assert.ok(pet.get('owner') === chris, 'Shen has Chris as an owner'); + + // check that the relationship has been established + pets = chris + .get('pets') + .toArray() + .map(pet => pet.get('name')); + assert.deepEqual(pets, ['Shen'], 'Chris has Shen as a pet'); + }); + + test('We can add a new record to a new record', async function(assert) { + let chris = store.createRecord('person', { + name: 'Chris', + pets: [], + }); + + let pet = store.createRecord('pet', { + name: 'Shen', + owner: null, + }); + + // check that we are properly configured + assert.ok(pet.get('owner') === null, 'Precondition: Our owner is null'); + + let pets = chris + .get('pets') + .toArray() + .map(pet => pet.get('name')); + assert.deepEqual(pets, [], 'Precondition: Chris has no pets'); + + pet.set('owner', chris); + + assert.ok(pet.get('owner') === chris, 'Shen has Chris as an owner'); + + // check that the relationship has been established + pets = chris + .get('pets') + .toArray() + .map(pet => pet.get('name')); + assert.deepEqual(pets, ['Shen'], 'Chris has Shen as a pet'); + }); + + test('We can add to a new record', async function(assert) { + let chris = store.push({ + data: { + id: '1', + type: 'person', + attributes: { name: 'Chris' }, + relationships: { + pets: { + data: [], + }, + }, + }, + }); + + let pet = store.createRecord('pet', { + name: 'Shen', + owner: null, + }); + + // check that we are properly configured + assert.ok(pet.get('owner') === null, 'Precondition: Our owner is null'); + + let pets = chris + .get('pets') + .toArray() + .map(pet => pet.get('name')); + assert.deepEqual(pets, [], 'Precondition: Chris has no pets'); + + pet.set('owner', chris); + + assert.ok(pet.get('owner') === chris, 'Shen has Chris as an owner'); + + // check that the relationship has been established + pets = chris + .get('pets') + .toArray() + .map(pet => pet.get('name')); + assert.deepEqual(pets, ['Shen'], 'Chris has Shen as a pet'); + }); + }); + + module('Adding an async belongsTo relationship to a record', function() { + test('We can add to a record', async function(assert) { + let chris = store.push({ + data: { + id: '1', + type: 'person', + attributes: { name: 'Chris' }, + relationships: { + bestFriend: { + data: null, + }, + }, + }, + }); + + let james = store.push({ + data: { + id: '1', + type: 'person', + attributes: { name: 'James' }, + relationships: { + bestFriend: { + data: null, + }, + }, + }, + }); + + // check that we are properly configured + let chrisBestFriend = await chris.get('bestFriend'); + let jamesBestFriend = await james.get('bestFriend'); + + assert.ok(chrisBestFriend === null, 'Precondition: Chris has no best friend'); + assert.ok(jamesBestFriend === null, 'Precondition: James has no best friend'); + + chris.set('bestFriend', james); + + // check that the relationship has been established + chrisBestFriend = await chris.get('bestFriend'); + jamesBestFriend = await james.get('bestFriend'); + + assert.ok(chrisBestFriend === james, 'Chris has James as a best friend'); + assert.ok(jamesBestFriend === chris, 'James has Chris as a best friend'); + }); + + test('We can add a new record to a record', async function(assert) { + let chris = store.push({ + data: { + id: '1', + type: 'person', + attributes: { name: 'Chris' }, + relationships: { + bestFriend: { + data: null, + }, + }, + }, + }); + + let james = store.createRecord('person', { + name: 'James', + bestFriend: null, + }); + + // check that we are properly configured + let chrisBestFriend = await chris.get('bestFriend'); + let jamesBestFriend = await james.get('bestFriend'); + + assert.ok(chrisBestFriend === null, 'Precondition: Chris has no best friend'); + assert.ok(jamesBestFriend === null, 'Precondition: James has no best friend'); + + chris.set('bestFriend', james); + + // check that the relationship has been established + chrisBestFriend = await chris.get('bestFriend'); + jamesBestFriend = await james.get('bestFriend'); + + assert.ok(chrisBestFriend === james, 'Chris has James as a best friend'); + assert.ok(jamesBestFriend === chris, 'James has Chris as a best friend'); + }); + + test('We can add a new record to a new record', async function(assert) { + let chris = store.createRecord('person', { + name: 'Chris', + bestFriend: null, + }); + + let james = store.createRecord('person', { + name: 'James', + bestFriend: null, + }); + + // check that we are properly configured + let chrisBestFriend = await chris.get('bestFriend'); + let jamesBestFriend = await james.get('bestFriend'); + + assert.ok(chrisBestFriend === null, 'Precondition: Chris has no best friend'); + assert.ok(jamesBestFriend === null, 'Precondition: James has no best friend'); + + chris.set('bestFriend', james); + + // check that the relationship has been established + chrisBestFriend = await chris.get('bestFriend'); + jamesBestFriend = await james.get('bestFriend'); + + assert.ok(chrisBestFriend === james, 'Chris has James as a best friend'); + assert.ok(jamesBestFriend === chris, 'James has Chris as a best friend'); + }); + + test('We can add to a new record', async function(assert) { + let chris = store.createRecord('person', { + name: 'Chris', + bestFriend: null, + }); + + let james = store.push({ + data: { + id: '1', + type: 'person', + attributes: { name: 'James' }, + relationships: { + bestFriend: { + data: null, + }, + }, + }, + }); + + // check that we are properly configured + let chrisBestFriend = await chris.get('bestFriend'); + let jamesBestFriend = await james.get('bestFriend'); + + assert.ok(chrisBestFriend === null, 'Precondition: Chris has no best friend'); + assert.ok(jamesBestFriend === null, 'Precondition: James has no best friend'); + + chris.set('bestFriend', james); + + // check that the relationship has been established + chrisBestFriend = await chris.get('bestFriend'); + jamesBestFriend = await james.get('bestFriend'); + + assert.ok(chrisBestFriend === james, 'Chris has James as a best friend'); + assert.ok(jamesBestFriend === chris, 'James has Chris as a best friend'); + }); + }); + }); +}); diff --git a/tests/integration/records/load-test.js b/tests/integration/records/load-test.js index f7836cc2d33..9d4a4105054 100644 --- a/tests/integration/records/load-test.js +++ b/tests/integration/records/load-test.js @@ -1,39 +1,194 @@ -import { reject } from 'rsvp'; +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { reject, resolve } from 'rsvp'; +import Store from 'ember-data/store'; +import Model from 'ember-data/model'; +import JSONAPIAdapter from 'ember-data/adapters/json-api'; +import JSONAPISerializer from 'ember-data/serializers/json-api'; +import { attr, belongsTo } from '@ember-decorators/data'; import { run } from '@ember/runloop'; -import setupStore from 'dummy/tests/helpers/store'; -import { module, test } from 'qunit'; +class Person extends Model { + @attr name; + @belongsTo('person', { async: true, inverse: 'bestFriend' }) + bestFriend; +} -import DS from 'ember-data'; +module('integration/load - Loading Records', function(hooks) { + let store; + setupTest(hooks); -const { hasMany } = DS; + hooks.beforeEach(function() { + let { owner } = this; + owner.register('service:store', Store); + owner.register('model:person', Person); + store = owner.lookup('service:store'); + }); -let Post, Comment, env; + test('When loading a record fails, the record is not left behind', async function(assert) { + this.owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + findRecord() { + return reject(); + }, + }) + ); -module('integration/load - Loading Records', { - beforeEach() { - Post = DS.Model.extend({ - comments: hasMany({ async: true }), + await store.findRecord('person', '1').catch(() => { + assert.equal(store.hasRecordForId('person', '1'), false); }); + }); - Comment = DS.Model.extend(); + test('Empty records remain in the empty state while data is being fetched', async function(assert) { + let payloads = [ + { + data: { + type: 'person', + id: '1', + attributes: { name: 'Chris' }, + relationships: { + bestFriend: { + data: { type: 'person', id: '2' }, + }, + }, + }, + included: [ + { + type: 'person', + id: '2', + attributes: { name: 'Shen' }, + relationships: { + bestFriend: { + data: { type: 'person', id: '1' }, + }, + }, + }, + ], + }, + { + data: { + type: 'person', + id: '1', + attributes: { name: 'Chris' }, + relationships: { + bestFriend: { + data: { type: 'person', id: '2' }, + }, + }, + }, + }, + { + data: { + type: 'person', + id: '1', + attributes: { name: 'Chris' }, + relationships: { + bestFriend: { + data: { type: 'person', id: '2' }, + }, + }, + }, + }, + ]; - env = setupStore({ post: Post, comment: Comment }); - }, + this.owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + findRecord() { + let payload = payloads.shift(); - afterEach() { - run(env.container, 'destroy'); - }, -}); + if (payload === undefined) { + return reject(new Error('Invalid Request')); + } -test('When loading a record fails, the record is not left behind', function(assert) { - env.adapter.findRecord = function(store, type, id, snapshot) { - return reject(); - }; + return resolve(payload); + }, + }) + ); + this.owner.register( + 'serializer:application', + JSONAPISerializer.extend({ + normalizeResponse(_, __, data) { + return data; + }, + }) + ); - return run(() => { - return env.store.findRecord('post', 1).catch(() => { - assert.equal(env.store.hasRecordForId('post', 1), false); - }); + let internalModel = store._internalModelForId('person', '1'); + + // test that our initial state is correct + assert.equal(internalModel.isEmpty(), true, 'We begin in the empty state'); + assert.equal(internalModel.isLoading(), false, 'We have not triggered a load'); + assert.equal(internalModel.isReloading, false, 'We are not reloading'); + + let recordPromise = store.findRecord('person', '1'); + + // test that during the initial load our state is correct + assert.equal( + internalModel.isEmpty(), + true, + 'awaiting first fetch: We remain in the empty state' + ); + assert.equal( + internalModel.isLoading(), + true, + 'awaiting first fetch: We have now triggered a load' + ); + assert.equal(internalModel.isReloading, false, 'awaiting first fetch: We are not reloading'); + + let record = await recordPromise; + + // test that after the initial load our state is correct + assert.equal(internalModel.isEmpty(), false, 'after first fetch: We are no longer empty'); + assert.equal(internalModel.isLoading(), false, 'after first fetch: We have loaded'); + assert.equal(internalModel.isReloading, false, 'after first fetch: We are not reloading'); + + let bestFriend = await record.get('bestFriend'); + let trueBestFriend = await bestFriend.get('bestFriend'); + + // shen is our retainer for the record we are testing + // that ensures unloadRecord later in this test does not fully + // discard the internalModel + let shen = store.peekRecord('person', '2'); + + assert.ok(bestFriend === shen, 'Precond: bestFriend is correct'); + assert.ok(trueBestFriend === record, 'Precond: bestFriend of bestFriend is correct'); + + recordPromise = record.reload(); + + // test that during a reload our state is correct + assert.equal(internalModel.isEmpty(), false, 'awaiting reload: We remain non-empty'); + assert.equal(internalModel.isLoading(), false, 'awaiting reload: We are not loading again'); + assert.equal(internalModel.isReloading, true, 'awaiting reload: We are reloading'); + + await recordPromise; + + // test that after a reload our state is correct + assert.equal(internalModel.isEmpty(), false, 'after reload: We remain non-empty'); + assert.equal(internalModel.isLoading(), false, 'after reload: We have loaded'); + assert.equal(internalModel.isReloading, false, 'after reload:: We are not reloading'); + + run(() => record.unloadRecord()); + + // test that after an unload our state is correct + assert.equal(internalModel.isEmpty(), true, 'after unload: We are empty again'); + assert.equal(internalModel.isLoading(), false, 'after unload: We are not loading'); + assert.equal(internalModel.isReloading, false, 'after unload:: We are not reloading'); + + recordPromise = store.findRecord('person', '1'); + + // test that during a reload-due-to-unload our state is correct + // This requires a retainer (the async bestFriend relationship) + assert.equal(internalModel.isEmpty(), true, 'awaiting second find: We remain empty'); + assert.equal(internalModel.isLoading(), true, 'awaiting second find: We are loading again'); + assert.equal(internalModel.isReloading, false, 'awaiting second find: We are not reloading'); + + await recordPromise; + + // test that after the reload-due-to-unload our state is correct + assert.equal(internalModel.isEmpty(), false, 'after second find: We are no longer empty'); + assert.equal(internalModel.isLoading(), false, 'after second find: We have loaded'); + assert.equal(internalModel.isReloading, false, 'after second find: We are not reloading'); }); }); diff --git a/tests/integration/relationships/json-api-links-test.js b/tests/integration/relationships/json-api-links-test.js index 72110bf59ce..1655e7ff434 100644 --- a/tests/integration/relationships/json-api-links-test.js +++ b/tests/integration/relationships/json-api-links-test.js @@ -678,6 +678,7 @@ module('integration/relationship/json-api-links | Relationship fetching', { const Pet = Model.extend({ name: attr(), owner: belongsTo('user', { async: false, inverse: 'pets' }), + friends: hasMany('pet', { async: false, inverse: 'friends' }), }); const Adapter = JSONAPIAdapter.extend(); @@ -1965,3 +1966,92 @@ test('We should not fetch a hasMany relationship with links that we know is empt 'We improperly fetched the link for a previously fetched and found to be empty relationship'; run(() => user2.get('pets')); }); + +test('We should not fetch a sync hasMany relationship with a link that is missing the data member', function(assert) { + assert.expect(1); + let { store, adapter } = env; + + let petPayload = { + data: { + type: 'pet', + id: '1', + attributes: { + name: 'Shen', + }, + relationships: { + friends: { + links: { + related: './shen/friends', + }, + }, + }, + }, + }; + + adapter.shouldBackgroundReloadRecord = () => false; + adapter.findRecord = () => { + assert.ok(false, 'We should not call findRecord'); + }; + adapter.findMany = () => { + assert.ok(false, 'We should not call findMany'); + }; + adapter.findHasMany = () => { + assert.ok(false, 'We should not call findHasMany'); + }; + adapter.findBelongsTo = () => { + assert.ok(false, 'We should not call findBelongsTo'); + }; + + // setup users + let shen = run(() => store.push(petPayload)); + + // should not fire a request + run(() => shen.get('pets')); + + assert.ok(true, 'We reached the end of the test'); +}); + +test('We should not fetch a sync belongsTo relationship with a link that is missing the data member', function(assert) { + assert.expect(1); + let { store, adapter } = env; + + let petPayload = { + data: { + type: 'pet', + id: '1', + attributes: { + name: 'Shen', + }, + relationships: { + owner: { + links: { + related: './shen/owner', + self: './owner/a', + }, + }, + }, + }, + }; + + adapter.shouldBackgroundReloadRecord = () => false; + adapter.findRecord = () => { + assert.ok(false, 'We should not call findRecord'); + }; + adapter.findMany = () => { + assert.ok(false, 'We should not call findMany'); + }; + adapter.findHasMany = () => { + assert.ok(false, 'We should not call findHasMany'); + }; + adapter.findBelongsTo = () => { + assert.ok(false, 'We should not call findBelongsTo'); + }; + + // setup users + let shen = run(() => store.push(petPayload)); + + // should not fire a request + run(() => shen.get('owner')); + + assert.ok(true, 'We reached the end of the test'); +}); diff --git a/tests/unit/store/push-test.js b/tests/unit/store/push-test.js index dfcd91a2340..ba547923e51 100644 --- a/tests/unit/store/push-test.js +++ b/tests/unit/store/push-test.js @@ -577,7 +577,7 @@ testInDebug( }, }); }); - }, /You pushed a record of type 'person' with a relationship 'phoneNumbers' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload./); + }, /You pushed a record of type 'person' with a relationship 'phoneNumbers' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty./); } );