From 3d25f4fdf42230f4fbaf90f2ec8e4896499c675d Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 16 Feb 2025 12:43:42 -0500 Subject: [PATCH 1/4] Implement TrackedArray --- packages/@glimmer/validator/index.ts | 1 + .../validator/lib/collections/array.ts | 219 ++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 packages/@glimmer/validator/lib/collections/array.ts diff --git a/packages/@glimmer/validator/index.ts b/packages/@glimmer/validator/index.ts index 0fad5d7e3..727bbf23c 100644 --- a/packages/@glimmer/validator/index.ts +++ b/packages/@glimmer/validator/index.ts @@ -8,6 +8,7 @@ if (Reflect.has(globalThis, GLIMMER_VALIDATOR_REGISTRATION)) { Reflect.set(globalThis, GLIMMER_VALIDATOR_REGISTRATION, true); +export { TrackedArray } from './lib/collections/array'; export { debug } from './lib/debug'; export { dirtyTagFor, tagFor, type TagMeta, tagMetaFor } from './lib/meta'; export { trackedData } from './lib/tracked-data'; diff --git a/packages/@glimmer/validator/lib/collections/array.ts b/packages/@glimmer/validator/lib/collections/array.ts new file mode 100644 index 000000000..8d299edea --- /dev/null +++ b/packages/@glimmer/validator/lib/collections/array.ts @@ -0,0 +1,219 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Unfortunately, TypeScript's ability to do inference *or* type-checking in a +// `Proxy`'s body is very limited, so we have to use a number of casts `as any` +// to make the internal accesses work. The type safety of these is guaranteed at +// the *call site* instead of within the body: you cannot do `Array.blah` in TS, +// and it will blow up in JS in exactly the same way, so it is safe to assume +// that properties within the getter have the correct type in TS. + +import { consumeTag } from '../tracking'; +import { createUpdatableTag, DIRTY_TAG } from '../validators'; + +const ARRAY_GETTER_METHODS = new Set([ + Symbol.iterator, + 'concat', + 'entries', + 'every', + 'filter', + 'find', + 'findIndex', + 'flat', + 'flatMap', + 'forEach', + 'includes', + 'indexOf', + 'join', + 'keys', + 'lastIndexOf', + 'map', + 'reduce', + 'reduceRight', + 'slice', + 'some', + 'values', +]); + +// For these methods, `Array` itself immediately gets the `.length` to return +// after invoking them. +const ARRAY_WRITE_THEN_READ_METHODS = new Set(['fill', 'push', 'unshift']); + +function convertToInt(prop: number | string | symbol): number | null { + if (typeof prop === 'symbol') return null; + + const num = Number(prop); + + if (isNaN(num)) return null; + + return num % 1 === 0 ? num : null; +} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class TrackedArray { + /** + * Creates an array from an iterable object. + * @param iterable An iterable object to convert to an array. + */ + static from(iterable: Iterable | ArrayLike): TrackedArray; + + /** + * Creates an array from an iterable object. + * @param iterable An iterable object to convert to an array. + * @param mapfn A mapping function to call on every element of the array. + * @param thisArg Value of 'this' used to invoke the mapfn. + */ + static from( + iterable: Iterable | ArrayLike, + mapfn: (v: T, k: number) => U, + thisArg?: unknown + ): TrackedArray; + + static from( + iterable: Iterable | ArrayLike, + mapfn?: (v: T, k: number) => U, + thisArg?: unknown + ): TrackedArray | TrackedArray { + return mapfn + ? new TrackedArray(Array.from(iterable, mapfn, thisArg)) + : new TrackedArray(Array.from(iterable)); + } + + static of(...arr: T[]): TrackedArray { + return new TrackedArray(arr); + } + + constructor(arr: T[] = []) { + const clone = arr.slice(); + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + + const boundFns = new Map any>(); + + /** + Flag to track whether we have *just* intercepted a call to `.push()` or + `.unshift()`, since in those cases (and only those cases!) the `Array` + itself checks `.length` to return from the function call. + */ + let nativelyAccessingLengthFromPushOrUnshift = false; + + return new Proxy(clone, { + get(target, prop /*, _receiver */) { + const index = convertToInt(prop); + + if (index !== null) { + self.#readStorageFor(index); + consumeTag(self.#collection); + + return target[index]; + } + + if (prop === 'length') { + // If we are reading `.length`, it may be a normal user-triggered + // read, or it may be a read triggered by Array itself. In the latter + // case, it is because we have just done `.push()` or `.unshift()`; in + // that case it is safe not to mark this as a *read* operation, since + // calling `.push()` or `.unshift()` cannot otherwise be part of a + // "read" operation safely, and if done during an *existing* read + // (e.g. if the user has already checked `.length` *prior* to this), + // that will still trigger the mutation-after-consumption assertion. + if (nativelyAccessingLengthFromPushOrUnshift) { + nativelyAccessingLengthFromPushOrUnshift = false; + } else { + consumeTag(self.#collection); + } + + return target[prop]; + } + + // Here, track that we are doing a `.push()` or `.unshift()` by setting + // the flag to `true` so that when the `.length` is read by `Array` (see + // immediately above), it knows not to dirty the collection. + if (ARRAY_WRITE_THEN_READ_METHODS.has(prop)) { + nativelyAccessingLengthFromPushOrUnshift = true; + } + + if (ARRAY_GETTER_METHODS.has(prop)) { + let fn = boundFns.get(prop); + + if (fn === undefined) { + fn = (...args) => { + consumeTag(self.#collection); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + return (target as any)[prop](...args); + }; + + boundFns.set(prop, fn); + } + + return fn; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return (target as any)[prop]; + }, + + set(target, prop, value /*, _receiver */) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + (target as any)[prop] = value; + + const index = convertToInt(prop); + + if (index !== null) { + self.#dirtyStorageFor(index); + self.#dirtyCollection(); + } else if (prop === 'length') { + self.#dirtyCollection(); + } + + return true; + }, + + getPrototypeOf() { + return TrackedArray.prototype; + }, + }) as TrackedArray; + } + + #collection = createUpdatableTag(); + + #storages = new Map>(); + + #readStorageFor(index: number) { + let storage = this.#storages.get(index); + + if (storage === undefined) { + storage = createUpdatableTag(); + this.#storages.set(index, storage); + } + + consumeTag(storage); + } + + #dirtyStorageFor(index: number): void { + const storage = this.#storages.get(index); + + if (storage) { + DIRTY_TAG(storage); + } + } + + #dirtyCollection() { + DIRTY_TAG(this.#collection); + this.#storages.clear(); + } +} + +// This rule is correct in the general case, but it doesn't understand +// declaration merging, which is how we're using the interface here. This says +// `TrackedArray` acts just like `Array`, but also has the properties +// declared via the `class` declaration above -- but without the cost of a +// subclass, which is much slower that the proxied array behavior. That is: a +// `TrackedArray` *is* an `Array`, just with a proxy in front of accessors and +// setters, rather than a subclass of an `Array` which would be de-optimized by +// the browsers. +// + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface TrackedArray extends Array {} + +// Ensure instanceof works correctly +Object.setPrototypeOf(TrackedArray.prototype, Array.prototype); From 98ebb510448d12b8e5702585aa34770c057ce7b9 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 16 Feb 2025 14:08:55 -0500 Subject: [PATCH 2/4] Progress on rendering tests -- unit tests were too easy -- rendering tests require a lot of adapting, becasue we can't use @ember/test-helpers --- .../test/-helpers/collection-reactivity.ts | 104 ++++++ .../test/-helpers/reactivity.ts | 42 +++ .../test/collections/array-test.ts | 238 +++++++++++++ packages/@glimmer/validator/package.json | 1 + .../validator/test/collections/array-test.ts | 330 ++++++++++++++++++ pnpm-lock.yaml | 172 ++------- 6 files changed, 745 insertions(+), 142 deletions(-) create mode 100644 packages/@glimmer-workspace/integration-tests/test/-helpers/collection-reactivity.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/-helpers/reactivity.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts create mode 100644 packages/@glimmer/validator/test/collections/array-test.ts diff --git a/packages/@glimmer-workspace/integration-tests/test/-helpers/collection-reactivity.ts b/packages/@glimmer-workspace/integration-tests/test/-helpers/collection-reactivity.ts new file mode 100644 index 000000000..b79f126e1 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/-helpers/collection-reactivity.ts @@ -0,0 +1,104 @@ +/* eslint-disable @typescript-eslint/no-this-alias */ +const test = QUnit.test; + +function compareResults(assert, items) { + findAll('.test-item').forEach((el, index) => { + let key = Array.isArray(items[index]) ? items[index][0] : index; + let value = Array.isArray(items[index]) ? items[index][1] : items[index]; + + assert.equal(el.innerText, `${key}.${value}`); + }); +} + +export function eachReactivityTest(desc, Klass) { + test(`${desc} #each reactivity`, async function (assert) { + let instance; + + class TestComponent extends Klass { + constructor() { + super(...arguments); + instance = this; + } + + get collection() { + throw new Error('did you forget to specify a collection?'); + } + } + + setComponentTemplate( + hbs` +
    + {{#each this.collection as |value index|}} +
  • {{index}}.{{value}}
  • + {{/each}} +
+ `, + TestComponent + ); + this.owner.register('component:test-component', TestComponent); + + await render(hbs``); + + compareResults( + assert, + Array.from(instance.collection).map((v, i) => [i, v]) + ); + + instance.update(); + + await settled(); + + compareResults( + assert, + Array.from(instance.collection).map((v, i) => [i, v]) + ); + }); +} + +export function eachInReactivityTest(desc, Klass) { + test(`${desc} #each-in reactivity`, async function (assert) { + let instance; + + class TestComponent extends Klass { + constructor() { + super(...arguments); + instance = this; + } + + get collection() { + throw new Error('did you forget to specify a collection?'); + } + } + + setComponentTemplate( + hbs` +
    + {{#each-in this.collection as |lhs rhs|}} +
  • {{lhs}}.{{rhs}}
  • + {{/each-in}} +
+ `, + TestComponent + ); + + this.owner.register('component:test-component', TestComponent); + + await render(hbs``); + + let { collection } = instance; + + compareResults( + assert, + Symbol.iterator in collection ? Array.from(collection) : Object.entries(collection) + ); + + instance.update(); + + await settled(); + + compareResults( + assert, + Symbol.iterator in collection ? Array.from(collection) : Object.entries(collection) + ); + }); +} diff --git a/packages/@glimmer-workspace/integration-tests/test/-helpers/reactivity.ts b/packages/@glimmer-workspace/integration-tests/test/-helpers/reactivity.ts new file mode 100644 index 000000000..9206714bc --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/-helpers/reactivity.ts @@ -0,0 +1,42 @@ +import type { RenderTest } from '@glimmer-workspace/integration-tests'; +import { defineComponent } from '@glimmer-workspace/integration-tests'; + +const assert = QUnit.assert; + +export function reactivityTest(context: RenderTest, Klass: any, shouldUpdate = true) { + let instance; + let count = 0; + + class TestComponent extends Klass { + constructor(...args: unknown[]) { + super(...args); + // eslint-disable-next-line @typescript-eslint/no-this-alias + instance = this; + } + + get value() { + count++; + + return super.value; + } + } + + let comp = defineComponent({}, `
{{this.value}}
`, { + strictMode: true, + definition: TestComponent, + }); + + context.renderComponent(comp); + + assert.equal(count, 1, 'The count is 1'); + + instance.update(); + + context.rerender(); + + assert.equal( + count, + shouldUpdate ? 2 : 1, + shouldUpdate ? `The count is updated` : `The could should not update` + ); +} diff --git a/packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts b/packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts new file mode 100644 index 000000000..35faa81f7 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts @@ -0,0 +1,238 @@ +import { TrackedArray } from '@glimmer/validator'; +import { + GlimmerishComponent as Component, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +import { reactivityTest } from '../-helpers/reactivity'; + +const ARRAY_GETTER_METHODS = [ + 'concat', + 'entries', + 'every', + 'filter', + 'find', + 'findIndex', + 'flat', + 'flatMap', + 'forEach', + 'includes', + 'indexOf', + 'join', + 'keys', + 'lastIndexOf', + 'map', + 'reduce', + 'reduceRight', + 'slice', + 'some', + 'values', +]; + +const ARRAY_SETTER_METHODS = [ + 'copyWithin', + 'fill', + 'pop', + 'push', + 'reverse', + 'shift', + 'sort', + 'splice', + 'unshift', +]; + +class TrackedArrayTest extends RenderTest { + static suiteName = `TrackedArray (rendering)`; + + @test + 'getting and setting an index'() { + reactivityTest( + this, + class extends Component { + arr = new TrackedArray(['foo']); + + get value() { + return this.arr[0]; + } + + update() { + this.arr[0] = 'bar'; + } + } + ); + } + + @test + 'Can push into a newly created TrackedArray during construction'() { + reactivityTest( + this, + class extends Component { + arr = new TrackedArray(); + + constructor(owner: Owner, args: object) { + super(owner, args); + this.arr.push('hello'); + } + + get value() { + return this.arr[0]; + } + + update() { + this.arr[0] = 'goodbye'; + } + } + ); + } + + @test + 'Can unshift into a newly created TrackedArray during construction'() { + reactivityTest( + this, + class extends Component { + arr = new TrackedArray(); + + constructor(owner: Owner, args: object) { + super(owner, args); + this.arr.unshift('hello'); + } + + get value() { + return this.arr[0]; + } + + update() { + this.arr[0] = 'goodbye'; + } + } + ); + } +} + +jitSuite(TrackedArrayTest); + +// QUnit.module('reactivity', () => { +// eachReactivityTest( +// '{{each}} works with new items', +// class extends Component { +// collection = new TrackedArray([1, 2, 3]); +// +// update() { +// this.collection.push(4); +// } +// } +// ); +// +// eachReactivityTest( +// '{{each}} works when updating old items', +// class extends Component { +// collection = new TrackedArray([1, 2, 3]); +// +// update() { +// this.collection[2] = 5; +// } +// } +// ); +// +// eachInReactivityTest( +// '{{each-in}} works with new items', +// class extends Component { +// collection = new TrackedArray([1, 2, 3]); +// +// update() { +// this.collection.push(4); +// } +// } +// ); +// +// eachInReactivityTest( +// '{{each-in}} works when updating old items', +// class extends Component { +// collection = new TrackedArray([1, 2, 3]); +// +// update() { +// this.collection[2] = 5; +// } +// } +// ); +// +// ARRAY_GETTER_METHODS.forEach((method) => { +// reactivityTest( +// `${method} individual index`, +// class extends Component { +// arr = new TrackedArray(['foo', 'bar']); +// +// get value() { +// // @ts-ignore -- this can't be represented easily in TS, and we +// // don't actually care that it is; we're *just* testing reactivity. +// return this.arr[method](() => { +// /* no op */ +// }); +// } +// +// update() { +// this.arr[0] = 'bar'; +// } +// } +// ); +// +// reactivityTest( +// `${method} collection tag`, +// class extends Component { +// arr = new TrackedArray(['foo', 'bar']); +// +// get value() { +// // @ts-ignore -- this can't be represented easily in TS, and we +// // don't actually care that it is; we're *just* testing reactivity. +// return this.arr[method](() => { +// /* no op */ +// }); +// } +// +// update() { +// this.arr.sort(); +// } +// } +// ); +// }); +// +// ARRAY_SETTER_METHODS.forEach((method) => { +// reactivityTest( +// `${method} individual index`, +// class extends Component { +// arr = new TrackedArray(['foo', 'bar']); +// +// get value() { +// return this.arr[0]; +// } +// +// update() { +// // @ts-ignore -- this can't be represented easily in TS, and we +// // don't actually care that it is; we're *just* testing reactivity. +// this.arr[method](undefined); +// } +// } +// ); +// +// reactivityTest( +// `${method} collection tag`, +// class extends Component { +// arr = new TrackedArray(['foo', 'bar']); +// +// get value() { +// return void this.arr.forEach(() => { +// /* no op */ +// }); +// } +// +// update() { +// // @ts-ignore -- this can't be represented easily in TS, and we +// // don't actually care that it is; we're *just* testing reactivity. +// this.arr[method](undefined); +// } +// } +// ); +// }); +// }); diff --git a/packages/@glimmer/validator/package.json b/packages/@glimmer/validator/package.json index 749fb8c94..0343924ab 100644 --- a/packages/@glimmer/validator/package.json +++ b/packages/@glimmer/validator/package.json @@ -41,6 +41,7 @@ "@glimmer-workspace/env": "workspace:*", "@glimmer/debug-util": "workspace:*", "eslint": "^9.18.0", + "expect-type": "^1.1.0", "publint": "^0.3.2", "rollup": "^4.31.0-0", "typescript": "*" diff --git a/packages/@glimmer/validator/test/collections/array-test.ts b/packages/@glimmer/validator/test/collections/array-test.ts new file mode 100644 index 000000000..9ce223562 --- /dev/null +++ b/packages/@glimmer/validator/test/collections/array-test.ts @@ -0,0 +1,330 @@ +/* eslint-disable import-x/no-extraneous-dependencies */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +import { TrackedArray } from '@glimmer/validator'; +import { expectTypeOf } from 'expect-type'; + +import { module, test } from '../-utils'; + +expectTypeOf().toMatchTypeOf>(); + +module('@glimmer/validator: TrackedArray', () => { + test('Can get values on array directly', (assert) => { + let arr = new TrackedArray(['foo']); + + assert.strictEqual(arr[0], 'foo'); + }); + + test('Can get length on array directly', (assert) => { + let arr = new TrackedArray(['foo']); + + assert.strictEqual(arr.length, 1); + }); + + test('Can set values on array directly', (assert) => { + let arr = new TrackedArray(); + arr[0] = 123; + + assert.strictEqual(arr[0], 123); + }); + + test('Can set length on array directly', (assert) => { + let arr = new TrackedArray(); + arr.length = 123; + + assert.strictEqual(arr.length, 123); + }); + + test('Can clear array by setting length to 0', (assert) => { + let arr = new TrackedArray([123]); + arr.length = 0; + + assert.strictEqual(arr.length, 0); + assert.strictEqual(arr[0], undefined); + }); + + module('methods', () => { + test('isArray', (assert) => { + let arr = new TrackedArray(); + + assert.ok(Array.isArray(arr)); + }); + + test('length', (assert) => { + let arr = new TrackedArray(); + + assert.equal(arr.length, 0); + + arr[100] = 1; + + assert.equal(arr.length, 101); + }); + + test('concat', (assert) => { + let arr = new TrackedArray(); + let arr2 = arr.concat([1], new TrackedArray([2])); + + assert.deepEqual(arr2, [1, 2]); + assert.notOk(arr2 instanceof TrackedArray); + }); + + test('copyWithin', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + arr.copyWithin(1, 0, 1); + + assert.deepEqual(arr, [1, 1, 3]); + }); + + test('entries', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + let iter = arr.entries(); + + assert.deepEqual(iter.next().value, [0, 1]); + assert.deepEqual(iter.next().value, [1, 2]); + assert.deepEqual(iter.next().value, [2, 3]); + assert.true(iter.next().done); + }); + + test('every', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + + assert.ok(arr.every((v) => typeof v === 'number')); + assert.notOk(arr.every((v) => v !== 2)); + }); + + test('fill', (assert) => { + let arr = new TrackedArray(); + arr.length = 100; + arr.fill(123); + + let count = 0; + let isCorrect = true; + + for (let value of arr) { + count++; + isCorrect = isCorrect && value === 123; + } + + assert.equal(count, 100); + assert.ok(isCorrect); + }); + + test('filter', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + let arr2 = arr.filter((v) => v > 1); + + assert.deepEqual(arr2, [2, 3]); + assert.notOk(arr2 instanceof TrackedArray); + }); + + test('find', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + + assert.equal( + arr.find((v) => v > 1), + 2 + ); + }); + + test('findIndex', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + + assert.equal( + arr.findIndex((v) => v > 1), + 1 + ); + }); + + test('flat', (assert) => { + let arr = new TrackedArray([1, 2, [3]]); + + assert.deepEqual(arr.flat(), [1, 2, 3]); + assert.deepEqual(arr, [1, 2, [3]]); + }); + + test('flatMap', (assert) => { + let arr = new TrackedArray([1, 2, [3]]); + + assert.deepEqual( + arr.flatMap((v) => (typeof v === 'number' ? v + 1 : v)), + [2, 3, 3] + ); + assert.deepEqual(arr, [1, 2, [3]]); + }); + + test('forEach', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + + arr.forEach((v, i) => assert.equal(v, i + 1)); + }); + + test('includes', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + + assert.true(arr.includes(1)); + assert.false(arr.includes(5)); + }); + + test('indexOf', (assert) => { + let arr = new TrackedArray([1, 2, 1]); + + assert.equal(arr.indexOf(1), 0); + assert.equal(arr.indexOf(5), -1); + }); + + test('join', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + + assert.equal(arr.join(','), '1,2,3'); + }); + + test('keys', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + let iter = arr.keys(); + + assert.equal(iter.next().value, 0); + assert.equal(iter.next().value, 1); + assert.equal(iter.next().value, 2); + assert.true(iter.next().done); + }); + + test('lastIndexOf', (assert) => { + let arr = new TrackedArray([3, 2, 3]); + + assert.equal(arr.lastIndexOf(3), 2); + assert.equal(arr.lastIndexOf(5), -1); + }); + + test('map', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + let arr2 = arr.map((v) => v + 1); + + assert.deepEqual(arr2, [2, 3, 4]); + assert.notOk(arr2 instanceof TrackedArray); + }); + + test('pop', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + let val = arr.pop(); + + assert.deepEqual(arr, [1, 2]); + assert.equal(val, 3); + }); + + test('push', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + let val = arr.push(4); + + assert.deepEqual(arr, [1, 2, 3, 4]); + assert.equal(val, 4); + }); + + test('reduce', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + + assert.equal( + arr.reduce((s, v) => s + v, ''), + '123' + ); + }); + + test('reduceRight', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + + assert.equal( + arr.reduceRight((s, v) => s + v, ''), + '321' + ); + }); + + test('reverse', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + arr.reverse(); + + assert.deepEqual(arr, [3, 2, 1]); + }); + + test('shift', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + let val = arr.shift(); + + assert.deepEqual(arr, [2, 3]); + assert.equal(val, 1); + }); + + test('slice', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + let arr2 = arr.slice(); + + assert.notEqual(arr, arr2); + assert.notOk(arr2 instanceof TrackedArray); + assert.deepEqual(arr, arr2); + }); + + test('some', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + + assert.ok(arr.some((v) => v > 1)); + assert.notOk(arr.some((v) => v < 1)); + }); + + test('sort', (assert) => { + let arr = new TrackedArray([3, 1, 2]); + let arr2 = arr.sort(); + + assert.equal(arr, arr2); + assert.deepEqual(arr, [1, 2, 3]); + }); + + test('sort (with method)', (assert) => { + let arr = new TrackedArray([3, 1, 2, 2]); + let arr2 = arr.sort((a, b) => { + if (a > b) return -1; + if (a < b) return 1; + return 0; + }); + + assert.equal(arr, arr2); + assert.deepEqual(arr, [3, 2, 2, 1]); + }); + + test('splice', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + let arr2 = arr.splice(1, 1); + + assert.notOk(arr2 instanceof TrackedArray); + assert.deepEqual(arr, [1, 3]); + assert.deepEqual(arr2, [2]); + }); + + test('unshift', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + let val = arr.unshift(0); + + assert.deepEqual(arr, [0, 1, 2, 3]); + assert.equal(val, 4); + }); + + test('values', (assert) => { + let arr = new TrackedArray([1, 2, 3]); + let iter = arr.values(); + + assert.equal(iter.next().value, 1); + assert.equal(iter.next().value, 2); + assert.equal(iter.next().value, 3); + assert.true(iter.next().done); + }); + + test('of', (assert) => { + let arr = TrackedArray.of(1, 2, 3); + + assert.deepEqual(arr, [1, 2, 3]); + }); + + test('from', (assert) => { + let arr = TrackedArray.from([1, 2, 3]); + + assert.deepEqual(arr, [1, 2, 3]); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c839eed15..d3bff9d06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,7 +73,7 @@ importers: version: 2.0.3 '@pnpm/workspace.find-packages': specifier: ^1000.0.3 - version: 1000.0.3(@pnpm/logger@1000.0.0) + version: 1000.0.3(@pnpm/logger@5.2.0) '@rollup/plugin-sucrase': specifier: ^5.0.2 version: 5.0.2(rollup@4.31.0-0) @@ -317,7 +317,7 @@ importers: version: 1000.1.0 '@pnpm/workspace.find-packages': specifier: ^1000.0.1 - version: 1000.0.5(@pnpm/logger@5.2.0) + version: 1000.0.5(@pnpm/logger@1000.0.0) '@types/node': specifier: ^20.17.10 version: 20.17.10 @@ -1560,6 +1560,9 @@ importers: eslint: specifier: ^9.18.0 version: 9.18.0(jiti@2.4.2) + expect-type: + specifier: ^1.1.0 + version: 1.1.0 publint: specifier: ^0.3.2 version: 0.3.2 @@ -12677,15 +12680,15 @@ snapshots: '@pnpm/types': 12.2.0 load-json-file: 6.2.0 - '@pnpm/cli-utils@1000.0.3(@pnpm/logger@1000.0.0)': + '@pnpm/cli-utils@1000.0.3(@pnpm/logger@5.2.0)': dependencies: '@pnpm/cli-meta': 1000.0.0 - '@pnpm/config': 1002.1.0(@pnpm/logger@1000.0.0) - '@pnpm/default-reporter': 1001.1.1(@pnpm/logger@1000.0.0) + '@pnpm/config': 1002.1.0(@pnpm/logger@5.2.0) + '@pnpm/default-reporter': 1001.1.1(@pnpm/logger@5.2.0) '@pnpm/error': 1000.0.1 - '@pnpm/logger': 1000.0.0 - '@pnpm/manifest-utils': 1000.0.2(@pnpm/logger@1000.0.0) - '@pnpm/package-is-installable': 1000.0.2(@pnpm/logger@1000.0.0) + '@pnpm/logger': 5.2.0 + '@pnpm/manifest-utils': 1000.0.2(@pnpm/logger@5.2.0) + '@pnpm/package-is-installable': 1000.0.2(@pnpm/logger@5.2.0) '@pnpm/read-project-manifest': 1000.0.1 '@pnpm/types': 1000.0.0 chalk: 4.1.2 @@ -12705,20 +12708,6 @@ snapshots: chalk: 4.1.2 load-json-file: 6.2.0 - '@pnpm/cli-utils@1000.0.5(@pnpm/logger@5.2.0)': - dependencies: - '@pnpm/cli-meta': 1000.0.1 - '@pnpm/config': 1002.1.2(@pnpm/logger@5.2.0) - '@pnpm/default-reporter': 1001.1.3(@pnpm/logger@5.2.0) - '@pnpm/error': 1000.0.1 - '@pnpm/logger': 5.2.0 - '@pnpm/manifest-utils': 1000.0.3(@pnpm/logger@5.2.0) - '@pnpm/package-is-installable': 1000.0.3(@pnpm/logger@5.2.0) - '@pnpm/read-project-manifest': 1000.0.3 - '@pnpm/types': 1000.1.0 - chalk: 4.1.2 - load-json-file: 6.2.0 - '@pnpm/cli-utils@4.0.9(@pnpm/logger@5.2.0)': dependencies: '@pnpm/cli-meta': 6.2.2 @@ -12737,7 +12726,7 @@ snapshots: '@pnpm/config.env-replace@3.0.0': {} - '@pnpm/config@1002.1.0(@pnpm/logger@1000.0.0)': + '@pnpm/config@1002.1.0(@pnpm/logger@5.2.0)': dependencies: '@pnpm/catalogs.config': 1000.0.1 '@pnpm/catalogs.types': 1000.0.0 @@ -12747,7 +12736,7 @@ snapshots: '@pnpm/git-utils': 1000.0.0 '@pnpm/matcher': 1000.0.0 '@pnpm/npm-conf': 2.3.1 - '@pnpm/pnpmfile': 1001.0.1(@pnpm/logger@1000.0.0) + '@pnpm/pnpmfile': 1001.0.1(@pnpm/logger@5.2.0) '@pnpm/read-project-manifest': 1000.0.1 '@pnpm/types': 1000.0.0 '@pnpm/workspace.read-manifest': 1000.0.1 @@ -12797,36 +12786,6 @@ snapshots: transitivePeerDependencies: - '@pnpm/logger' - '@pnpm/config@1002.1.2(@pnpm/logger@5.2.0)': - dependencies: - '@pnpm/catalogs.config': 1000.0.1 - '@pnpm/catalogs.types': 1000.0.0 - '@pnpm/config.env-replace': 3.0.0 - '@pnpm/constants': 1001.0.0 - '@pnpm/error': 1000.0.1 - '@pnpm/git-utils': 1000.0.0 - '@pnpm/matcher': 1000.0.0 - '@pnpm/npm-conf': 3.0.0 - '@pnpm/pnpmfile': 1001.0.3(@pnpm/logger@5.2.0) - '@pnpm/read-project-manifest': 1000.0.3 - '@pnpm/types': 1000.1.0 - '@pnpm/workspace.read-manifest': 1000.0.1 - better-path-resolve: 1.0.0 - camelcase: 6.3.0 - camelcase-keys: 6.2.2 - can-write-to-dir: 1.1.1 - is-subdir: 1.2.0 - is-windows: 1.0.2 - normalize-registry-url: 2.0.0 - path-absolute: 1.0.1 - path-name: 1.0.0 - ramda: '@pnpm/ramda@0.28.1' - read-ini-file: 4.0.0 - realpath-missing: 1.1.0 - which: '@pnpm/which@3.0.1' - transitivePeerDependencies: - - '@pnpm/logger' - '@pnpm/config@21.8.6(@pnpm/logger@5.2.0)': dependencies: '@pnpm/catalogs.config': 0.1.1 @@ -12868,9 +12827,9 @@ snapshots: '@pnpm/logger': 5.2.0 '@pnpm/types': 12.2.0 - '@pnpm/core-loggers@1000.1.0(@pnpm/logger@1000.0.0)': + '@pnpm/core-loggers@1000.1.0(@pnpm/logger@5.2.0)': dependencies: - '@pnpm/logger': 1000.0.0 + '@pnpm/logger': 5.2.0 '@pnpm/types': 1000.0.0 '@pnpm/core-loggers@1000.1.1(@pnpm/logger@1000.0.0)': @@ -12878,11 +12837,6 @@ snapshots: '@pnpm/logger': 1000.0.0 '@pnpm/types': 1000.1.0 - '@pnpm/core-loggers@1000.1.1(@pnpm/logger@5.2.0)': - dependencies: - '@pnpm/logger': 5.2.0 - '@pnpm/types': 1000.1.0 - '@pnpm/crypto.base32-hash@3.0.1': dependencies: '@pnpm/crypto.polyfill': 1.0.0 @@ -12912,15 +12866,15 @@ snapshots: '@pnpm/dedupe.types@2.0.0': {} - '@pnpm/default-reporter@1001.1.1(@pnpm/logger@1000.0.0)': + '@pnpm/default-reporter@1001.1.1(@pnpm/logger@5.2.0)': dependencies: '@pnpm/cli-meta': 1000.0.0 - '@pnpm/config': 1002.1.0(@pnpm/logger@1000.0.0) - '@pnpm/core-loggers': 1000.1.0(@pnpm/logger@1000.0.0) + '@pnpm/config': 1002.1.0(@pnpm/logger@5.2.0) + '@pnpm/core-loggers': 1000.1.0(@pnpm/logger@5.2.0) '@pnpm/dedupe.issues-renderer': 1000.0.0 '@pnpm/dedupe.types': 1000.0.0 '@pnpm/error': 1000.0.1 - '@pnpm/logger': 1000.0.0 + '@pnpm/logger': 5.2.0 '@pnpm/render-peer-issues': 1000.0.1 '@pnpm/types': 1000.0.0 ansi-diff: 1.2.0 @@ -12960,30 +12914,6 @@ snapshots: stacktracey: 2.1.8 string-length: 4.0.2 - '@pnpm/default-reporter@1001.1.3(@pnpm/logger@5.2.0)': - dependencies: - '@pnpm/cli-meta': 1000.0.1 - '@pnpm/config': 1002.1.2(@pnpm/logger@5.2.0) - '@pnpm/core-loggers': 1000.1.1(@pnpm/logger@5.2.0) - '@pnpm/dedupe.issues-renderer': 1000.0.0 - '@pnpm/dedupe.types': 1000.0.0 - '@pnpm/error': 1000.0.1 - '@pnpm/logger': 5.2.0 - '@pnpm/render-peer-issues': 1000.0.2 - '@pnpm/types': 1000.1.0 - ansi-diff: 1.2.0 - boxen: 5.1.2 - chalk: 4.1.2 - cli-truncate: 2.1.0 - normalize-path: 3.0.0 - pretty-bytes: 5.6.0 - pretty-ms: 7.0.1 - ramda: '@pnpm/ramda@0.28.1' - rxjs: 7.8.1 - semver: 7.6.3 - stacktracey: 2.1.8 - string-length: 4.0.2 - '@pnpm/default-reporter@14.0.6(@pnpm/logger@5.2.0)': dependencies: '@pnpm/cli-meta': 6.2.2 @@ -13146,9 +13076,9 @@ snapshots: bole: 5.0.17 ndjson: 2.0.0 - '@pnpm/manifest-utils@1000.0.2(@pnpm/logger@1000.0.0)': + '@pnpm/manifest-utils@1000.0.2(@pnpm/logger@5.2.0)': dependencies: - '@pnpm/core-loggers': 1000.1.0(@pnpm/logger@1000.0.0) + '@pnpm/core-loggers': 1000.1.0(@pnpm/logger@5.2.0) '@pnpm/error': 1000.0.1 '@pnpm/types': 1000.0.0 transitivePeerDependencies: @@ -13162,14 +13092,6 @@ snapshots: transitivePeerDependencies: - '@pnpm/logger' - '@pnpm/manifest-utils@1000.0.3(@pnpm/logger@5.2.0)': - dependencies: - '@pnpm/core-loggers': 1000.1.1(@pnpm/logger@5.2.0) - '@pnpm/error': 1000.0.1 - '@pnpm/types': 1000.1.0 - transitivePeerDependencies: - - '@pnpm/logger' - '@pnpm/manifest-utils@6.0.9(@pnpm/logger@5.2.0)': dependencies: '@pnpm/core-loggers': 10.0.7(@pnpm/logger@5.2.0) @@ -13215,13 +13137,13 @@ snapshots: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 - '@pnpm/package-is-installable@1000.0.2(@pnpm/logger@1000.0.0)': + '@pnpm/package-is-installable@1000.0.2(@pnpm/logger@5.2.0)': dependencies: '@pnpm/cli-meta': 1000.0.0 - '@pnpm/core-loggers': 1000.1.0(@pnpm/logger@1000.0.0) + '@pnpm/core-loggers': 1000.1.0(@pnpm/logger@5.2.0) '@pnpm/env.system-node-version': 1000.0.0 '@pnpm/error': 1000.0.1 - '@pnpm/logger': 1000.0.0 + '@pnpm/logger': 5.2.0 '@pnpm/types': 1000.0.0 detect-libc: 2.0.3 execa: safe-execa@0.1.2 @@ -13241,19 +13163,6 @@ snapshots: mem: 8.1.1 semver: 7.6.3 - '@pnpm/package-is-installable@1000.0.3(@pnpm/logger@5.2.0)': - dependencies: - '@pnpm/cli-meta': 1000.0.1 - '@pnpm/core-loggers': 1000.1.1(@pnpm/logger@5.2.0) - '@pnpm/env.system-node-version': 1000.0.1 - '@pnpm/error': 1000.0.1 - '@pnpm/logger': 5.2.0 - '@pnpm/types': 1000.1.0 - detect-libc: 2.0.3 - execa: safe-execa@0.1.2 - mem: 8.1.1 - semver: 7.6.3 - '@pnpm/package-is-installable@9.0.12(@pnpm/logger@5.2.0)': dependencies: '@pnpm/cli-meta': 6.2.2 @@ -13293,14 +13202,14 @@ snapshots: '@pnpm/patching.types@1000.0.0': {} - '@pnpm/pnpmfile@1001.0.1(@pnpm/logger@1000.0.0)': + '@pnpm/pnpmfile@1001.0.1(@pnpm/logger@5.2.0)': dependencies: - '@pnpm/core-loggers': 1000.1.0(@pnpm/logger@1000.0.0) + '@pnpm/core-loggers': 1000.1.0(@pnpm/logger@5.2.0) '@pnpm/crypto.hash': 1000.0.0 '@pnpm/error': 1000.0.1 '@pnpm/hooks.types': 1001.0.0 '@pnpm/lockfile.types': 1001.0.0 - '@pnpm/logger': 1000.0.0 + '@pnpm/logger': 5.2.0 '@pnpm/store-controller-types': 1000.1.0 '@pnpm/types': 1000.0.0 chalk: 4.1.2 @@ -13319,19 +13228,6 @@ snapshots: chalk: 4.1.2 path-absolute: 1.0.1 - '@pnpm/pnpmfile@1001.0.3(@pnpm/logger@5.2.0)': - dependencies: - '@pnpm/core-loggers': 1000.1.1(@pnpm/logger@5.2.0) - '@pnpm/crypto.hash': 1000.0.0 - '@pnpm/error': 1000.0.1 - '@pnpm/hooks.types': 1001.0.1 - '@pnpm/lockfile.types': 1001.0.1 - '@pnpm/logger': 5.2.0 - '@pnpm/store-controller-types': 1001.0.0 - '@pnpm/types': 1000.1.0 - chalk: 4.1.2 - path-absolute: 1.0.1 - '@pnpm/pnpmfile@6.0.13(@pnpm/logger@5.2.0)': dependencies: '@pnpm/core-loggers': 10.0.7(@pnpm/logger@5.2.0) @@ -13490,11 +13386,11 @@ snapshots: dependencies: isexe: 2.0.0 - '@pnpm/workspace.find-packages@1000.0.3(@pnpm/logger@1000.0.0)': + '@pnpm/workspace.find-packages@1000.0.3(@pnpm/logger@5.2.0)': dependencies: - '@pnpm/cli-utils': 1000.0.3(@pnpm/logger@1000.0.0) + '@pnpm/cli-utils': 1000.0.3(@pnpm/logger@5.2.0) '@pnpm/fs.find-packages': 1000.0.1 - '@pnpm/logger': 1000.0.0 + '@pnpm/logger': 5.2.0 '@pnpm/types': 1000.0.0 '@pnpm/util.lex-comparator': 3.0.0 @@ -13506,14 +13402,6 @@ snapshots: '@pnpm/types': 1000.1.0 '@pnpm/util.lex-comparator': 3.0.0 - '@pnpm/workspace.find-packages@1000.0.5(@pnpm/logger@5.2.0)': - dependencies: - '@pnpm/cli-utils': 1000.0.5(@pnpm/logger@5.2.0) - '@pnpm/fs.find-packages': 1000.0.3 - '@pnpm/logger': 5.2.0 - '@pnpm/types': 1000.1.0 - '@pnpm/util.lex-comparator': 3.0.0 - '@pnpm/workspace.find-packages@4.0.14(@pnpm/logger@5.2.0)': dependencies: '@pnpm/cli-utils': 4.0.9(@pnpm/logger@5.2.0) From 5b5b3e1d1d0899b7e80edcc3aa4e4366ad432fb6 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 16 Feb 2025 14:57:10 -0500 Subject: [PATCH 3/4] All tests ported --- .../integration-tests/lib/render-test.ts | 146 +++++++++ .../test/-helpers/collection-reactivity.ts | 104 ------- .../test/-helpers/reactivity.ts | 42 --- .../test/collections/array-test.ts | 284 +++++++++--------- 4 files changed, 294 insertions(+), 282 deletions(-) delete mode 100644 packages/@glimmer-workspace/integration-tests/test/-helpers/collection-reactivity.ts delete mode 100644 packages/@glimmer-workspace/integration-tests/test/-helpers/reactivity.ts diff --git a/packages/@glimmer-workspace/integration-tests/lib/render-test.ts b/packages/@glimmer-workspace/integration-tests/lib/render-test.ts index 7cdb99ffa..175d1bf28 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/render-test.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/render-test.ts @@ -27,6 +27,7 @@ import { CURLY_TEST_COMPONENT, GLIMMER_TEST_COMPONENT } from './components'; import { assertElementShape, assertEmberishElement } from './dom/assertions'; import { assertingElement, toInnerHTML } from './dom/simple-utils'; import { equalTokens, isServerMarker, normalizeSnapshot } from './snapshot'; +import { defineComponent } from './test-helpers/define'; type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; type Present = Exclude; @@ -430,6 +431,151 @@ export class RenderTest implements IRenderTest { inTransaction(result.env, () => destroy(result)); } + private assertEachCompareResults(items) { + [...(this.element as unknown as HTMLElement).querySelectorAll('.test-item')].forEach( + (el, index) => { + let key = Array.isArray(items[index]) ? items[index][0] : index; + let value = Array.isArray(items[index]) ? items[index][1] : items[index]; + + QUnit.assert.equal(el.innerText, `${key}.${value}`); + } + ); + } + + protected assertReactivity( + Klass: new (...args: any[]) => { get value(): T; update: () => void }, + shouldUpdate = true, + message?: string + ) { + let instance; + let count = 0; + + class TestComponent extends Klass { + constructor(...args: unknown[]) { + super(...args); + // eslint-disable-next-line @typescript-eslint/no-this-alias + instance = this; + } + + override get value() { + count++; + + return super.value; + } + } + + if (message) { + QUnit.assert.ok(true, message); + } + + let comp = defineComponent({}, `
{{this.value}}
`, { + strictMode: true, + definition: TestComponent, + }); + + this.renderComponent(comp); + + QUnit.assert.equal(count, 1, `The count is 1`); + + instance.update(); + + this.rerender(); + + QUnit.assert.equal( + count, + shouldUpdate ? 2 : 1, + shouldUpdate ? `The count is updated` : `The could should not update` + ); + + this.assertStableRerender(); + } + + protected assertEachInReactivity( + Klass: new (...args: any[]) => { collection: number[]; update: () => void } + ) { + let instance: TestComponent; + + class TestComponent extends Klass { + constructor(...args) { + super(...args); + // eslint-disable-next-line + instance = this; + } + } + + let comp = defineComponent( + {}, + ` +
    + {{#each-in this.collection as |lhs rhs|}} +
  • {{lhs}}.{{rhs}}
  • + {{/each-in}} +
+`, + { + strictMode: true, + definition: TestComponent, + } + ); + + this.renderComponent(comp); + + let { collection } = instance; + + this.assertEachCompareResults( + Symbol.iterator in collection ? Array.from(collection) : Object.entries(collection) + ); + + instance.update(); + + this.rerender(); + + this.assertEachCompareResults( + Symbol.iterator in collection ? Array.from(collection) : Object.entries(collection) + ); + } + + protected assertEachReactivity( + Klass: new (...args: any[]) => { collection: number[]; update: () => void } + ) { + let instance: TestComponent; + + class TestComponent extends Klass { + constructor(...args: any[]) { + super(...args); + // eslint-disable-next-line + instance = this; + } + } + + let comp = defineComponent( + {}, + ` +
    + {{#each this.collection as |value index|}} +
  • {{index}}.{{value}}
  • + {{/each}} +
+`, + { + strictMode: true, + definition: TestComponent, + } + ); + + this.renderComponent(comp); + + this.assertEachCompareResults(Array.from(instance.collection).map((v, i) => [i, v])); + + instance.update(); + + this.rerender(); + + this.assertEachCompareResults(Array.from(instance.collection).map((v, i) => [i, v])); + + this.assertStableRerender(); + } + protected set(key: string, value: unknown): void { this.context[key] = value; dirtyTagFor(this.context, key); diff --git a/packages/@glimmer-workspace/integration-tests/test/-helpers/collection-reactivity.ts b/packages/@glimmer-workspace/integration-tests/test/-helpers/collection-reactivity.ts deleted file mode 100644 index b79f126e1..000000000 --- a/packages/@glimmer-workspace/integration-tests/test/-helpers/collection-reactivity.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* eslint-disable @typescript-eslint/no-this-alias */ -const test = QUnit.test; - -function compareResults(assert, items) { - findAll('.test-item').forEach((el, index) => { - let key = Array.isArray(items[index]) ? items[index][0] : index; - let value = Array.isArray(items[index]) ? items[index][1] : items[index]; - - assert.equal(el.innerText, `${key}.${value}`); - }); -} - -export function eachReactivityTest(desc, Klass) { - test(`${desc} #each reactivity`, async function (assert) { - let instance; - - class TestComponent extends Klass { - constructor() { - super(...arguments); - instance = this; - } - - get collection() { - throw new Error('did you forget to specify a collection?'); - } - } - - setComponentTemplate( - hbs` -
    - {{#each this.collection as |value index|}} -
  • {{index}}.{{value}}
  • - {{/each}} -
- `, - TestComponent - ); - this.owner.register('component:test-component', TestComponent); - - await render(hbs``); - - compareResults( - assert, - Array.from(instance.collection).map((v, i) => [i, v]) - ); - - instance.update(); - - await settled(); - - compareResults( - assert, - Array.from(instance.collection).map((v, i) => [i, v]) - ); - }); -} - -export function eachInReactivityTest(desc, Klass) { - test(`${desc} #each-in reactivity`, async function (assert) { - let instance; - - class TestComponent extends Klass { - constructor() { - super(...arguments); - instance = this; - } - - get collection() { - throw new Error('did you forget to specify a collection?'); - } - } - - setComponentTemplate( - hbs` -
    - {{#each-in this.collection as |lhs rhs|}} -
  • {{lhs}}.{{rhs}}
  • - {{/each-in}} -
- `, - TestComponent - ); - - this.owner.register('component:test-component', TestComponent); - - await render(hbs``); - - let { collection } = instance; - - compareResults( - assert, - Symbol.iterator in collection ? Array.from(collection) : Object.entries(collection) - ); - - instance.update(); - - await settled(); - - compareResults( - assert, - Symbol.iterator in collection ? Array.from(collection) : Object.entries(collection) - ); - }); -} diff --git a/packages/@glimmer-workspace/integration-tests/test/-helpers/reactivity.ts b/packages/@glimmer-workspace/integration-tests/test/-helpers/reactivity.ts deleted file mode 100644 index 9206714bc..000000000 --- a/packages/@glimmer-workspace/integration-tests/test/-helpers/reactivity.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { RenderTest } from '@glimmer-workspace/integration-tests'; -import { defineComponent } from '@glimmer-workspace/integration-tests'; - -const assert = QUnit.assert; - -export function reactivityTest(context: RenderTest, Klass: any, shouldUpdate = true) { - let instance; - let count = 0; - - class TestComponent extends Klass { - constructor(...args: unknown[]) { - super(...args); - // eslint-disable-next-line @typescript-eslint/no-this-alias - instance = this; - } - - get value() { - count++; - - return super.value; - } - } - - let comp = defineComponent({}, `
{{this.value}}
`, { - strictMode: true, - definition: TestComponent, - }); - - context.renderComponent(comp); - - assert.equal(count, 1, 'The count is 1'); - - instance.update(); - - context.rerender(); - - assert.equal( - count, - shouldUpdate ? 2 : 1, - shouldUpdate ? `The count is updated` : `The could should not update` - ); -} diff --git a/packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts b/packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts index 35faa81f7..f464e3bf3 100644 --- a/packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/collections/array-test.ts @@ -6,8 +6,6 @@ import { test, } from '@glimmer-workspace/integration-tests'; -import { reactivityTest } from '../-helpers/reactivity'; - const ARRAY_GETTER_METHODS = [ 'concat', 'entries', @@ -48,8 +46,7 @@ class TrackedArrayTest extends RenderTest { @test 'getting and setting an index'() { - reactivityTest( - this, + this.assertReactivity( class extends Component { arr = new TrackedArray(['foo']); @@ -66,13 +63,12 @@ class TrackedArrayTest extends RenderTest { @test 'Can push into a newly created TrackedArray during construction'() { - reactivityTest( - this, + this.assertReactivity( class extends Component { arr = new TrackedArray(); - constructor(owner: Owner, args: object) { - super(owner, args); + constructor(...args: unknown[]) { + super(...args); this.arr.push('hello'); } @@ -89,13 +85,12 @@ class TrackedArrayTest extends RenderTest { @test 'Can unshift into a newly created TrackedArray during construction'() { - reactivityTest( - this, + this.assertReactivity( class extends Component { arr = new TrackedArray(); - constructor(owner: Owner, args: object) { - super(owner, args); + constructor(...args: unknown[]) { + super(...args); this.arr.unshift('hello'); } @@ -109,130 +104,147 @@ class TrackedArrayTest extends RenderTest { } ); } + + @test + '{{each}} works with new items'() { + this.assertEachReactivity( + class extends Component { + collection = new TrackedArray([1, 2, 3]); + + update() { + this.collection.push(4); + } + } + ); + } + + @test + '{{each}} works when updating old items'() { + this.assertEachReactivity( + class extends Component { + collection = new TrackedArray([1, 2, 3]); + + update() { + this.collection[2] = 5; + } + } + ); + } + + @test({ skip: true }) + '{{each-in}} works with new items'() { + this.assertEachInReactivity( + class extends Component { + collection = new TrackedArray([1, 2, 3]); + + update() { + this.collection.push(4); + } + } + ); + } + + @test({ skip: true }) + '{{each-in}} works when updating old items'() { + this.assertEachInReactivity( + class extends Component { + collection = new TrackedArray([1, 2, 3]); + + update() { + this.collection[2] = 5; + } + } + ); + } + + @test + ARRAY_GETTER_METHODS() { + ARRAY_GETTER_METHODS.forEach((method) => { + this.assertReactivity( + class extends Component { + arr = new TrackedArray(['foo', 'bar']); + + get value() { + // @ts-expect-error -- this can't be represented easily in TS, and we + // don't actually care that it is; we're *just* testing reactivity. + return this.arr[method](() => { + /* no op */ + }); + } + + update() { + this.arr[0] = 'bar'; + } + }, + true, + `[[${method} individual index]]` + ); + + this.assertReactivity( + class extends Component { + arr = new TrackedArray(['foo', 'bar']); + + get value() { + // @ts-expect-error -- this can't be represented easily in TS, and we + // don't actually care that it is; we're *just* testing reactivity. + return this.arr[method](() => { + /* no op */ + }); + } + + update() { + this.arr.sort(); + } + }, + true, + `[[${method} collection tag]]` + ); + }); + } + + @test + ARRAY_SETTER_METHODS() { + ARRAY_SETTER_METHODS.forEach((method) => { + this.assertReactivity( + class extends Component { + arr = new TrackedArray(['foo', 'bar']); + + get value() { + return this.arr[0]; + } + + update() { + // @ts-expect-error -- this can't be represented easily in TS, and we + // don't actually care that it is; we're *just* testing reactivity. + this.arr[method](undefined); + } + }, + true, + `[[${method} individual index]]` + ); + + this.assertReactivity( + class extends Component { + arr = new TrackedArray(['foo', 'bar']); + + get value() { + return void this.arr.forEach(() => { + /* no op */ + }); + } + + update() { + // @ts-expect-error -- this can't be represented easily in TS, and we + // don't actually care that it is; we're *just* testing reactivity. + this.arr[method](undefined); + } + }, + true, + + `[[${method} collection tag]]` + ); + }); + } } jitSuite(TrackedArrayTest); - -// QUnit.module('reactivity', () => { -// eachReactivityTest( -// '{{each}} works with new items', -// class extends Component { -// collection = new TrackedArray([1, 2, 3]); -// -// update() { -// this.collection.push(4); -// } -// } -// ); -// -// eachReactivityTest( -// '{{each}} works when updating old items', -// class extends Component { -// collection = new TrackedArray([1, 2, 3]); -// -// update() { -// this.collection[2] = 5; -// } -// } -// ); -// -// eachInReactivityTest( -// '{{each-in}} works with new items', -// class extends Component { -// collection = new TrackedArray([1, 2, 3]); -// -// update() { -// this.collection.push(4); -// } -// } -// ); -// -// eachInReactivityTest( -// '{{each-in}} works when updating old items', -// class extends Component { -// collection = new TrackedArray([1, 2, 3]); -// -// update() { -// this.collection[2] = 5; -// } -// } -// ); -// -// ARRAY_GETTER_METHODS.forEach((method) => { -// reactivityTest( -// `${method} individual index`, -// class extends Component { -// arr = new TrackedArray(['foo', 'bar']); -// -// get value() { -// // @ts-ignore -- this can't be represented easily in TS, and we -// // don't actually care that it is; we're *just* testing reactivity. -// return this.arr[method](() => { -// /* no op */ -// }); -// } -// -// update() { -// this.arr[0] = 'bar'; -// } -// } -// ); -// -// reactivityTest( -// `${method} collection tag`, -// class extends Component { -// arr = new TrackedArray(['foo', 'bar']); -// -// get value() { -// // @ts-ignore -- this can't be represented easily in TS, and we -// // don't actually care that it is; we're *just* testing reactivity. -// return this.arr[method](() => { -// /* no op */ -// }); -// } -// -// update() { -// this.arr.sort(); -// } -// } -// ); -// }); -// -// ARRAY_SETTER_METHODS.forEach((method) => { -// reactivityTest( -// `${method} individual index`, -// class extends Component { -// arr = new TrackedArray(['foo', 'bar']); -// -// get value() { -// return this.arr[0]; -// } -// -// update() { -// // @ts-ignore -- this can't be represented easily in TS, and we -// // don't actually care that it is; we're *just* testing reactivity. -// this.arr[method](undefined); -// } -// } -// ); -// -// reactivityTest( -// `${method} collection tag`, -// class extends Component { -// arr = new TrackedArray(['foo', 'bar']); -// -// get value() { -// return void this.arr.forEach(() => { -// /* no op */ -// }); -// } -// -// update() { -// // @ts-ignore -- this can't be represented easily in TS, and we -// // don't actually care that it is; we're *just* testing reactivity. -// this.arr[method](undefined); -// } -// } -// ); -// }); -// }); From 3dc051f4d2de0126e8c42ee7d5c828107f849bd8 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 16 Feb 2025 15:11:22 -0500 Subject: [PATCH 4/4] Lint:fix --- .../validator/test/collections/array-test.ts | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/@glimmer/validator/test/collections/array-test.ts b/packages/@glimmer/validator/test/collections/array-test.ts index 9ce223562..ebdf8e275 100644 --- a/packages/@glimmer/validator/test/collections/array-test.ts +++ b/packages/@glimmer/validator/test/collections/array-test.ts @@ -1,6 +1,5 @@ /* eslint-disable import-x/no-extraneous-dependencies */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ + import { TrackedArray } from '@glimmer/validator'; import { expectTypeOf } from 'expect-type'; @@ -53,11 +52,11 @@ module('@glimmer/validator: TrackedArray', () => { test('length', (assert) => { let arr = new TrackedArray(); - assert.equal(arr.length, 0); + assert.strictEqual(arr.length, 0); arr[100] = 1; - assert.equal(arr.length, 101); + assert.strictEqual(arr.length, 101); }); test('concat', (assert) => { @@ -105,7 +104,7 @@ module('@glimmer/validator: TrackedArray', () => { isCorrect = isCorrect && value === 123; } - assert.equal(count, 100); + assert.strictEqual(count, 100); assert.ok(isCorrect); }); @@ -120,7 +119,7 @@ module('@glimmer/validator: TrackedArray', () => { test('find', (assert) => { let arr = new TrackedArray([1, 2, 3]); - assert.equal( + assert.strictEqual( arr.find((v) => v > 1), 2 ); @@ -129,7 +128,7 @@ module('@glimmer/validator: TrackedArray', () => { test('findIndex', (assert) => { let arr = new TrackedArray([1, 2, 3]); - assert.equal( + assert.strictEqual( arr.findIndex((v) => v > 1), 1 ); @@ -155,7 +154,7 @@ module('@glimmer/validator: TrackedArray', () => { test('forEach', (assert) => { let arr = new TrackedArray([1, 2, 3]); - arr.forEach((v, i) => assert.equal(v, i + 1)); + arr.forEach((v, i) => assert.strictEqual(v, i + 1)); }); test('includes', (assert) => { @@ -168,31 +167,31 @@ module('@glimmer/validator: TrackedArray', () => { test('indexOf', (assert) => { let arr = new TrackedArray([1, 2, 1]); - assert.equal(arr.indexOf(1), 0); - assert.equal(arr.indexOf(5), -1); + assert.strictEqual(arr.indexOf(1), 0); + assert.strictEqual(arr.indexOf(5), -1); }); test('join', (assert) => { let arr = new TrackedArray([1, 2, 3]); - assert.equal(arr.join(','), '1,2,3'); + assert.strictEqual(arr.join(','), '1,2,3'); }); test('keys', (assert) => { let arr = new TrackedArray([1, 2, 3]); let iter = arr.keys(); - assert.equal(iter.next().value, 0); - assert.equal(iter.next().value, 1); - assert.equal(iter.next().value, 2); + assert.strictEqual(iter.next().value, 0); + assert.strictEqual(iter.next().value, 1); + assert.strictEqual(iter.next().value, 2); assert.true(iter.next().done); }); test('lastIndexOf', (assert) => { let arr = new TrackedArray([3, 2, 3]); - assert.equal(arr.lastIndexOf(3), 2); - assert.equal(arr.lastIndexOf(5), -1); + assert.strictEqual(arr.lastIndexOf(3), 2); + assert.strictEqual(arr.lastIndexOf(5), -1); }); test('map', (assert) => { @@ -208,7 +207,7 @@ module('@glimmer/validator: TrackedArray', () => { let val = arr.pop(); assert.deepEqual(arr, [1, 2]); - assert.equal(val, 3); + assert.strictEqual(val, 3); }); test('push', (assert) => { @@ -216,13 +215,14 @@ module('@glimmer/validator: TrackedArray', () => { let val = arr.push(4); assert.deepEqual(arr, [1, 2, 3, 4]); - assert.equal(val, 4); + assert.strictEqual(val, 4); }); test('reduce', (assert) => { let arr = new TrackedArray([1, 2, 3]); - assert.equal( + assert.strictEqual( + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands arr.reduce((s, v) => s + v, ''), '123' ); @@ -231,7 +231,8 @@ module('@glimmer/validator: TrackedArray', () => { test('reduceRight', (assert) => { let arr = new TrackedArray([1, 2, 3]); - assert.equal( + assert.strictEqual( + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands arr.reduceRight((s, v) => s + v, ''), '321' ); @@ -249,7 +250,7 @@ module('@glimmer/validator: TrackedArray', () => { let val = arr.shift(); assert.deepEqual(arr, [2, 3]); - assert.equal(val, 1); + assert.strictEqual(val, 1); }); test('slice', (assert) => { @@ -272,7 +273,7 @@ module('@glimmer/validator: TrackedArray', () => { let arr = new TrackedArray([3, 1, 2]); let arr2 = arr.sort(); - assert.equal(arr, arr2); + assert.strictEqual(arr, arr2); assert.deepEqual(arr, [1, 2, 3]); }); @@ -284,7 +285,7 @@ module('@glimmer/validator: TrackedArray', () => { return 0; }); - assert.equal(arr, arr2); + assert.strictEqual(arr, arr2); assert.deepEqual(arr, [3, 2, 2, 1]); }); @@ -302,16 +303,16 @@ module('@glimmer/validator: TrackedArray', () => { let val = arr.unshift(0); assert.deepEqual(arr, [0, 1, 2, 3]); - assert.equal(val, 4); + assert.strictEqual(val, 4); }); test('values', (assert) => { let arr = new TrackedArray([1, 2, 3]); let iter = arr.values(); - assert.equal(iter.next().value, 1); - assert.equal(iter.next().value, 2); - assert.equal(iter.next().value, 3); + assert.strictEqual(iter.next().value, 1); + assert.strictEqual(iter.next().value, 2); + assert.strictEqual(iter.next().value, 3); assert.true(iter.next().done); });