mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-22 16:09:14 +08:00
149b389508
* Switch to using Typescript instead of Babel * Switch all es6 / jsx file extensions to ts / tsx * Convert Utils to a TS module from module.exports style module * Move everything from module.exports to typescript exports * Define .d.ts files for mailspring-exports and component kit… Yes it seems this is the best option :( * Load up on those @types * Synthesize TS types from PropTypes for standard components * Add types to Model classes and move constructor constants to instance vars * 9800 => 7700 TS errors * 7700 => 5600 TS errors * 5600 => 5330 TS errors * 5330 => 4866 TS errors * 4866 => 4426 TS errors * 4426 => 2411 TS errors * 2411 > 1598 TS errors * 1598 > 769 TS errors * 769 > 129 TS errors * 129 > 22 TS errors * Fix runtime errors * More runtime error fixes * Remove support for custom .es6 file extension * Remove a few odd remaining references to Nylas * Don’t ship Typescript support in the compiled app for now * Fix issues in compiled app - module resolution in TS is case sensitive? * README updates * Fix a few more TS errors * Make “No Signature” option clickable + selectable * Remove flicker when saving file and reloading keymaps * Fix mail rule item height in preferences * Fix missing spacing in thread sharing popover * Fix scrollbar ticks being nested incorrectly * Add Japanese as a manually reviewed language * Prevent the thread list from “sticking” * Re-use Sheet when switching root tabs, prevent sidebar from resetting * Ensure specs run * Update package configuration to avoid shpping types * Turn eslint back on - we will opt-in to the TS rules one by one
351 lines
14 KiB
TypeScript
351 lines
14 KiB
TypeScript
import DatabaseStore from '../../src/flux/stores/database-store';
|
|
|
|
import { QueryRange } from '../../src/flux/models/query-range';
|
|
import { MutableQueryResultSet } from '../../src/flux/models/mutable-query-result-set';
|
|
import { QuerySubscription } from '../../src/flux/models/query-subscription';
|
|
import { Thread } from '../../src/flux/models/thread';
|
|
import * as Utils from '../../src/flux/models/utils';
|
|
|
|
describe('QuerySubscription', function QuerySubscriptionSpecs() {
|
|
describe('constructor', () =>
|
|
describe('when a query is provided', () => {
|
|
it('should finalize the query', () => {
|
|
const query = DatabaseStore.findAll<Thread>(Thread);
|
|
const subscription = new QuerySubscription(query);
|
|
expect(subscription).toBeDefined();
|
|
expect(query._finalized).toBe(true);
|
|
});
|
|
|
|
it('should throw an exception if the query is a count query, which cannot be observed', () => {
|
|
const query = DatabaseStore.count(Thread);
|
|
expect(() => {
|
|
const subscription = new QuerySubscription(query);
|
|
return subscription;
|
|
}).toThrow();
|
|
});
|
|
|
|
it('should call `update` to initialize the result set', () => {
|
|
const query = DatabaseStore.findAll<Thread>(Thread);
|
|
spyOn(QuerySubscription.prototype, 'update');
|
|
const subscription = new QuerySubscription(query);
|
|
expect(subscription).toBeDefined();
|
|
expect(QuerySubscription.prototype.update).toHaveBeenCalled();
|
|
});
|
|
|
|
describe('when initialModels are provided', () =>
|
|
it('should apply the models and trigger', () => {
|
|
const query = DatabaseStore.findAll<Thread>(Thread);
|
|
const threads = [1, 2, 3, 4, 5].map(i => new Thread({ id: i }));
|
|
const subscription = new QuerySubscription(query, { initialModels: threads });
|
|
expect(subscription._set).not.toBe(null);
|
|
}));
|
|
}));
|
|
|
|
describe('query', () =>
|
|
it('should return the query', () => {
|
|
const query = DatabaseStore.findAll<Thread>(Thread);
|
|
const subscription = new QuerySubscription(query);
|
|
expect(subscription.query()).toBe(query);
|
|
}));
|
|
|
|
describe('addCallback', () =>
|
|
it('should emit the last result to the new callback if one is available', () => {
|
|
const cb = jasmine.createSpy('callback');
|
|
spyOn(QuerySubscription.prototype, 'update').andReturn();
|
|
const subscription = new QuerySubscription(DatabaseStore.findAll<Thread>(Thread));
|
|
subscription._lastResult = 'something';
|
|
runs(() => {
|
|
subscription.addCallback(cb);
|
|
advanceClock();
|
|
});
|
|
waitsFor(() => cb.calls.length > 0);
|
|
runs(() => expect(cb).toHaveBeenCalledWith('something'));
|
|
}));
|
|
|
|
describe('applyChangeRecord', () => {
|
|
spyOn(Utils, 'generateTempId').andCallFake(() => undefined);
|
|
|
|
const scenarios = [
|
|
{
|
|
name: 'query with full set of objects (4)',
|
|
query: DatabaseStore.findAll<Thread>(Thread)
|
|
.where(Thread.attributes.accountId.equal('a'))
|
|
.limit(4)
|
|
.offset(2),
|
|
lastModels: [
|
|
new Thread({ accountId: 'a', id: '4', lastMessageReceivedTimestamp: 4 }),
|
|
new Thread({ accountId: 'a', id: '3', lastMessageReceivedTimestamp: 3 }),
|
|
new Thread({ accountId: 'a', id: '2', lastMessageReceivedTimestamp: 2 }),
|
|
new Thread({ accountId: 'a', id: '1', lastMessageReceivedTimestamp: 1 }),
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'Item in set saved - new id, same sort value',
|
|
change: {
|
|
objectClass: Thread.name,
|
|
objects: [
|
|
new Thread({
|
|
accountId: 'a',
|
|
id: '4',
|
|
lastMessageReceivedTimestamp: 4,
|
|
subject: 'hello',
|
|
}),
|
|
],
|
|
type: 'persist',
|
|
},
|
|
nextModels: [
|
|
new Thread({
|
|
accountId: 'a',
|
|
id: '4',
|
|
lastMessageReceivedTimestamp: 4,
|
|
subject: 'hello',
|
|
}),
|
|
new Thread({ accountId: 'a', id: '3', lastMessageReceivedTimestamp: 3 }),
|
|
new Thread({ accountId: 'a', id: '2', lastMessageReceivedTimestamp: 2 }),
|
|
new Thread({ accountId: 'a', id: '1', lastMessageReceivedTimestamp: 1 }),
|
|
],
|
|
mustUpdate: false,
|
|
mustTrigger: true,
|
|
},
|
|
{
|
|
name: 'Item in set saved - new sort value',
|
|
change: {
|
|
objectClass: Thread.name,
|
|
objects: [new Thread({ accountId: 'a', id: '5', lastMessageReceivedTimestamp: 3.5 })],
|
|
type: 'persist',
|
|
},
|
|
nextModels: [
|
|
new Thread({ accountId: 'a', id: '4', lastMessageReceivedTimestamp: 4 }),
|
|
new Thread({ accountId: 'a', id: '5', lastMessageReceivedTimestamp: 3.5 }),
|
|
new Thread({ accountId: 'a', id: '3', lastMessageReceivedTimestamp: 3 }),
|
|
new Thread({ accountId: 'a', id: '2', lastMessageReceivedTimestamp: 2 }),
|
|
],
|
|
mustUpdate: true,
|
|
mustTrigger: true,
|
|
},
|
|
{
|
|
name: 'Item saved - does not match query clauses, offset > 0',
|
|
change: {
|
|
objectClass: Thread.name,
|
|
objects: [new Thread({ accountId: 'b', id: '5', lastMessageReceivedTimestamp: 5 })],
|
|
type: 'persist',
|
|
},
|
|
nextModels: 'unchanged',
|
|
mustUpdate: true,
|
|
},
|
|
{
|
|
name: 'Item saved - matches query clauses',
|
|
change: {
|
|
objectClass: Thread.name,
|
|
objects: [new Thread({ accountId: 'a', id: '5', lastMessageReceivedTimestamp: -2 })],
|
|
type: 'persist',
|
|
},
|
|
mustUpdate: true,
|
|
},
|
|
{
|
|
name: 'Item in set saved - no longer matches query clauses',
|
|
change: {
|
|
objectClass: Thread.name,
|
|
objects: [new Thread({ accountId: 'b', id: '4', lastMessageReceivedTimestamp: 4 })],
|
|
type: 'persist',
|
|
},
|
|
nextModels: [
|
|
new Thread({ accountId: 'a', id: '3', lastMessageReceivedTimestamp: 3 }),
|
|
new Thread({ accountId: 'a', id: '2', lastMessageReceivedTimestamp: 2 }),
|
|
new Thread({ accountId: 'a', id: '1', lastMessageReceivedTimestamp: 1 }),
|
|
],
|
|
mustUpdate: true,
|
|
},
|
|
{
|
|
name: 'Item in set deleted',
|
|
change: {
|
|
objectClass: Thread.name,
|
|
objects: [new Thread({ accountId: 'a', id: '4' })],
|
|
type: 'unpersist',
|
|
},
|
|
nextModels: [
|
|
new Thread({ accountId: 'a', id: '3', lastMessageReceivedTimestamp: 3 }),
|
|
new Thread({ accountId: 'a', id: '2', lastMessageReceivedTimestamp: 2 }),
|
|
new Thread({ accountId: 'a', id: '1', lastMessageReceivedTimestamp: 1 }),
|
|
],
|
|
mustUpdate: true,
|
|
},
|
|
{
|
|
name: 'Item not in set deleted',
|
|
change: {
|
|
objectClass: Thread.name,
|
|
objects: [new Thread({ accountId: 'a', id: '5' })],
|
|
type: 'unpersist',
|
|
},
|
|
nextModels: 'unchanged',
|
|
mustUpdate: false,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'query with multiple sort orders',
|
|
query: DatabaseStore.findAll<Thread>(Thread)
|
|
.where(Thread.attributes.accountId.equal('a'))
|
|
.limit(4)
|
|
.offset(2)
|
|
.order([
|
|
Thread.attributes.lastMessageReceivedTimestamp.ascending(),
|
|
Thread.attributes.unread.descending(),
|
|
]),
|
|
lastModels: [
|
|
new Thread({ accountId: 'a', id: '1', lastMessageReceivedTimestamp: 1, unread: true }),
|
|
new Thread({ accountId: 'a', id: '2', lastMessageReceivedTimestamp: 1, unread: false }),
|
|
new Thread({ accountId: 'a', id: '3', lastMessageReceivedTimestamp: 1, unread: false }),
|
|
new Thread({ accountId: 'a', id: '4', lastMessageReceivedTimestamp: 2, unread: true }),
|
|
],
|
|
tests: [
|
|
{
|
|
name: 'Item in set saved, secondary sort order changed',
|
|
change: {
|
|
objectClass: Thread.name,
|
|
objects: [
|
|
new Thread({
|
|
accountId: 'a',
|
|
id: '3',
|
|
lastMessageReceivedTimestamp: 1,
|
|
unread: true,
|
|
}),
|
|
],
|
|
type: 'persist',
|
|
},
|
|
mustUpdate: true,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
jasmine.unspy(Utils, 'generateTempId');
|
|
|
|
describe('scenarios', () =>
|
|
scenarios.forEach(scenario => {
|
|
scenario.tests.forEach(test => {
|
|
it(`with ${scenario.name}, should correctly apply ${test.name}`, () => {
|
|
const subscription = new QuerySubscription(scenario.query);
|
|
subscription._set = new MutableQueryResultSet();
|
|
subscription._set.addModelsInRange(
|
|
scenario.lastModels,
|
|
new QueryRange({ start: 0, end: scenario.lastModels.length })
|
|
);
|
|
|
|
spyOn(subscription, 'update');
|
|
spyOn(subscription, '_createResultAndTrigger');
|
|
subscription._updateInFlight = false;
|
|
subscription.applyChangeRecord(test.change);
|
|
|
|
if (test.mustUpdate) {
|
|
expect(subscription.update).toHaveBeenCalledWith({ mustRefetchEntireRange: true });
|
|
} else if (test.nextModels === 'unchanged') {
|
|
expect(subscription._set.models()).toEqual(scenario.lastModels);
|
|
} else {
|
|
expect(subscription._set.models()).toEqual(test.nextModels);
|
|
}
|
|
|
|
if (test.mustTriger) {
|
|
expect(subscription._createResultAndTrigger).toHaveBeenCalled();
|
|
}
|
|
});
|
|
});
|
|
}));
|
|
});
|
|
|
|
describe('update', () => {
|
|
beforeEach(() =>
|
|
spyOn(QuerySubscription.prototype, '_fetchRange').andCallFake(() => {
|
|
if (this._set == null) {
|
|
this._set = new MutableQueryResultSet();
|
|
}
|
|
return Promise.resolve();
|
|
})
|
|
);
|
|
|
|
describe('when the query has an infinite range', () => {
|
|
it('should call _fetchRange for the entire range', () => {
|
|
const subscription = new QuerySubscription(DatabaseStore.findAll<Thread>(Thread));
|
|
subscription.update();
|
|
advanceClock();
|
|
expect(subscription._fetchRange).toHaveBeenCalledWith(QueryRange.infinite(), {
|
|
fetchEntireModels: true,
|
|
version: 1,
|
|
});
|
|
});
|
|
|
|
it('should fetch full full models only when the previous set is empty', () => {
|
|
const subscription = new QuerySubscription(DatabaseStore.findAll<Thread>(Thread));
|
|
subscription._set = new MutableQueryResultSet();
|
|
subscription._set.addModelsInRange([new Thread()], new QueryRange({ start: 0, end: 1 }));
|
|
subscription.update();
|
|
advanceClock();
|
|
expect(subscription._fetchRange).toHaveBeenCalledWith(QueryRange.infinite(), {
|
|
fetchEntireModels: false,
|
|
version: 1,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when the query has a range', () => {
|
|
beforeEach(() => {
|
|
this.query = DatabaseStore.findAll<Thread>(Thread).limit(10);
|
|
});
|
|
|
|
describe('when we have no current range', () =>
|
|
it('should call _fetchRange for the entire range and fetch full models', () => {
|
|
const subscription = new QuerySubscription(this.query);
|
|
subscription._set = null;
|
|
subscription.update();
|
|
advanceClock();
|
|
expect(subscription._fetchRange).toHaveBeenCalledWith(this.query.range(), {
|
|
fetchEntireModels: true,
|
|
version: 1,
|
|
});
|
|
}));
|
|
|
|
describe('when we have a previous range', () => {
|
|
it('should call _fetchRange with the missingRange', () => {
|
|
const customRange = jasmine.createSpy('customRange1');
|
|
spyOn(QueryRange, 'rangesBySubtracting').andReturn([customRange]);
|
|
const subscription = new QuerySubscription(this.query);
|
|
subscription._set = new MutableQueryResultSet();
|
|
subscription._set.addModelsInRange([new Thread()], new QueryRange({ start: 0, end: 1 }));
|
|
|
|
advanceClock();
|
|
subscription._fetchRange.reset();
|
|
subscription._updateInFlight = false;
|
|
subscription.update();
|
|
advanceClock();
|
|
expect(subscription._fetchRange.callCount).toBe(1);
|
|
expect(subscription._fetchRange.calls[0].args).toEqual([
|
|
customRange,
|
|
{ fetchEntireModels: true, version: 1 },
|
|
]);
|
|
});
|
|
|
|
it('should call _fetchRange for the entire query range when the missing range encompasses more than one range', () => {
|
|
const customRange1 = jasmine.createSpy('customRange1');
|
|
const customRange2 = jasmine.createSpy('customRange2');
|
|
spyOn(QueryRange, 'rangesBySubtracting').andReturn([customRange1, customRange2]);
|
|
|
|
const range = new QueryRange({ start: 0, end: 1 });
|
|
const subscription = new QuerySubscription(this.query);
|
|
subscription._set = new MutableQueryResultSet();
|
|
subscription._set.addModelsInRange([new Thread()], range);
|
|
|
|
advanceClock();
|
|
subscription._fetchRange.reset();
|
|
subscription._updateInFlight = false;
|
|
subscription.update();
|
|
advanceClock();
|
|
expect(subscription._fetchRange.callCount).toBe(1);
|
|
expect(subscription._fetchRange.calls[0].args).toEqual([
|
|
this.query.range(),
|
|
{ fetchEntireModels: true, version: 1 },
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|