From 277ba7fdc76b3d096af9a4438559a820af9b2442 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 1 Oct 2023 19:19:15 -0700 Subject: [PATCH] feat(private): reactive simple fields (#8948) --- packages/schema-record/src/record.ts | 20 +- packages/tracking/package.json | 3 +- packages/tracking/rollup.config.mjs | 2 +- packages/tracking/src/-private.ts | 81 ++++++- packages/tracking/src/index.ts | 2 +- pnpm-lock.yaml | 4 + .../tests/-utils/reactive-context.ts | 86 +++++++ .../tests/reactivity/basic-fields-test.ts | 222 ++++++++++++++++++ .../tests/reactivity/derivation-test.ts | 65 +++++ .../tests/reactivity/resource-test.ts | 87 +++++++ tests/schema-record/tests/test-helper.js | 4 + 11 files changed, 561 insertions(+), 15 deletions(-) create mode 100644 tests/schema-record/tests/-utils/reactive-context.ts create mode 100644 tests/schema-record/tests/reactivity/basic-fields-test.ts create mode 100644 tests/schema-record/tests/reactivity/derivation-test.ts create mode 100644 tests/schema-record/tests/reactivity/resource-test.ts diff --git a/packages/schema-record/src/record.ts b/packages/schema-record/src/record.ts index c1e69eab0e5..57f72f62c64 100644 --- a/packages/schema-record/src/record.ts +++ b/packages/schema-record/src/record.ts @@ -2,12 +2,13 @@ import type Store from '@ember-data/store'; import type { StableRecordIdentifier } from "@ember-data/types/q/identifier"; import type { FieldSchema, SchemaService } from './schema'; import { Cache } from '@ember-data/types/q/cache'; -import { Document } from '@ember-data/store/-private/document'; import { tracked } from '@glimmer/tracking'; import { Link, Links, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api'; import { StoreRequestInput } from '@ember-data/store/-private/cache-handler'; import { Future } from '@ember-data/request'; import { DEBUG } from '@ember-data/env'; +import { NotificationType } from '@ember-data/store/-private/managers/notification-manager'; +import { addToTransaction, entangleSignal } from '@ember-data/tracking/-private'; export const Destroy = Symbol('Destroy'); export const RecordStore = Symbol('Store'); @@ -110,6 +111,7 @@ export class SchemaRecord { declare [RecordStore]: Store; declare [Identifier]: StableRecordIdentifier; declare [Editable]: boolean; + declare ___notifications: unknown; constructor(store: Store, identifier: StableRecordIdentifier, editable: boolean) { this[RecordStore] = store; @@ -120,6 +122,20 @@ export class SchemaRecord { const cache = store.cache; const fields = schema.fields(identifier); + const signals = new Map(); + this.___notifications = store.notifications.subscribe(identifier, (_: StableRecordIdentifier, type: NotificationType, key?: string) => { + switch (type) { + case 'attributes': + if (key) { + const signal = signals.get(key); + if (signal) { + addToTransaction(signal); + } + } + break; + } + }); + return new Proxy(this, { get(target, prop, receiver) { if (prop === Destroy) { @@ -140,8 +156,10 @@ export class SchemaRecord { switch (field.kind) { case 'attribute': + entangleSignal(signals, this, field.name); return computeAttribute(schema, cache, target, identifier, field, prop as string); case 'resource': + entangleSignal(signals, this, field.name); return computeResource(store, cache, target, identifier, field, prop as string); case 'derived': diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 62243f466cb..147e5307e2d 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -29,7 +29,8 @@ "dependencies": { "@ember-data/private-build-infra": "workspace:5.5.0-alpha.10", "@embroider/macros": "^1.13.1", - "ember-cli-babel": "^8.1.0" + "ember-cli-babel": "^8.1.0", + "@glimmer/validator": "^0.84.3" }, "files": [ "addon-main.js", diff --git a/packages/tracking/rollup.config.mjs b/packages/tracking/rollup.config.mjs index 34fe8a317de..9fecfb83ff7 100644 --- a/packages/tracking/rollup.config.mjs +++ b/packages/tracking/rollup.config.mjs @@ -12,7 +12,7 @@ export default { // You can augment this if you need to. output: addon.output(), - external: ['@embroider/macros'], + external: ['@embroider/macros', '@glimmer/validator'], plugins: [ // These are the modules that users should be able to import from your diff --git a/packages/tracking/src/-private.ts b/packages/tracking/src/-private.ts index a1bedd43ad1..a8b84ae14aa 100644 --- a/packages/tracking/src/-private.ts +++ b/packages/tracking/src/-private.ts @@ -1,3 +1,6 @@ +import { tagForProperty } from '@ember/-internals/metal'; +import { consumeTag, dirtyTag } from '@glimmer/validator'; + import { DEBUG } from '@ember-data/env'; /** @@ -18,8 +21,8 @@ type OpaqueFn = (...args: unknown[]) => unknown; type Tag = { ref: null; t: boolean }; type Transaction = { cbs: Set; - props: Set; - sub: Set; + props: Set; + sub: Set; parent: Transaction | null; }; let TRANSACTION: Transaction | null = null; @@ -37,18 +40,26 @@ function createTransaction() { TRANSACTION = transaction; } -export function subscribe(obj: Tag): void { +export function subscribe(obj: Tag | Signal): void { if (TRANSACTION) { TRANSACTION.sub.add(obj); + } else if ('tag' in obj) { + // @ts-expect-error - we are using Ember's Tag not Glimmer's + consumeTag(obj.tag); } else { obj.ref; } } -function updateRef(obj: Tag): void { +function updateRef(obj: Tag | Signal): void { if (DEBUG) { try { - obj.ref = null; + if ('tag' in obj) { + // @ts-expect-error - we are using Ember's Tag not Glimmer's + dirtyTag(obj.tag); + } else { + obj.ref = null; + } } catch (e: unknown) { if (e instanceof Error) { if (e.message.includes('You attempted to update `ref` on `Tag`')) { @@ -97,7 +108,12 @@ function updateRef(obj: Tag): void { throw e; } } else { - obj.ref = null; + if ('tag' in obj) { + // @ts-expect-error - we are using Ember's Tag not Glimmer's + dirtyTag(obj.tag); + } else { + obj.ref = null; + } } } @@ -107,13 +123,18 @@ function flushTransaction() { transaction.cbs.forEach((cb) => { cb(); }); - transaction.props.forEach((obj: Tag) => { + transaction.props.forEach((obj) => { // mark this mutation as part of a transaction obj.t = true; updateRef(obj); }); - transaction.sub.forEach((obj: Tag) => { - obj.ref; + transaction.sub.forEach((obj) => { + if ('tag' in obj) { + // @ts-expect-error - we are using Ember's Tag not Glimmer's + consumeTag(obj.tag); + } else { + obj.ref; + } }); } async function untrack() { @@ -125,14 +146,14 @@ async function untrack() { transaction.cbs.forEach((cb) => { cb(); }); - transaction.props.forEach((obj: Tag) => { + transaction.props.forEach((obj) => { // mark this mutation as part of a transaction obj.t = true; updateRef(obj); }); } -export function addToTransaction(obj: Tag): void { +export function addToTransaction(obj: Tag | Signal): void { if (TRANSACTION) { TRANSACTION.props.add(obj); } else { @@ -213,3 +234,41 @@ export function memoTransact(method: T): (...args: unknown[] return ret as ReturnType; }; } + +interface Signal { + _debug_base?: string; + _debug_prop?: string; + + t: boolean; + shouldReset: boolean; + tag: ReturnType; +} + +export function createSignal(obj: T, key: K): Signal { + const _signal: Signal = { + tag: tagForProperty(obj, key), + t: false, + shouldReset: false, + }; + + if (DEBUG) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-base-to-string + _signal._debug_base = obj.constructor?.name ?? obj.toString?.() ?? 'unknown'; + _signal._debug_prop = key; + } + + return _signal; +} + +export function entangleSignal( + signals: Map, + obj: T, + key: K +): void { + let signal = signals.get(key); + if (!signal) { + signal = createSignal(obj, key); + signals.set(key, signal); + } + subscribe(signal); +} diff --git a/packages/tracking/src/index.ts b/packages/tracking/src/index.ts index 414a60fccd3..ec6db514704 100644 --- a/packages/tracking/src/index.ts +++ b/packages/tracking/src/index.ts @@ -1 +1 @@ -export { transact, memoTransact, untracked } from './-private'; +export { transact, memoTransact, untracked, entangleSignal } from './-private'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77c7d5c28b8..a2c298fc1e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1355,6 +1355,9 @@ importers: '@embroider/macros': specifier: ^1.12.2 version: 1.13.1(@babel/core@7.23.0) + '@glimmer/validator': + specifier: ^0.84.3 + version: 0.84.3 ember-cli-babel: specifier: ^8.1.0 version: 8.1.0(@babel/core@7.23.0) @@ -17999,6 +18002,7 @@ packages: dependencies: '@ember-data/private-build-infra': file:packages/private-build-infra '@embroider/macros': 1.13.1(@babel/core@7.23.0) + '@glimmer/validator': 0.84.3 ember-cli-babel: 8.1.0(@babel/core@7.23.0) transitivePeerDependencies: - '@babel/core' diff --git a/tests/schema-record/tests/-utils/reactive-context.ts b/tests/schema-record/tests/-utils/reactive-context.ts new file mode 100644 index 00000000000..6afdaf25e11 --- /dev/null +++ b/tests/schema-record/tests/-utils/reactive-context.ts @@ -0,0 +1,86 @@ +import { render, TestContext } from '@ember/test-helpers'; +import Component from '@glimmer/component'; + +import type { FieldSchema } from '@warp-drive/schema-record/schema'; + +import { hbs } from 'ember-cli-htmlbars'; + +import type { ResourceRelationship } from '@ember-data/types/cache/relationship'; + +export async function reactiveContext(this: TestContext, record: T, fields: FieldSchema[]) { + const _fields: string[] = ['idCount', 'id', '$typeCount', '$type']; + fields.forEach((field) => { + _fields.push(field.name + 'Count'); + _fields.push(field.name); + }); + + class ReactiveComponent extends Component { + get __allFields() { + return _fields; + } + } + + const counters: Record = {}; + counters['id'] = 0; + counters['$type'] = 0; + + Object.defineProperty(ReactiveComponent.prototype, 'idCount', { + get() { + return counters['id']; + }, + }); + Object.defineProperty(ReactiveComponent.prototype, '$typeCount', { + get() { + return counters['$type']; + }, + }); + + Object.defineProperty(ReactiveComponent.prototype, 'id', { + get() { + counters['id']++; + return record['id'] as unknown; + }, + }); + Object.defineProperty(ReactiveComponent.prototype, '$type', { + get() { + counters['$type']++; + return record['$type'] as unknown; + }, + }); + + fields.forEach((field) => { + counters[field.name] = 0; + Object.defineProperty(ReactiveComponent.prototype, field.name + 'Count', { + get() { + return counters[field.name]; + }, + }); + Object.defineProperty(ReactiveComponent.prototype, field.name, { + get() { + counters[field.name]++; + + if (field.kind === 'attribute' || field.kind === 'derived') { + return record[field.name] as unknown; + } else if (field.kind === 'resource') { + return (record[field.name] as ResourceRelationship).data?.id; + } + }, + }); + }); + + this.owner.register('component:reactive-component', ReactiveComponent); + this.owner.register( + 'template:components/reactive-component', + hbs`
    {{#each this.__allFields as |prop|}}
  • {{prop}}: {{get this prop}}
  • {{/each}}
` + ); + + await render(hbs``); + + function reset() { + fields.forEach((field) => { + counters[field.name] = 0; + }); + } + + return { counters, reset, fieldOrder: _fields }; +} diff --git a/tests/schema-record/tests/reactivity/basic-fields-test.ts b/tests/schema-record/tests/reactivity/basic-fields-test.ts new file mode 100644 index 00000000000..3c5e37367e3 --- /dev/null +++ b/tests/schema-record/tests/reactivity/basic-fields-test.ts @@ -0,0 +1,222 @@ +import { rerender } from '@ember/test-helpers'; + +import type { SchemaRecord } from '@warp-drive/schema-record/record'; +import type { FieldSchema, Transform } from '@warp-drive/schema-record/schema'; +import { SchemaService } from '@warp-drive/schema-record/schema'; +import { module, test } from 'qunit'; + +import { setupRenderingTest } from 'ember-qunit'; + +import type Store from '@ember-data/store'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; + +import { reactiveContext } from '../-utils/reactive-context'; + +interface User { + id: string | null; + $type: 'user'; + name: string; + age: number; + netWorth: number; + coolometer: number; + rank: number; +} + +module('Reactivity | basic fields can receive remote updates', function (hooks) { + setupRenderingTest(hooks); + + test('we can use simple fields with no `type`', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const schema = new SchemaService(); + store.registerSchema(schema); + + schema.defineSchema('user', [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ]); + const fieldsMap = schema.schemas.get('user')!.fields; + const fields: FieldSchema[] = [...fieldsMap.values()]; + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { name: 'Rey Pupatine' }, + }, + }) as User; + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Rey Pupatine', 'name is accessible'); + + const { counters, fieldOrder } = await reactiveContext.call(this, record, fields); + const nameIndex = fieldOrder.indexOf('name'); + + assert.strictEqual(counters.id, 1, 'idCount is 1'); + assert.strictEqual(counters.$type, 1, '$typeCount is 1'); + assert.strictEqual(counters.name, 1, 'nameCount is 1'); + + assert.dom(`li:nth-child(${nameIndex + 1})`).hasText('name: Rey Pupatine', 'name is rendered'); + + // remote update + store.push({ + data: { + type: 'user', + id: '1', + attributes: { name: 'Rey Skybarker' }, + }, + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Rey Skybarker', 'name is accessible'); + + await rerender(); + + assert.strictEqual(counters.id, 1, 'idCount is 1'); + assert.strictEqual(counters.$type, 1, '$typeCount is 1'); + assert.strictEqual(counters.name, 2, 'nameCount is 1'); + + assert.dom(`li:nth-child(${nameIndex + 1})`).hasText('name: Rey Skybarker', 'name is rendered'); + }); + + test('we can use simple fields with a `type`', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const schema = new SchemaService(); + store.registerSchema(schema); + + const FloatTransform: Transform = { + serialize(value: string | number, options: { precision?: number } | null, _record: SchemaRecord): string { + return typeof value === 'number' + ? value.toFixed(options?.precision ?? 3) + : Number(value).toFixed(options?.precision ?? 3); + }, + hydrate(value: string, _options: { precision?: number } | null, _record: SchemaRecord): number { + if (value === undefined || value === null) { + return 0; + } + return Number(value); + }, + defaultValue(_options: { precision?: number } | null, _identifier: StableRecordIdentifier): string { + const v = 0; + return v.toFixed(_options?.precision ?? 3); + }, + }; + + schema.registerTransform('float', FloatTransform); + + schema.defineSchema('user', [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + { + name: 'rank', + type: 'float', + kind: 'attribute', + options: { precision: 0 }, + }, + { + name: 'age', + type: 'float', + options: { precision: 0 }, + kind: 'attribute', + }, + { + name: 'netWorth', + type: 'float', + options: { precision: 2 }, + kind: 'attribute', + }, + { + name: 'coolometer', + type: 'float', + kind: 'attribute', + }, + ]); + + const fieldsMap = schema.schemas.get('user')!.fields; + const fields: FieldSchema[] = [...fieldsMap.values()]; + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Rey Pupatine', + age: '3', + netWorth: '1000000.01', + coolometer: '100.000', + }, + }, + }) as User; + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Rey Pupatine', 'name is accessible'); + assert.strictEqual(record.age, 3, 'age is accessible'); + assert.strictEqual(record.netWorth, 1_000_000.01, 'netWorth is accessible'); + assert.strictEqual(record.coolometer, 100, 'coolometer is accessible'); + assert.strictEqual(record.rank, 0, 'rank is accessible'); + + const { counters, fieldOrder } = await reactiveContext.call(this, record, fields); + const nameIndex = fieldOrder.indexOf('name'); + + assert.strictEqual(counters.id, 1, 'idCount is 1'); + assert.strictEqual(counters.$type, 1, '$typeCount is 1'); + assert.strictEqual(counters.name, 1, 'nameCount is 1'); + assert.strictEqual(counters.age, 1, 'ageCount is 1'); + assert.strictEqual(counters.netWorth, 1, 'netWorthCount is 1'); + assert.strictEqual(counters.coolometer, 1, 'coolometerCount is 1'); + assert.strictEqual(counters.rank, 1, 'rankCount is 1'); + + assert.dom(`li:nth-child(${nameIndex + 1})`).hasText('name: Rey Pupatine', 'name is rendered'); + assert.dom(`li:nth-child(${nameIndex + 3})`).hasText('rank: 0', 'rank is rendered'); + assert.dom(`li:nth-child(${nameIndex + 5})`).hasText('age: 3', 'age is rendered'); + assert.dom(`li:nth-child(${nameIndex + 7})`).hasText('netWorth: 1000000.01', 'netWorth is rendered'); + assert.dom(`li:nth-child(${nameIndex + 9})`).hasText('coolometer: 100', 'coolometer is rendered'); + + // remote update + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Rey Skybarker', + age: '4', + netWorth: '1000000.01', + coolometer: '100.001', + rank: '10', + }, + }, + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Rey Skybarker', 'name is accessible'); + assert.strictEqual(record.age, 4, 'age is accessible'); + assert.strictEqual(record.netWorth, 1_000_000.01, 'netWorth is accessible'); + assert.strictEqual(record.coolometer, 100.001, 'coolometer is accessible'); + assert.strictEqual(record.rank, 10, 'rank is accessible'); + + await rerender(); + + assert.strictEqual(counters.id, 1, 'idCount is 1'); + assert.strictEqual(counters.$type, 1, '$typeCount is 1'); + assert.strictEqual(counters.name, 2, 'nameCount is 2'); + assert.strictEqual(counters.age, 2, 'ageCount is 2'); + assert.strictEqual(counters.netWorth, 1, 'netWorthCount is 1'); + assert.strictEqual(counters.coolometer, 2, 'coolometerCount is 2'); + assert.strictEqual(counters.rank, 2, 'rankCount is 2'); + + assert.dom(`li:nth-child(${nameIndex + 1})`).hasText('name: Rey Skybarker', 'name is rendered'); + assert.dom(`li:nth-child(${nameIndex + 3})`).hasText('rank: 10', 'rank is rendered'); + assert.dom(`li:nth-child(${nameIndex + 5})`).hasText('age: 4', 'age is rendered'); + assert.dom(`li:nth-child(${nameIndex + 7})`).hasText('netWorth: 1000000.01', 'netWorth is rendered'); + assert.dom(`li:nth-child(${nameIndex + 9})`).hasText('coolometer: 100.001', 'coolometer is rendered'); + }); +}); diff --git a/tests/schema-record/tests/reactivity/derivation-test.ts b/tests/schema-record/tests/reactivity/derivation-test.ts new file mode 100644 index 00000000000..21a17c6e77e --- /dev/null +++ b/tests/schema-record/tests/reactivity/derivation-test.ts @@ -0,0 +1,65 @@ +import { SchemaRecord } from '@warp-drive/schema-record/record'; +import { SchemaService } from '@warp-drive/schema-record/schema'; +import { module, skip as test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type Store from '@ember-data/store'; + +interface User { + id: string | null; + $type: 'user'; + firstName: string; + lastName: string; + readonly fullName: string; +} + +module('Reactivity | derivation', function (hooks) { + setupTest(hooks); + + test('we can use simple fields with no `type`', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const schema = new SchemaService(); + store.registerSchema(schema); + + function concat( + record: SchemaRecord & { [key: string]: unknown }, + options: Record | null, + _prop: string + ): string { + if (!options) throw new Error(`options is required`); + const opts = options as { fields: string[]; separator?: string }; + return opts.fields.map((field) => record[field]).join(opts.separator ?? ''); + } + + schema.registerDerivation('concat', concat); + + schema.defineSchema('user', [ + { + name: 'firstName', + type: null, + kind: 'attribute', + }, + { + name: 'lastName', + type: null, + kind: 'attribute', + }, + { + name: 'fullName', + type: 'concat', + options: { fields: ['firstName', 'lastName'], separator: ' ' }, + kind: 'derived', + }, + ]); + + const record = store.createRecord('user', { firstName: 'Rey', lastName: 'Skybarker' }) as User; + + assert.strictEqual(record.id, null, 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + + assert.strictEqual(record.firstName, 'Rey', 'firstName is accessible'); + assert.strictEqual(record.lastName, 'Skybarker', 'lastName is accessible'); + assert.strictEqual(record.fullName, 'Rey Skybarker', 'fullName is accessible'); + }); +}); diff --git a/tests/schema-record/tests/reactivity/resource-test.ts b/tests/schema-record/tests/reactivity/resource-test.ts new file mode 100644 index 00000000000..9f5307c5c36 --- /dev/null +++ b/tests/schema-record/tests/reactivity/resource-test.ts @@ -0,0 +1,87 @@ +import { SchemaRecord } from '@warp-drive/schema-record/record'; +import { SchemaService } from '@warp-drive/schema-record/schema'; +import { module, skip as test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type Store from '@ember-data/store'; +import { Document } from '@ember-data/store/-private/document'; + +interface User { + id: string | null; + $type: 'user'; + name: string; + bestFriend: Document; +} + +module('Reactivity | resource', function (hooks) { + setupTest(hooks); + + test('we can use simple fields with no `type`', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const schema = new SchemaService(); + store.registerSchema(schema); + + function concat( + record: SchemaRecord & { [key: string]: unknown }, + options: Record | null, + _prop: string + ): string { + if (!options) throw new Error(`options is required`); + const opts = options as { fields: string[]; separator?: string }; + return opts.fields.map((field) => record[field]).join(opts.separator ?? ''); + } + + schema.registerDerivation('concat', concat); + + schema.defineSchema('user', [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + { + name: 'bestFriend', + type: 'user', + kind: 'resource', + options: { inverse: 'bestFriend', async: true }, + }, + ]); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '2' }, + }, + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Rey', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '1' }, + }, + }, + }, + ], + }) as User; + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Chris', 'name is accessible'); + assert.strictEqual(record.bestFriend.data?.id, '2', 'bestFriend.id is accessible'); + assert.strictEqual(record.bestFriend.data?.$type, 'user', 'bestFriend.user is accessible'); + assert.strictEqual(record.bestFriend.data?.name, 'Rey', 'bestFriend.name is accessible'); + }); +}); diff --git a/tests/schema-record/tests/test-helper.js b/tests/schema-record/tests/test-helper.js index bf70c2fce19..cd953cf4d1d 100644 --- a/tests/schema-record/tests/test-helper.js +++ b/tests/schema-record/tests/test-helper.js @@ -1,6 +1,7 @@ import { setApplication } from '@ember/test-helpers'; import * as QUnit from 'qunit'; +import { setup } from 'qunit-dom'; import { start } from 'ember-qunit'; @@ -16,6 +17,9 @@ if (QUnit.urlParams.enableoptionalfeatures) { window.EmberDataENV = { ENABLE_OPTIONAL_FEATURES: true }; } +QUnit.dump.maxDepth = 6; +setup(QUnit.assert); + configureAsserts(); setApplication(Application.create(config.APP));