Skip to content
This repository was archived by the owner on Oct 29, 2024. It is now read-only.

Commit 896b3b5

Browse files
committed
Add tests for @cached decorator
1 parent e3be65f commit 896b3b5

File tree

5 files changed

+242
-6
lines changed

5 files changed

+242
-6
lines changed

packages/@glimmer/tracking/src/cached.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,18 @@ export const cached: PropertyDecorator = (...args: any[]) => {
7373
) {
7474
throwCachedInvalidArgsError(args);
7575
}
76-
if (DEBUG && 'get' in descriptor && typeof descriptor.get !== 'function')
76+
if (DEBUG && (!('get' in descriptor) || typeof descriptor.get !== 'function')) {
7777
throwCachedGetterOnlyError(key);
78+
}
7879

7980
const caches = new WeakMap();
8081
const getter = descriptor.get;
82+
8183
descriptor.get = function (): unknown {
82-
if (!caches.has(this)) caches.set(this, createCache(getter.bind(this)));
84+
if (!caches.has(this)) {
85+
caches.set(this, createCache(getter.bind(this)));
86+
}
87+
8388
return getValue(caches.get(this));
8489
};
8590
};
@@ -94,7 +99,7 @@ function throwCachedGetterOnlyError(key: string): never {
9499
throw new Error(`The @cached decorator must be applied to getters. '${key}' is not a getter.`);
95100
}
96101

