Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor notification / listeners tests #6297

Merged
merged 10 commits into from
Dec 5, 2023
Merged

Conversation

kraenhansen
Copy link
Member

What, How & Why?

This is a refactor of our listener / notification tests, adding missing coverage for some of our collection types and (hopefully) increasing the readability of the tests in the process.

☑️ ToDos

  • 📝 Changelog entry
  • 📝 Compatibility label is updated or copied from previous entry
  • 📝 Update COMPATIBILITY.md
  • 🚦 Tests
  • 📦 Updated internal package version in consuming package.jsons (if updating internal packages)
  • 📱 Check the React Native/other sample apps work if necessary
  • 💥 Breaking label has been applied or is not necessary

@kraenhansen kraenhansen self-assigned this Dec 4, 2023
declare function setTimeout(cb: (args: any[]) => void, timeout: number): any;
declare type Timer = number;
declare function setTimeout(cb: (args: any[]) => void, timeout: number): Timer;
declare function clearTimeout(timer: Timer): void;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed to expose the clearTimeout to use it in the "listener stub" util.

@kraenhansen kraenhansen changed the title Refactore notification / listeners tests Refactor notification / listeners tests Dec 4, 2023
Copy link

coveralls-official bot commented Dec 4, 2023

Coverage Status

coverage: 86.51% (+0.3%) from 86.182%
when pulling 6327b3d on kh/observable-tests
into ca6abdd on main.

