@expressive/mvc 0.53.3 → 0.53.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ import { Model, PARENT, define, uid } from './model';
2
+ const LOOKUP = new WeakMap();
3
+ const KEYS = new Map();
4
+ function key(T, upstream) {
5
+ let K = KEYS.get(T);
6
+ if (!K)
7
+ KEYS.set(T, K = Symbol(typeof T == "symbol" ? "get " + T.description : String(T)));
8
+ return upstream ? key(K) : K;
9
+ }
10
+ function keys(from, upstream) {
11
+ const keys = new Set();
12
+ do {
13
+ keys.add(key(from, upstream));
14
+ } while ((from = Object.getPrototypeOf(from)) !== Model);
15
+ return keys;
16
+ }
17
+ class Context {
18
+ static get(from, callback) {
19
+ const waiting = LOOKUP.get(from);
20
+ if (waiting instanceof Context) {
21
+ if (callback)
22
+ callback(waiting);
23
+ return waiting;
24
+ }
25
+ if (callback)
26
+ if (waiting)
27
+ waiting.push(callback);
28
+ else
29
+ LOOKUP.set(from, [callback]);
30
+ }
31
+ constructor(inputs) {
32
+ this.layer = new Map();
33
+ this.cleanup = new Set();
34
+ if (inputs)
35
+ this.include(inputs);
36
+ }
37
+ /** Run callback when a specified type is registered to a **child** context. */
38
+ has(Type, callback) {
39
+ define(this, key(Type, true), { value: callback });
40
+ }
41
+ /** Find specified type registered to a parent context. Returns undefined if none are found. */
42
+ get(Type) {
43
+ const result = this[key(Type)];
44
+ if (result === null)
45
+ throw new Error(`Did find ${Type} in context, but multiple were defined.`);
46
+ return result;
47
+ }
48
+ push(inputs) {
49
+ const next = Object.create(this);
50
+ next.layer = new Map();
51
+ next.cleanup = new Set();
52
+ if (inputs)
53
+ next.include(inputs);
54
+ return next;
55
+ }
56
+ pop() {
57
+ for (const key of Object.getOwnPropertySymbols(this))
58
+ delete this[key];
59
+ this.cleanup.forEach(cb => cb());
60
+ this.layer.clear();
61
+ }
62
+ include(inputs, forEach) {
63
+ const init = new Map();
64
+ if (typeof inputs == "function" || inputs instanceof Model)
65
+ inputs = { 0: inputs };
66
+ Object.entries(inputs).forEach(([K, V]) => {
67
+ if (Model.is(V) || V instanceof Model)
68
+ return;
69
+ if (K && K != V)
70
+ V = `${V} (as '${K}')`;
71
+ throw new Error(`Context may only include instance or class \`extends Model\` but got ${V}.`);
72
+ });
73
+ for (const key in inputs) {
74
+ const input = inputs[key];
75
+ const exists = this.layer.get(key);
76
+ if (!exists) {
77
+ const instance = this.add(input);
78
+ this.layer.set(key, input);
79
+ init.set(instance, true);
80
+ }
81
+ // Context must force-reset because inputs are no longer safe.
82
+ else if (exists !== input) {
83
+ this.pop();
84
+ this.id = uid();
85
+ this.include(inputs);
86
+ }
87
+ }
88
+ for (const [model, explicit] of init) {
89
+ model.set();
90
+ if (typeof forEach == "function")
91
+ forEach(model, explicit);
92
+ else if (forEach && explicit)
93
+ model.set(forEach);
94
+ for (const [_key, value] of model)
95
+ if (PARENT.get(value) === model) {
96
+ this.add(value, true);
97
+ init.set(value, false);
98
+ }
99
+ }
100
+ }
101
+ add(input, implicit) {
102
+ let T;
103
+ let I;
104
+ if (typeof input == "function") {
105
+ Model.is(input);
106
+ T = input;
107
+ I = new input();
108
+ this.cleanup.add(() => I.set(null));
109
+ }
110
+ else {
111
+ T = input.constructor;
112
+ I = input;
113
+ }
114
+ keys(T, true).forEach(K => {
115
+ const result = this[K];
116
+ if (!result)
117
+ return;
118
+ const callback = result(I);
119
+ if (callback)
120
+ this.cleanup.add(callback);
121
+ });
122
+ keys(T).forEach(K => {
123
+ const value = this.hasOwnProperty(K) ? null : I;
124
+ if (value || this[K] !== I && !implicit)
125
+ define(this, K, {
126
+ configurable: true,
127
+ value
128
+ });
129
+ });
130
+ const waiting = LOOKUP.get(I);
131
+ if (waiting instanceof Array)
132
+ waiting.forEach(cb => cb(this));
133
+ LOOKUP.set(I, this);
134
+ return I;
135
+ }
136
+ }
137
+ export { Context };
@@ -0,0 +1,211 @@
1
+ import { Context } from './context';
2
+ import { Model } from './model';
3
+ class Example extends Model {
4
+ }
5
+ ;
6
+ class Example2 extends Example {
7
+ }
8
+ ;
9
+ it("will add instance to context", () => {
10
+ const example = Example.new();
11
+ const context = new Context();
12
+ context.include(example);
13
+ expect(context.get(Example)).toBe(example);
14
+ });
15
+ it("will create instance in context", () => {
16
+ const context = new Context(Example);
17
+ expect(context.get(Example)).toBeInstanceOf(Example);
18
+ });
19
+ it("will not create base Model", () => {
20
+ // @ts-expect-error
21
+ const attempt = () => new Context({ Model });
22
+ expect(attempt).toThrowError("Cannot create base Model.");
23
+ });
24
+ it("will include children of Model", () => {
25
+ class Test extends Model {
26
+ constructor() {
27
+ super(...arguments);
28
+ this.example = new Example();
29
+ }
30
+ }
31
+ const context = new Context({ Test });
32
+ expect(context.get(Example)).toBeInstanceOf(Example);
33
+ });
34
+ it("will not include initialized child", () => {
35
+ class Test extends Model {
36
+ constructor() {
37
+ super(...arguments);
38
+ // this will be initialized before parent is
39
+ this.example = Example.new();
40
+ }
41
+ }
42
+ const context = new Context({ Test });
43
+ expect(context.get(Example)).toBeUndefined();
44
+ });
45
+ it("will access upstream controller", () => {
46
+ const example = Example.new();
47
+ const context = new Context({ example });
48
+ const context2 = context.push();
49
+ expect(context2.get(Example)).toBe(example);
50
+ });
51
+ it("will register all subtypes", () => {
52
+ const example2 = new Example2();
53
+ const context = new Context({ example2 });
54
+ expect(context.get(Example2)).toBe(example2);
55
+ expect(context.get(Example)).toBe(example2);
56
+ });
57
+ it("will return undefined if not found", () => {
58
+ const context = new Context();
59
+ const got = context.get(Example);
60
+ expect(got).toBeUndefined();
61
+ });
62
+ it("will complain if multiple registered", () => {
63
+ const context = new Context({
64
+ e1: Example,
65
+ e2: Example
66
+ });
67
+ const fetch = () => context.get(Example);
68
+ expect(fetch).toThrowError(`Did find Example in context, but multiple were defined.`);
69
+ });
70
+ it("will ignore if multiple but same", () => {
71
+ const example = Example.new();
72
+ const context = new Context({
73
+ e1: example,
74
+ e2: example
75
+ });
76
+ const got = context.get(Example);
77
+ expect(got).toBe(example);
78
+ });
79
+ it("will destroy modules created by layer", () => {
80
+ class Test extends Model {
81
+ constructor() {
82
+ super();
83
+ this.destroyed = jest.fn();
84
+ this.get(null, this.destroyed);
85
+ }
86
+ }
87
+ class Test1 extends Test {
88
+ }
89
+ ;
90
+ class Test2 extends Test {
91
+ }
92
+ ;
93
+ class Test3 extends Test {
94
+ }
95
+ ;
96
+ const test2 = Test2.new();
97
+ const context1 = new Context({ Test1 });
98
+ const context2 = context1.push({ test2, Test3 });
99
+ const test1 = context2.get(Test1);
100
+ const test3 = context2.get(Test3);
101
+ context2.pop();
102
+ expect(test1.destroyed).not.toBeCalled();
103
+ expect(test2.destroyed).not.toBeCalled();
104
+ expect(test3.destroyed).toBeCalled();
105
+ });
106
+ describe("include", () => {
107
+ class Foo extends Model {
108
+ }
109
+ class Bar extends Model {
110
+ }
111
+ class FooBar extends Model {
112
+ constructor() {
113
+ super(...arguments);
114
+ this.foo = new Foo();
115
+ }
116
+ }
117
+ it("will register in batch", () => {
118
+ const foo = Foo.new();
119
+ const bar = Bar.new();
120
+ const context = new Context({ foo, bar });
121
+ expect(context.get(Foo)).toBe(foo);
122
+ expect(context.get(Bar)).toBe(bar);
123
+ });
124
+ it("will callback once per unique added", () => {
125
+ const foo = Foo.new();
126
+ const bar = Bar.new();
127
+ const cb = jest.fn();
128
+ const context = new Context();
129
+ context.include({ foo, bar }, cb);
130
+ expect(cb).toBeCalledWith(foo, true);
131
+ expect(cb).toBeCalledWith(bar, true);
132
+ expect(cb).toBeCalledTimes(2);
133
+ context.include({ foo, bar }, cb);
134
+ expect(cb).toBeCalledTimes(2);
135
+ const foo2 = Foo.new();
136
+ context.include({ foo, bar, foo2 }, cb);
137
+ expect(cb).toBeCalledWith(foo2, true);
138
+ expect(cb).toBeCalledTimes(3);
139
+ });
140
+ it("will assign to newly added instances", () => {
141
+ class Test extends Model {
142
+ constructor() {
143
+ super(...arguments);
144
+ this.value = 0;
145
+ }
146
+ }
147
+ const t1 = Test.new();
148
+ const t2 = Test.new();
149
+ const context = new Context();
150
+ context.include({ t1 }, { value: 1 });
151
+ expect(t1.value).toBe(1);
152
+ t1.value = 2;
153
+ context.include({ t1, t2 }, { value: 3 });
154
+ // t1 is already in context, should not be reassigned.
155
+ expect(t1.value).toBe(2);
156
+ expect(t2.value).toBe(3);
157
+ });
158
+ // This will be made more elegant later.
159
+ it("will hard-reset if inputs differ", () => {
160
+ const bazDidDie = jest.fn();
161
+ class Baz extends Model {
162
+ constructor() {
163
+ super();
164
+ this.get(() => bazDidDie);
165
+ }
166
+ }
167
+ const foo = Foo.new();
168
+ const bar = Bar.new();
169
+ const context = new Context({ foo, bar, Baz });
170
+ const idPriorToUpdate = context.id;
171
+ const baz = context.get(Baz);
172
+ context.include({ foo, bar: Bar.new(), Baz });
173
+ // key should change despite technically same layer.
174
+ expect(context.id).not.toBe(idPriorToUpdate);
175
+ // expect all instances did get replaced.
176
+ expect(context.get(Bar)).not.toBe(bar);
177
+ // expect Baz will have been force-replaced.
178
+ expect(bazDidDie).toBeCalled();
179
+ const newBaz = context.get(Baz);
180
+ expect(newBaz).toBeInstanceOf(Baz);
181
+ expect(newBaz).not.toBe(baz);
182
+ });
183
+ it("will register children implicitly", () => {
184
+ const foobar = FooBar.new();
185
+ const context = new Context({ foobar });
186
+ expect(context.get(FooBar)).toBe(foobar);
187
+ expect(context.get(Foo)).toBe(foobar.foo);
188
+ });
189
+ it("will prefer explicit over implicit", () => {
190
+ const foo = Foo.new();
191
+ const foobar = FooBar.new();
192
+ const context = new Context({ foobar, foo });
193
+ expect(context.get(FooBar)).toBe(foobar);
194
+ expect(context.get(Foo)).not.toBe(foobar.foo);
195
+ expect(context.get(Foo)).toBe(foo);
196
+ });
197
+ });
198
+ it("will throw on bad include", () => {
199
+ const context = new Context();
200
+ expect(() => context.include(undefined)).toThrowError();
201
+ });
202
+ it("will throw on base Model include", () => {
203
+ const context = new Context();
204
+ // @ts-ignore
205
+ expect(() => context.include({ Model })).toThrowError("Cannot create base Model.");
206
+ });
207
+ it("will throw on bad include property", () => {
208
+ const context = new Context();
209
+ // @ts-ignore
210
+ expect(() => context.include({ Thing: undefined })).toThrowError("Context may only include instance or class `extends Model` but got undefined (as 'Thing').");
211
+ });
@@ -0,0 +1,182 @@
1
+ /** Placeholder event determines if model is initialized or not. */
2
+ const onReady = () => null;
3
+ const LISTENERS = new WeakMap();
4
+ /** Events pending for a given object. */
5
+ const PENDING = new WeakMap();
6
+ /** Update register. */
7
+ const PENDING_KEYS = new WeakMap();
8
+ /** Central event dispatch. Bunches all updates to occur at same time. */
9
+ const DISPATCH = new Set();
10
+ const OBSERVER = new WeakMap();
11
+ function addListener(subject, callback, select) {
12
+ let listeners = LISTENERS.get(subject);
13
+ if (!listeners)
14
+ LISTENERS.set(subject, listeners = new Map([[onReady, undefined]]));
15
+ const filter = select === undefined ? undefined : new Set([select]);
16
+ if (!listeners.has(onReady) && !filter)
17
+ callback.call(subject, true, subject);
18
+ listeners.set(callback, filter);
19
+ return () => listeners.delete(callback);
20
+ }
21
+ function emit(model, key) {
22
+ const listeners = LISTENERS.get(model);
23
+ const notReady = listeners.has(onReady);
24
+ if (key === true && !notReady)
25
+ return;
26
+ let pending = PENDING.get(model);
27
+ if (pending) {
28
+ pending.add(key);
29
+ return;
30
+ }
31
+ PENDING.set(model, pending = new Set(notReady ? [true, key] : [key]));
32
+ for (const key of pending)
33
+ for (const [callback, filter] of listeners)
34
+ if (!filter || filter.has(key)) {
35
+ const after = callback.call(model, key, model);
36
+ if (after)
37
+ enqueue(after);
38
+ else if (after === null)
39
+ listeners.delete(callback);
40
+ }
41
+ if (key === null)
42
+ listeners.clear();
43
+ PENDING.delete(model);
44
+ }
45
+ function event(subject, key, silent) {
46
+ if (key === null)
47
+ return emit(subject, key);
48
+ if (!key)
49
+ return emit(subject, true);
50
+ let pending = PENDING_KEYS.get(subject);
51
+ if (!pending) {
52
+ PENDING_KEYS.set(subject, pending = new Set());
53
+ if (!silent)
54
+ enqueue(() => {
55
+ emit(subject, false);
56
+ PENDING_KEYS.delete(subject);
57
+ });
58
+ }
59
+ pending.add(key);
60
+ if (!silent)
61
+ emit(subject, key);
62
+ }
63
+ function pending(subject) {
64
+ if (PENDING_KEYS.has(subject))
65
+ return {
66
+ then: (res) => new Promise(res => {
67
+ const remove = addListener(subject, key => {
68
+ if (key !== true) {
69
+ remove();
70
+ return res.bind(null, Array.from(PENDING_KEYS.get(subject)));
71
+ }
72
+ });
73
+ }).then(res)
74
+ };
75
+ }
76
+ function enqueue(eventHandler) {
77
+ if (!DISPATCH.size)
78
+ setTimeout(() => {
79
+ DISPATCH.forEach(event => {
80
+ try {
81
+ event();
82
+ }
83
+ catch (err) {
84
+ console.error(err);
85
+ }
86
+ });
87
+ DISPATCH.clear();
88
+ }, 0);
89
+ DISPATCH.add(eventHandler);
90
+ }
91
+ function createProxy(from, observer) {
92
+ const proxy = Object.create(from);
93
+ OBSERVER.set(proxy, observer);
94
+ return proxy;
95
+ }
96
+ function watch(from, key, value) {
97
+ const access = OBSERVER.get(from);
98
+ return access ? access(from, key, value) : value;
99
+ }
100
+ function createEffect(target, callback, argument) {
101
+ const listeners = LISTENERS.get(target);
102
+ let unset;
103
+ let reset;
104
+ function invoke() {
105
+ let stale;
106
+ const subscriber = createProxy(target, access);
107
+ LISTENERS.set(subscriber, listeners);
108
+ function onUpdate() {
109
+ if (stale)
110
+ return null;
111
+ stale = true;
112
+ if (reset && unset) {
113
+ unset(true);
114
+ unset = undefined;
115
+ }
116
+ return reset;
117
+ }
118
+ function access(from, key, value) {
119
+ if (value === undefined && argument)
120
+ throw new Error(`${from}.${key} is required in this context.`);
121
+ const listeners = LISTENERS.get(from);
122
+ let listener = listeners.get(onUpdate);
123
+ if (!listener)
124
+ listeners.set(onUpdate, listener = new Set);
125
+ listener.add(key);
126
+ const nested = LISTENERS.get(value);
127
+ if (nested)
128
+ LISTENERS.set(value = createProxy(value, access), nested);
129
+ return value;
130
+ }
131
+ try {
132
+ const ctx = context(argument === false);
133
+ const ret = callback.call(subscriber, subscriber);
134
+ const flush = ctx();
135
+ reset = ret === null ? null : invoke;
136
+ unset = key => {
137
+ if (typeof ret == "function")
138
+ ret(key);
139
+ flush();
140
+ };
141
+ }
142
+ catch (err) {
143
+ if (err instanceof Promise) {
144
+ reset = undefined;
145
+ err.then(invoke);
146
+ }
147
+ else
148
+ throw err;
149
+ }
150
+ }
151
+ function cleanup() {
152
+ if (unset)
153
+ unset(false);
154
+ reset = null;
155
+ }
156
+ ;
157
+ if (EffectContext && argument !== false)
158
+ EffectContext.add(cleanup);
159
+ addListener(target, key => {
160
+ if (key === true)
161
+ invoke();
162
+ else if (!reset)
163
+ return reset;
164
+ if (key === null && unset)
165
+ unset(null);
166
+ });
167
+ return cleanup;
168
+ }
169
+ let EffectContext;
170
+ export function context(ignore) {
171
+ if (ignore)
172
+ return () => () => { };
173
+ const current = EffectContext;
174
+ const ctx = EffectContext = new Set;
175
+ return () => {
176
+ EffectContext = current;
177
+ return () => {
178
+ ctx.forEach(fn => fn());
179
+ };
180
+ };
181
+ }
182
+ export { addListener, createEffect, createProxy, event, PENDING_KEYS, pending, watch };
@@ -0,0 +1,183 @@
1
+ import { createEffect, createProxy } from './control';
2
+ import { set } from './instruction/set';
3
+ import { use } from './instruction/use';
4
+ import { mockError } from './mocks';
5
+ import { Model } from './model';
6
+ describe("effect", () => {
7
+ it("will run after properties", () => {
8
+ const mock = jest.fn();
9
+ class Test extends Model {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.property = use((_key, _model, state) => {
13
+ this.get(() => mock(state));
14
+ });
15
+ this.foo = 1;
16
+ this.bar = 2;
17
+ }
18
+ }
19
+ Test.new();
20
+ expect(mock).toBeCalledWith({ foo: 1, bar: 2 });
21
+ });
22
+ it("will enforce values if required", () => {
23
+ class Test extends Model {
24
+ constructor() {
25
+ super(...arguments);
26
+ this.property = undefined;
27
+ }
28
+ }
29
+ const test = Test.new("ID");
30
+ const attempt = () => {
31
+ createEffect(test, $ => {
32
+ expect($.property);
33
+ }, true);
34
+ };
35
+ expect(attempt).toThrowError(`ID.property is required in this context.`);
36
+ });
37
+ it("will still get events after silent ones", async () => {
38
+ class Test extends Model {
39
+ constructor() {
40
+ super(...arguments);
41
+ this.value1 = 1;
42
+ this.value2 = 2;
43
+ }
44
+ }
45
+ const test = Test.new();
46
+ const didGetValue = jest.fn();
47
+ test.get($ => {
48
+ didGetValue($.value1, $.value2);
49
+ });
50
+ expect(didGetValue).toBeCalledWith(1, 2);
51
+ test.set({ value1: 10 }, true);
52
+ test.set({ value2: 20 });
53
+ await expect(test).toHaveUpdated();
54
+ expect(didGetValue).toBeCalledWith(10, 20);
55
+ expect(didGetValue).toBeCalledTimes(2);
56
+ });
57
+ it("will cleanup nested effects", async () => {
58
+ class Test extends Model {
59
+ constructor() {
60
+ super(...arguments);
61
+ this.foo = 1;
62
+ this.bar = 2;
63
+ }
64
+ }
65
+ const test = Test.new();
66
+ const didInvoke = jest.fn();
67
+ const done = createEffect(test, ({ foo }) => {
68
+ createEffect(test, ({ bar }) => {
69
+ didInvoke({ foo, bar });
70
+ });
71
+ });
72
+ expect(didInvoke).toBeCalledTimes(1);
73
+ expect(didInvoke).toBeCalledWith({ foo: 1, bar: 2 });
74
+ await test.set({ bar: 3 });
75
+ expect(didInvoke).toBeCalledWith({ foo: 1, bar: 3 });
76
+ await test.set({ foo: 2, bar: 4 });
77
+ expect(didInvoke).toBeCalledWith({ foo: 2, bar: 4 });
78
+ expect(didInvoke).not.toBeCalledWith({ foo: 1, bar: 4 });
79
+ await test.set({ bar: 2 });
80
+ expect(didInvoke).toBeCalledWith({ foo: 2, bar: 2 });
81
+ done();
82
+ await test.set({ bar: 1 });
83
+ expect(didInvoke).not.toBeCalledWith({ foo: 2, bar: 1 });
84
+ expect(didInvoke).toBeCalledTimes(4);
85
+ });
86
+ });
87
+ describe("suspense", () => {
88
+ it("will seem to throw error outside react", () => {
89
+ class Test extends Model {
90
+ constructor() {
91
+ super(...arguments);
92
+ this.value = set();
93
+ }
94
+ }
95
+ const instance = Test.new("ID");
96
+ let didThrow;
97
+ try {
98
+ void instance.value;
99
+ }
100
+ catch (err) {
101
+ didThrow = err;
102
+ }
103
+ expect(String(didThrow)).toMatchInlineSnapshot(`"Error: ID.value is not yet available."`);
104
+ });
105
+ it("will reject if model destroyed before resolved", async () => {
106
+ class Test extends Model {
107
+ constructor() {
108
+ super(...arguments);
109
+ this.value = set();
110
+ }
111
+ }
112
+ const instance = Test.new("ID");
113
+ let didThrow;
114
+ try {
115
+ void instance.value;
116
+ }
117
+ catch (err) {
118
+ didThrow = err;
119
+ }
120
+ instance.set(null);
121
+ await expect(didThrow).rejects.toThrowError(`ID is destroyed.`);
122
+ });
123
+ });
124
+ describe("errors", () => {
125
+ const error = mockError();
126
+ it("will throw sync error to the console", async () => {
127
+ class Test extends Model {
128
+ constructor() {
129
+ super(...arguments);
130
+ this.value = 1;
131
+ }
132
+ }
133
+ ;
134
+ const test = Test.new();
135
+ test.set(() => {
136
+ throw new Error("sync error");
137
+ });
138
+ const attempt = () => test.value = 2;
139
+ expect(attempt).toThrowError(`sync error`);
140
+ });
141
+ it("will log async error to the console", async () => {
142
+ class Test extends Model {
143
+ constructor() {
144
+ super(...arguments);
145
+ this.value = 1;
146
+ }
147
+ }
148
+ ;
149
+ const expected = new Error("async error");
150
+ const test = Test.new();
151
+ test.get($ => {
152
+ if ($.value == 2)
153
+ throw expected;
154
+ });
155
+ test.value = 2;
156
+ await expect(test).toHaveUpdated();
157
+ expect(error).toBeCalledWith(expected);
158
+ });
159
+ });
160
+ describe("proxy", () => {
161
+ it("will create a proxy object", () => {
162
+ class Test extends Model {
163
+ constructor() {
164
+ super(...arguments);
165
+ this.foo = 1;
166
+ this.bar = 2;
167
+ this.baz = 3;
168
+ }
169
+ }
170
+ const test = Test.new("Test");
171
+ const getter = jest.fn((self, key, value) => {
172
+ return `${self}.${key}=${value}`;
173
+ });
174
+ const proxy = createProxy(test, getter);
175
+ expect(proxy.foo).toBe(`Test.foo=1`);
176
+ expect(getter).toBeCalledWith(test, "foo", 1);
177
+ expect(proxy.bar).toBe(`Test.bar=2`);
178
+ expect(getter).toBeCalledWith(test, "bar", 2);
179
+ expect(proxy.baz).toBe(`Test.baz=3`);
180
+ expect(getter).toBeCalledWith(test, "baz", 3);
181
+ expect(getter).toBeCalledTimes(3);
182
+ });
183
+ });