97-
function throwCachedInvalidArgsError(args: unknown[]): never {
102+
function throwCachedInvalidArgsError(args: unknown[] = []): never {
98103
throw new Error(
99104
`You attempted to use @cached on with ${
100105
args.length > 1 ? 'arguments' : 'an argument'
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
2+
const { test } = QUnit;
3+
4+
import { DEBUG } from '@glimmer/env';
5+
import { tracked, cached } from '@glimmer/tracking';
6+
7+
import * as TSFixtures from './fixtures/typescript';
8+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
9+
// @ts-ignore
10+
import * as BabelFixtures from './fixtures/babel';
11+
12+
QUnit.module('[@glimmer/tracking] @cached Decorators');
13+
14+
test('it works', function (assert) {
15+
class Person {
16+
@tracked firstName = 'Jen';
17+
@tracked lastName = 'Weber';
18+
19+
@cached
20+
get fullName() {
21+
const fullName = `${this.firstName} ${this.lastName}`;
22+
assert.step(fullName);
23+
return fullName;
24+
}
25+
}
26+
27+
const person = new Person();
28+
assert.verifySteps([], 'getter is not called after class initialization');
29+
30+
assert.strictEqual(person.fullName, 'Jen Weber');
31+
assert.verifySteps(['Jen Weber'], 'getter was called after property access');
32+
33+
assert.strictEqual(person.fullName, 'Jen Weber');
34+
assert.verifySteps([], 'getter was not called again after repeated property access');
35+
36+
person.firstName = 'Kenneth';
37+
assert.verifySteps([], 'changing a property does not trigger an eager re-computation');
38+
39+
assert.strictEqual(person.fullName, 'Kenneth Weber');
40+
assert.verifySteps(['Kenneth Weber'], 'accessing the property triggers a re-computation');
41+
42+
assert.strictEqual(person.fullName, 'Kenneth Weber');
43+
assert.verifySteps([], 'getter was not called again after repeated property access');
44+
45+
person.lastName = 'Larsen';
46+
assert.verifySteps([], 'changing a property does not trigger an eager re-computation');
47+
48+
assert.strictEqual(person.fullName, 'Kenneth Larsen');
49+
assert.verifySteps(['Kenneth Larsen'], 'accessing the property triggers a re-computation');
50+
});
51+
52+
// https://github.com/ember-polyfills/ember-cached-decorator-polyfill/issues/7
53+
test('it has a separate cache per class instance', function (assert) {
54+
class Person {
55+
@tracked firstName: string;
56+
@tracked lastName: string;
57+
58+
constructor(firstName: string, lastName: string) {
59+
this.firstName = firstName;
60+
this.lastName = lastName;
61+
}
62+
63+
@cached
64+
get fullName() {
65+
const fullName = `${this.firstName} ${this.lastName}`;
66+
assert.step(fullName);
67+
return fullName;
68+
}
69+
}
70+
71+
const jen = new Person('Jen', 'Weber');
72+
const chris = new Person('Chris', 'Garrett');
73+
74+
assert.verifySteps([], 'getter is not called after class initialization');
75+
76+
assert.strictEqual(jen.fullName, 'Jen Weber');
77+
assert.verifySteps(['Jen Weber'], 'getter was called after property access');
78+
79+
assert.strictEqual(jen.fullName, 'Jen Weber');
80+
assert.verifySteps([], 'getter was not called again after repeated property access');
81+
82+
assert.strictEqual(chris.fullName, 'Chris Garrett', 'other instance has a different value');
83+
assert.verifySteps(['Chris Garrett'], 'getter was called after property access');
84+
85+
assert.strictEqual(chris.fullName, 'Chris Garrett');
86+
assert.verifySteps([], 'getter was not called again after repeated property access');
87+
88+
chris.lastName = 'Manson';
89+
assert.verifySteps([], 'changing a property does not trigger an eager re-computation');
90+
91+
assert.strictEqual(jen.fullName, 'Jen Weber', 'other instance is unaffected');
92+
assert.verifySteps([], 'getter was not called again after repeated property access');
93+
94+
assert.strictEqual(chris.fullName, 'Chris Manson');
95+
assert.verifySteps(['Chris Manson'], 'getter was called after property access');
96+
97+
assert.strictEqual(jen.fullName, 'Jen Weber', 'other instance is unaffected');
98+
assert.verifySteps([], 'getter was not called again after repeated property access');
99+
});
100+
101+
[
102+
['Babel', BabelFixtures],
103+
['TypeScript', TSFixtures],
104+
].forEach(([compiler, F]) => {
105+
QUnit.module(`[@glimmer/tracking] Cached Property Decorators with ${compiler}`);
106+
107+
if (DEBUG) {
108+
test('Cached decorator on a property throws an error', (assert) => {
109+
assert.throws(F.createClassWithCachedProperty);
110+
});
111+
112+
test('Cached decorator with a setter throws an error', (assert) => {
113+
assert.throws(F.createClassWithCachedSetter);
114+
});
115+
116+
test('Cached decorator with arguments throws an error', function (assert) {
117+
assert.throws(
118+
F.createClassWithCachedDependentKeys,
119+
/@cached\('firstName', 'lastName'\)/,
120+
'the correct error is thrown'
121+
);
122+
});
123+
124+
test('Using @cached as a decorator factory throws an error', function (assert) {
125+
assert.throws(
126+
F.createClassWithCachedAsDecoratorFactory,
127+
/@cached\(\)/,
128+
'The correct error is thrown'
129+
);
130+
});
131+
}
132+
});

packages/@glimmer/tracking/test/fixtures/babel.js

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { tracked } from '@glimmer/tracking';
1+
import {cached, tracked} from '@glimmer/tracking';
22

33
export class Tom {
44
@tracked firstName = 'Tom';
@@ -16,7 +16,7 @@ class FrozenToran {
1616

1717
Object.freeze(FrozenToran);
1818

19-
export { FrozenToran };
19+
export {FrozenToran};
2020

2121
export class PersonWithCount {
2222
@tracked _firstName = 'Tom';
@@ -100,6 +100,14 @@ export function createClassWithTrackedGetter() {
100100
return new PersonWithTrackedGetter();
101101
}
102102

103+
export function createClassWithCachedProperty() {
104+
class PersonWithCachedProperty {
105+
@cached firstName = 'Tom';
106+
}
107+
108+
return new PersonWithCachedProperty();
109+
}
110+
103111
export function createClassWithTrackedSetter() {
104112
class PersonWithTrackedSetter {
105113
@tracked firstName = 'Tom';
@@ -115,6 +123,22 @@ export function createClassWithTrackedSetter() {
115123
return new PersonWithTrackedSetter();
116124
}
117125

126+
export function createClassWithCachedSetter() {
127+
class PersonWithCachedSetter {
128+
@tracked firstName = 'Tom';
129+
@tracked lastName;
130+
131+
@cached set fullName(fullName) {
132+
const [firstName, lastName] = fullName.split(' ');
133+
this.firstName = firstName;
134+
this.lastName = lastName;
135+
}
136+
}
137+
138+
return new PersonWithCachedSetter();
139+
}
140+
141+
118142
export function createClassWithTrackedDependentKeys() {
119143
class DependentKeysAreCool {
120144
@tracked('firstName', 'lastName') fullName() {
@@ -127,6 +151,18 @@ export function createClassWithTrackedDependentKeys() {
127151
return new DependentKeysAreCool();
128152
}
129153

154+
export function createClassWithCachedDependentKeys() {
155+
class DependentKeysAreCool {
156+
@cached('firstName', 'lastName') fullName() {
157+
return `${this.firstName} ${this.lastName}`;
158+
}
159+
160+
@tracked firstName = 'Tom';
161+
@tracked lastName = 'Dale';
162+
}
163+
return new DependentKeysAreCool();
164+
}
165+
130166
export function createClassWithTrackedAsDecoratorFactory() {
131167
class DependentKeysAreCool {
132168
@tracked() fullName() {
@@ -138,3 +174,15 @@ export function createClassWithTrackedAsDecoratorFactory() {
138174
}
139175
return new DependentKeysAreCool();
140176
}
177+
178+
export function createClassWithCachedAsDecoratorFactory() {
179+
class DependentKeysAreCool {
180+
@cached() fullName() {
181+
return `${this.firstName} ${this.lastName}`;
182+
}
183+
184+
@tracked firstName = 'Tom';
185+
@tracked lastName = 'Dale';
186+
}
187+
return new DependentKeysAreCool();
188+
}

packages/@glimmer/tracking/test/fixtures/typescript.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { tracked } from '@glimmer/tracking';
1+
import { cached, tracked } from '@glimmer/tracking';
22

33
export class Tom {
44
@tracked firstName = 'Tom';
@@ -100,6 +100,14 @@ export function createClassWithTrackedGetter(): any {
100100
return new PersonWithTrackedGetter();
101101
}
102102

103+
export function createClassWithCachedProperty(): any {
104+
class PersonWithCachedProperty {
105+
@cached firstName = 'Tom';
106+
}
107+
108+
return new PersonWithCachedProperty();
109+
}
110+
103111
export function createClassWithTrackedSetter(): any {
104112
class PersonWithTrackedSetter {
105113
@tracked firstName = 'Tom';
@@ -116,6 +124,22 @@ export function createClassWithTrackedSetter(): any {
116124
return new PersonWithTrackedSetter();
117125
}
118126

127+
export function createClassWithCachedSetter(): any {
128+
class PersonWithCachedSetter {
129+
@tracked firstName = 'Tom';
130+
@tracked lastName: any;
131+
132+
// @ts-ignore
133+
@cached set fullName(fullName) {
134+
const [firstName, lastName] = fullName.split(' ');
135+
this.firstName = firstName;
136+
this.lastName = lastName;
137+
}
138+
}
139+
140+
return new PersonWithCachedSetter();
141+
}
142+
119143
export function createClassWithTrackedDependentKeys(): any {
120144
class DependentKeysAreCool {
121145
// @ts-ignore
@@ -129,6 +153,19 @@ export function createClassWithTrackedDependentKeys(): any {
129153
return new DependentKeysAreCool();
130154
}
131155

156+
export function createClassWithCachedDependentKeys(): any {
157+
class DependentKeysAreCool {
158+
// @ts-ignore
159+
@cached('firstName', 'lastName') fullName() {
160+
return `${this.firstName} ${this.lastName}`;
161+
}
162+
163+
@tracked firstName = 'Tom';
164+
@tracked lastName = 'Dale';
165+
}
166+
return new DependentKeysAreCool();
167+
}
168+
132169
export function createClassWithTrackedAsDecoratorFactory(): any {
133170
class DependentKeysAreCool {
134171
// @ts-ignore
@@ -141,3 +178,16 @@ export function createClassWithTrackedAsDecoratorFactory(): any {
141178
}
142179
return new DependentKeysAreCool();
143180
}
181+
182+
export function createClassWithCachedAsDecoratorFactory(): any {
183+
class DependentKeysAreCool {
184+
// @ts-ignore
185+
@cached() fullName() {
186+
return `${this.firstName} ${this.lastName}`;
187+
}
188+
189+
@tracked firstName = 'Tom';
190+
@tracked lastName = 'Dale';
191+
}
192+
return new DependentKeysAreCool();
193+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
import './tracked-decorator-test';
2+
import './cached-decorator-test';

0 commit comments

Comments
 (0)