Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
343 changes: 343 additions & 0 deletions packages/react-meteor-data/suspense/useTracker.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
/* global Meteor, Tinytest */
import React, { Suspense } from 'react';
import { renderToString } from 'react-dom/server';
import { Mongo } from 'meteor/mongo';
import { render } from '@testing-library/react';
import { useTracker, cacheMap } from './useTracker';

const clearCache = async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
cacheMap.clear();
};

const setupTest = (data = { id: 0, updated: 0 }) => {
const Coll = new Mongo.Collection(null);
data && Coll.insertAsync(data);

return { Coll, simpleFetch: () => Coll.find().fetchAsync() };
};

const TestSuspense = ({ children }) => {
return <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>;
};

/**
* Test for useTracker with Suspense
*/
Tinytest.addAsync(
'suspense/useTracker - Data query validation',
async (test) => {
const { simpleFetch } = setupTest();

let returnValue;

const Test = () => {
returnValue = useTracker('TestDocs', simpleFetch);

return null;
};

// first return promise
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);
test.isUndefined(
returnValue,
'Return value should be undefined as find promise unresolved'
);
// wait promise
await new Promise((resolve) => setTimeout(resolve, 100));
// return data
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);

test.equal(
returnValue.length,
1,
'Return value should be an array with one document'
);

await clearCache();
}
);

Tinytest.addAsync(
'suspense/useTracker - Test proper cache invalidation',
async function (test) {
const { Coll, simpleFetch } = setupTest();

let returnValue;

const Test = () => {
returnValue = useTracker('TestDocs', simpleFetch);
return null;
};

// first return promise
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);
// wait promise
await new Promise((resolve) => setTimeout(resolve, 100));
// return data
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);

test.equal(
returnValue[0].updated,
0,
'Return value should be an array with initial value as find promise resolved'
);

Coll.updateAsync({ id: 0 }, { $inc: { updated: 1 } });
await new Promise((resolve) => setTimeout(resolve, 100));

// second return promise
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);

test.equal(
returnValue[0].updated,
0,
'Return value should still not updated as second find promise unresolved'
);

// wait promise
await new Promise((resolve) => setTimeout(resolve, 100));
// return data
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);

test.equal(
returnValue[0].updated,
1,
'Return value should be an array with one document with value updated'
);

await clearCache();
}
);

Meteor.isClient &&
Tinytest.addAsync(
'suspense/useTracker - Test useTracker with skipUpdate',
async function (test) {
const { Coll, simpleFetch } = setupTest({ id: 0, updated: 0, other: 0 });

let returnValue;

const Test = () => {
returnValue = useTracker('TestDocs', simpleFetch, (prev, next) => {
// Skip update if the document has not changed
return prev[0].updated === next[0].updated;
});

return null;
};

// first return promise
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);
// wait promise
await new Promise((resolve) => setTimeout(resolve, 100));
// return data
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);

test.equal(
returnValue[0].updated,
0,
'Return value should be an array with initial value as find promise resolved'
);

Coll.updateAsync({ id: 0 }, { $inc: { other: 1 } });
await new Promise((resolve) => setTimeout(resolve, 100));

// second return promise
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);
// wait promise
await new Promise((resolve) => setTimeout(resolve, 100));
// return data
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);

test.equal(
returnValue[0].other,
0,
'Return value should still not updated as skipUpdate returned true'
);

await clearCache();
}
);

// https://github.com/meteor/react-packages/issues/454
Meteor.isClient &&
Tinytest.addAsync(
'suspense/useTracker - Testing performance with multiple Trackers',
async (test) => {
const TestCollections = [];
let returnDocs = new Map();

for (let i = 0; i < 100; i++) {
const { Coll } = setupTest(null);

for (let i = 0; i < 100; i++) {
Coll.insertAsync({ id: i });
}

TestCollections.push(Coll);
}

const Test = ({ collection, index }) => {
const docsCount = useTracker(`TestDocs${index}`, () =>
collection.find().fetchAsync()
).length;

returnDocs.set(`TestDocs${index}`, docsCount);

return null;
};
const TestWrap = () => {
return (
<TestSuspense>
{TestCollections.map((collection, index) => (
<Test key={index} collection={collection} index={index} />
))}
</TestSuspense>
);
};

// first return promise
renderToString(<TestWrap />);
// wait promise
await new Promise((resolve) => setTimeout(resolve, 100));
// return data
renderToString(<TestWrap />);

test.equal(returnDocs.size, 100, 'should return 100 collections');

const docsCount = Array.from(returnDocs.values()).reduce(
(a, b) => a + b,
0
);

test.equal(docsCount, 10000, 'should return 10000 documents');

await clearCache();
}
);

Meteor.isServer &&
Tinytest.addAsync(
'suspense/useTracker - Test no memory leaks',
async function (test) {
const { simpleFetch } = setupTest();

let returnValue;

const Test = () => {
returnValue = useTracker('TestDocs', simpleFetch);

return null;
};

// first return promise
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);
// wait promise
await new Promise((resolve) => setTimeout(resolve, 100));
// return data
renderToString(
<TestSuspense>
<Test />
</TestSuspense>
);
// wait cleanup
await new Promise((resolve) => setTimeout(resolve, 100));

test.equal(
cacheMap.size,
0,
'Cache map should be empty as server cache should be cleared after render'
);
}
);

Meteor.isClient &&
Tinytest.addAsync(
'suspense/useTracker - Test no memory leaks',
async function (test) {
const { simpleFetch } = setupTest({ id: 0, name: 'a' });

const Test = () => {
const docs = useTracker('TestDocs', simpleFetch);

return <div>{docs[0]?.name}</div>;
};

const { queryByText, findByText, unmount } = render(<Test />, {
container: document.createElement('container'),
wrapper: TestSuspense,
});

test.isNotNull(
queryByText('Loading...'),
'Throw Promise as needed to trigger the fallback.'
);

test.isTrue(await findByText('a'), 'Need to return data');

unmount();
// wait cleanup
await new Promise((resolve) => setTimeout(resolve, 100));

test.equal(
cacheMap.size,
0,
'Cache map should be empty as component unmounted and cache cleared'
);
}
);
Loading