Comment on lines +175 to +176
// Skipping on React Native because the callback is called one too many times
it.skipIf(environment.reactNative, "calls listener", async function (this: RealmContext) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were already skipping this test on React Native:

// TODO: Figure out why the event is fired twice on React Native
it.skipIf(environment.reactNative, "should work for beforenotify", function (this: RealmContext, done) {

I still don't know why this is happening and it would make sense for us to look into it at some point. I've created #6298 to track this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it happens both on Android and iOS? I mean, the event loop is different and I wonder if it is related to core's scheduler or our code?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it happens on Android and iOS.

Copy link
Contributor

@takameyer takameyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing stuff here. Really easy to read and follow. Couple comments, but can be disregarded at will.

////////////////////////////////////////////////////////////////////////////

/**
* This suite aims to test observability of Realm, Realm.Object and all collection types.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit nit picky, but "observable" to is a term from RxJS and means something different when I read that. Without reading this PR, I'm not sure I would deduce that event handlers were being tested here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our listeners are loosely modeled over node.js' event emitters

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I choose "observable" as a reference to the very general "observer pattern" in software design: The ability to register listeners (i.e. observers) of an object and have notifications trigger on change, which is probably also what RxJS and lots of other reactive / observable libraries are referencing.


function expectRealmNotifications(
realm: Realm,
eventName: "change" | "schema" | "beforenotify",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to figure out what the difference between "change" and "beforenotify" is and have realized that this isn't documented. Perhaps we could add this while we are at it.

Copy link
Member Author

@kraenhansen kraenhansen Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - it's not ideal. I'll leave that for a follow up PR. To be completely honest, I don't know the intended use-case for it: Perhaps we should actually just break the API and not expose it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P.S. You could use RealmEventName here, but it doesn't seem to be exported in index.ts. (Noticed that our enum is also not in index.ts. So if we do expose it, we could also use those in place of the hardcoded ones.)

If I'm not mistaken, I think one of the use cases is to react to potential runtime schema change initiated on the backend? We currently simulate this in our tests via realm._updateSchema().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created #6300.

...callbacks,
(...args: Args) => {
last(...args);
// Wait just a sec before resolving, in case another callback fires right after
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 50 equal to "a sec"? Or should the comment be more accurate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should probably update that comment :)

////////////////////////////////////////////////////////////////////////////

/**
* This suite aims to test observability of Realm, Realm.Object and all collection types.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our listeners are loosely modeled over node.js' event emitters

Comment on lines +175 to +176
// Skipping on React Native because the callback is called one too many times
it.skipIf(environment.reactNative, "calls listener", async function (this: RealmContext) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it happens both on Android and iOS? I mean, the event loop is different and I wonder if it is related to core's scheduler or our code?


this.realm.write(() => {
// collection["bob"].name = "Bobby";
// TODO: It seems we cannot trigger a notification when a property value changes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we open an issue about it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either that or we should clarify what the intended behaviour is. I'll follow up on this 👍

@kraenhansen kraenhansen merged commit 39d441c into main Dec 5, 2023
23 checks passed
@kraenhansen kraenhansen deleted the kh/observable-tests branch December 5, 2023 10:56
Copy link
Contributor

@elle-j elle-j left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great refactor!

import { createListenerStub } from "../utils/listener-stub";
import { createPromiseHandle } from "../utils/promise-handle";

type Observable = Realm | Realm.Object<any> | Realm.Results<any> | Realm.List<any> | Realm.Dictionary | Realm.Set<any>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you see value in taking a similar approach in the sync directory? E.g. for App, User, and SyncSession.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't think of those, but yes - that would be great!

Comment on lines +51 to +54
...expectedChangeSets.map(
(expectedChanges, c) => (realm: Realm, name: string, schema?: Realm.CanonicalObjectSchema[]) => {
expect(name).equals(eventName, `Realm change event #${c} name didn't match`);
expect(schema).deep.equals(expectedChanges.schema, `Realm change event #${c} schema didn't match`);
Copy link
Contributor

@elle-j elle-j Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming of c initially threw me off a bit, thinking it may be misused as I didn't associate the name to an index. But what do you think of moving the part within map(..) to another function to speed up readability? Perhaps something like

...expectedChangeSets.map(createRealmListenerCallback)

If so, then it could also apply to the other expectXNotifications functions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend do gravitate towards inline arrow functions for simple mappings / transformations like these, especially if they're not reused.


function expectRealmNotifications(
realm: Realm,
eventName: "change" | "schema" | "beforenotify",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P.S. You could use RealmEventName here, but it doesn't seem to be exported in index.ts. (Noticed that our enum is also not in index.ts. So if we do expose it, we could also use those in place of the hardcoded ones.)

If I'm not mistaken, I think one of the use cases is to react to potential runtime schema change initiated on the backend? We currently simulate this in our tests via realm._updateSchema().

Comment on lines +102 to +103
// describe("App", () => {});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// describe("App", () => {});

this.realm.create("Person", { name: "Alice" });
});

await completion;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 😎

Comment on lines +131 to +150
it("removes listeners", async function (this: RealmContext) {
const handle = createPromiseHandle();

const listener = createListenerStub(handle, () => {
this.realm.removeListener("change", listener);
setImmediate(() => {
this.realm.write(() => {
this.realm.create("Person", { name: "Bob" });
});
});
});

this.realm.addListener("change", listener);

this.realm.write(() => {
this.realm.create("Person", { name: "Alice" });
});

await handle;
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the pattern taken with the expectXNotifications functions. Could something similar be applied to the removal tests for expecting the removal?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the await expectXNotifications reads really good, but I hadn't found a good abstraction for this.
I gave it another go: #6301

Comment on lines +25 to +26
if (i >= callbacks.length) {
throw new Error(`Sequence was called more than ${callbacks.length} times (call #${i})`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Maybe either Sequence was called ${callbacks.length} or more times (call #${i}) or Sequence was called more than ${callbacks.length} times (call #${i + 1}) is more correct?

bimusiek pushed a commit to bimusiek/realm-js that referenced this pull request Mar 14, 2024
* Observable tests scaffold

* Default T for Realm.Dictionary

* WIP

* WIP

* Deleted existing notification tests

* Adding documentation to the test utils

* Removed import of missing tests

* Removed unwanted comments

* Skipping "beforenotify" test on React Native

* Update listener-stub.ts
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 14, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants