Skip to content

Commit

Permalink
Add tests for @cached decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli committed Oct 5, 2021
1 parent 3b937dd commit 427fe2a
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 6 deletions.
11 changes: 8 additions & 3 deletions packages/@glimmer/tracking/src/cached.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,18 @@ export const cached: PropertyDecorator = (...args: any[]) => {
) {
throwCachedInvalidArgsError(args);
}
if (DEBUG && 'get' in descriptor && typeof descriptor.get !== 'function')
if (DEBUG && (!('get' in descriptor) || typeof descriptor.get !== 'function')) {
throwCachedGetterOnlyError(key);
}

const caches = new WeakMap();
const getter = descriptor.get;

descriptor.get = function (): unknown {
if (!caches.has(this)) caches.set(this, createCache(getter.bind(this)));
if (!caches.has(this)) {
caches.set(this, createCache(getter.bind(this)));
}

return getValue(caches.get(this));
};
};
Expand All @@ -94,7 +99,7 @@ function throwCachedGetterOnlyError(key: string): never {
throw new Error(`The @cached decorator must be applied to getters. '${key}' is not a getter.`);
}

function throwCachedInvalidArgsError(args: unknown[]): never {
function throwCachedInvalidArgsError(args: unknown[] = []): never {
throw new Error(
`You attempted to use @cached on with ${
args.length > 1 ? 'arguments' : 'an argument'
Expand Down
132 changes: 132 additions & 0 deletions packages/@glimmer/tracking/test/cached-decorator-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
const { test } = QUnit;

import { DEBUG } from '@glimmer/env';
import { tracked, cached } from '@glimmer/tracking';

import * as TSFixtures from './fixtures/typescript';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as BabelFixtures from './fixtures/babel';

QUnit.module('[@glimmer/tracking] @cached Decorators');

test('it works', function (assert) {
class Person {
@tracked firstName = 'Jen';
@tracked lastName = 'Weber';

@cached
get fullName() {
const fullName = `${this.firstName} ${this.lastName}`;
assert.step(fullName);
return fullName;
}
}

const person = new Person();
assert.verifySteps([], 'getter is not called after class initialization');

assert.strictEqual(person.fullName, 'Jen Weber');
assert.verifySteps(['Jen Weber'], 'getter was called after property access');

assert.strictEqual(person.fullName, 'Jen Weber');
assert.verifySteps([], 'getter was not called again after repeated property access');

person.firstName = 'Kenneth';
assert.verifySteps([], 'changing a property does not trigger an eager re-computation');

assert.strictEqual(person.fullName, 'Kenneth Weber');
assert.verifySteps(['Kenneth Weber'], 'accessing the property triggers a re-computation');

assert.strictEqual(person.fullName, 'Kenneth Weber');
assert.verifySteps([], 'getter was not called again after repeated property access');

person.lastName = 'Larsen';
assert.verifySteps([], 'changing a property does not trigger an eager re-computation');

assert.strictEqual(person.fullName, 'Kenneth Larsen');
assert.verifySteps(['Kenneth Larsen'], 'accessing the property triggers a re-computation');
});

// https://github.com/ember-polyfills/ember-cached-decorator-polyfill/issues/7
test('it has a separate cache per class instance', function (assert) {
class Person {
@tracked firstName: string;
@tracked lastName: string;

constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}

@cached
get fullName() {
const fullName = `${this.firstName} ${this.lastName}`;
assert.step(fullName);
return fullName;
}
}

const jen = new Person('Jen', 'Weber');
const chris = new Person('Chris', 'Garrett');

assert.verifySteps([], 'getter is not called after class initialization');

assert.strictEqual(jen.fullName, 'Jen Weber');
assert.verifySteps(['Jen Weber'], 'getter was called after property access');

assert.strictEqual(jen.fullName, 'Jen Weber');
assert.verifySteps([], 'getter was not called again after repeated property access');

assert.strictEqual(chris.fullName, 'Chris Garrett', 'other instance has a different value');
assert.verifySteps(['Chris Garrett'], 'getter was called after property access');

assert.strictEqual(chris.fullName, 'Chris Garrett');
assert.verifySteps([], 'getter was not called again after repeated property access');

chris.lastName = 'Manson';
assert.verifySteps([], 'changing a property does not trigger an eager re-computation');

assert.strictEqual(jen.fullName, 'Jen Weber', 'other instance is unaffected');
assert.verifySteps([], 'getter was not called again after repeated property access');

assert.strictEqual(chris.fullName, 'Chris Manson');
assert.verifySteps(['Chris Manson'], 'getter was called after property access');

assert.strictEqual(jen.fullName, 'Jen Weber', 'other instance is unaffected');
assert.verifySteps([], 'getter was not called again after repeated property access');
});

[
['Babel', BabelFixtures],
['TypeScript', TSFixtures],
].forEach(([compiler, F]) => {
QUnit.module(`[@glimmer/tracking] Cached Property Decorators with ${compiler}`);

if (DEBUG) {
test('Cached decorator on a property throws an error', (assert) => {
assert.throws(F.createClassWithCachedProperty);
});

test('Cached decorator with a setter throws an error', (assert) => {
assert.throws(F.createClassWithCachedSetter);
});

test('Cached decorator with arguments throws an error', function (assert) {
assert.throws(
F.createClassWithCachedDependentKeys,
/@cached\('firstName', 'lastName'\)/,
'the correct error is thrown'
);
});

test('Using @cached as a decorator factory throws an error', function (assert) {
assert.throws(
F.createClassWithCachedAsDecoratorFactory,
/@cached\(\)/,
'The correct error is thrown'
);
});
}
});
52 changes: 50 additions & 2 deletions packages/@glimmer/tracking/test/fixtures/babel.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { tracked } from '@glimmer/tracking';
import {cached, tracked} from '@glimmer/tracking';

export class Tom {
@tracked firstName = 'Tom';
Expand All @@ -16,7 +16,7 @@ class FrozenToran {

Object.freeze(FrozenToran);

export { FrozenToran };
export {FrozenToran};

export class PersonWithCount {
@tracked _firstName = 'Tom';
Expand Down Expand Up @@ -100,6 +100,14 @@ export function createClassWithTrackedGetter() {
return new PersonWithTrackedGetter();
}

export function createClassWithCachedProperty() {
class PersonWithCachedProperty {
@cached firstName = 'Tom';
}

return new PersonWithCachedProperty();
}

export function createClassWithTrackedSetter() {
class PersonWithTrackedSetter {
@tracked firstName = 'Tom';
Expand All @@ -115,6 +123,22 @@ export function createClassWithTrackedSetter() {
return new PersonWithTrackedSetter();
}

export function createClassWithCachedSetter() {
class PersonWithCachedSetter {
@tracked firstName = 'Tom';
@tracked lastName;

@cached set fullName(fullName) {
const [firstName, lastName] = fullName.split(' ');
this.firstName = firstName;
this.lastName = lastName;
}
}

return new PersonWithCachedSetter();
}


export function createClassWithTrackedDependentKeys() {
class DependentKeysAreCool {
@tracked('firstName', 'lastName') fullName() {
Expand All @@ -127,6 +151,18 @@ export function createClassWithTrackedDependentKeys() {
return new DependentKeysAreCool();
}

export function createClassWithCachedDependentKeys() {
class DependentKeysAreCool {
@cached('firstName', 'lastName') fullName() {
return `${this.firstName} ${this.lastName}`;
}

@tracked firstName = 'Tom';
@tracked lastName = 'Dale';
}
return new DependentKeysAreCool();
}

export function createClassWithTrackedAsDecoratorFactory() {
class DependentKeysAreCool {
@tracked() fullName() {
Expand All @@ -138,3 +174,15 @@ export function createClassWithTrackedAsDecoratorFactory() {
}
return new DependentKeysAreCool();
}

export function createClassWithCachedAsDecoratorFactory() {
class DependentKeysAreCool {
@cached() fullName() {
return `${this.firstName} ${this.lastName}`;
}

@tracked firstName = 'Tom';
@tracked lastName = 'Dale';
}
return new DependentKeysAreCool();
}
52 changes: 51 additions & 1 deletion packages/@glimmer/tracking/test/fixtures/typescript.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { tracked } from '@glimmer/tracking';
import { cached, tracked } from '@glimmer/tracking';

export class Tom {
@tracked firstName = 'Tom';
Expand Down Expand Up @@ -100,6 +100,14 @@ export function createClassWithTrackedGetter(): any {
return new PersonWithTrackedGetter();
}

export function createClassWithCachedProperty(): any {
class PersonWithCachedProperty {
@cached firstName = 'Tom';
}

return new PersonWithCachedProperty();
}

export function createClassWithTrackedSetter(): any {
class PersonWithTrackedSetter {
@tracked firstName = 'Tom';
Expand All @@ -116,6 +124,22 @@ export function createClassWithTrackedSetter(): any {
return new PersonWithTrackedSetter();
}

export function createClassWithCachedSetter(): any {
class PersonWithCachedSetter {
@tracked firstName = 'Tom';
@tracked lastName: any;

// @ts-ignore
@cached set fullName(fullName) {
const [firstName, lastName] = fullName.split(' ');
this.firstName = firstName;
this.lastName = lastName;
}
}

return new PersonWithCachedSetter();
}

export function createClassWithTrackedDependentKeys(): any {
class DependentKeysAreCool {
// @ts-ignore
Expand All @@ -129,6 +153,19 @@ export function createClassWithTrackedDependentKeys(): any {
return new DependentKeysAreCool();
}

export function createClassWithCachedDependentKeys(): any {
class DependentKeysAreCool {
// @ts-ignore
@cached('firstName', 'lastName') fullName() {
return `${this.firstName} ${this.lastName}`;
}

@tracked firstName = 'Tom';
@tracked lastName = 'Dale';
}
return new DependentKeysAreCool();
}

export function createClassWithTrackedAsDecoratorFactory(): any {
class DependentKeysAreCool {
// @ts-ignore
Expand All @@ -141,3 +178,16 @@ export function createClassWithTrackedAsDecoratorFactory(): any {
}
return new DependentKeysAreCool();
}

export function createClassWithCachedAsDecoratorFactory(): any {
class DependentKeysAreCool {
// @ts-ignore
@cached() fullName() {
return `${this.firstName} ${this.lastName}`;
}

@tracked firstName = 'Tom';
@tracked lastName = 'Dale';
}
return new DependentKeysAreCool();
}
1 change: 1 addition & 0 deletions packages/@glimmer/tracking/test/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import './tracked-decorator-test';
import './cached-decorator-test';

0 comments on commit 427fe2a

Please sign in to comment.