Mailspring/spec/stores/thread-counts-store-spec.coffee

333 lines
12 KiB
CoffeeScript

_ = require 'underscore'
DatabaseStore = require '../../src/flux/stores/database-store'
DatabaseTransaction = require '../../src/flux/stores/database-transaction'
ThreadCountsStore = require '../../src/flux/stores/thread-counts-store'
Thread = require '../../src/flux/models/thread'
Category = require '../../src/flux/models/category'
Matcher = require '../../src/flux/attributes/matcher'
WindowBridge = require '../../src/window-bridge'
category1 = new Category(id: "l1", name: "inbox", displayName: "Inbox")
category2 = new Category(id: "l2", name: "archive", displayName: "Archive")
category3 = new Category(id: "l3", displayName: "Happy Days")
category4 = new Category(id: "l4", displayName: "Sad Days")
category5 = new Category(id: "l5", name: 'all', displayName: "All Mail")
category6 = new Category(id: "l6", name: 'trash', displayName: "Trash")
# Values here are the "after" state. Below, the spy on the query returns the
# "current" state.
threadA = new Thread
id: "A"
unread: true
categories: [category1, category4, category5]
categoriesType: 'labels'
threadB = new Thread
id: "B"
unread: true
categories: [category3, category5]
categoriesType: 'labels'
threadC = new Thread
id: "C"
unread: false
categories: [category1, category3, category5]
categoriesType: 'labels'
threadD = new Thread
id: "D"
unread: true
categories: [category6]
categoriesType: 'labels'
threadE = new Thread
id: "E"
unread: true
categories: [category1, category5]
categoriesType: 'labels'
describe "ThreadCountsStore", ->
describe "unreadCountForCategoryId", ->
it "returns null if no count exists for the category id", ->
expect(ThreadCountsStore.unreadCountForCategoryId('nan')).toBe(null)
it "returns the count plus any unsaved deltas", ->
ThreadCountsStore._counts =
'b': 3
'a': 5
ThreadCountsStore._deltas =
'a': -1
expect(ThreadCountsStore.unreadCountForCategoryId('a')).toBe(4)
expect(ThreadCountsStore.unreadCountForCategoryId('b')).toBe(3)
describe "when the mutation observer reports count changes", ->
describe "in the work window", ->
beforeEach ->
spyOn(NylasEnv, 'isWorkWindow').andReturn(true)
it "should merge count deltas into existing count detlas", ->
ThreadCountsStore._deltas =
'l1': -1
'l2': 2
ThreadCountsStore._onCountsChanged({'l1': -1, 'l2': 1, 'l3': 2})
expect(ThreadCountsStore._deltas).toEqual({
'l1': -2,
'l2': 3,
'l3': 2
})
it "should queue a save of the counts", ->
spyOn(ThreadCountsStore, '_saveCountsSoon')
ThreadCountsStore._onCountsChanged({'l1': -1, 'l2': 1, 'l3': 2})
expect(ThreadCountsStore._saveCountsSoon).toHaveBeenCalled()
describe "in other windows", ->
beforeEach ->
spyOn(NylasEnv, 'isWorkWindow').andReturn(false)
it "should use the WindowBridge to forward the invocation to the work window", ->
spyOn(WindowBridge, 'runInWorkWindow')
payload = {'l1': -1, 'l2': 1, 'l3': 2}
ThreadCountsStore._onCountsChanged(payload)
expect(WindowBridge.runInWorkWindow).toHaveBeenCalledWith('ThreadCountsStore', '_onCountsChanged', [payload])
describe "when counts are persisted", ->
it "should update it's _counts cache and trigger", ->
newCounts = {
'abc': 1
}
spyOn(ThreadCountsStore, 'trigger')
ThreadCountsStore._onCountsBlobRead(newCounts)
expect(ThreadCountsStore._counts).toEqual(newCounts)
expect(ThreadCountsStore.trigger).toHaveBeenCalled()
describe "_fetchCountsMissing", ->
beforeEach ->
ThreadCountsStore._categories = [
new Category(id: "l1", name: "inbox", displayName: "Inbox", accountId: 'a1'),
new Category(id: "l2", name: "archive", displayName: "Archive", accountId: 'a1'),
new Category(id: "l3", displayName: "Happy Days", accountId: 'a1'),
new Category(id: "l4", displayName: "Sad Days", accountId: 'a1')
]
ThreadCountsStore._deltas =
l1: 10
l2: 0
l3: 3
l4: 12
ThreadCountsStore._counts =
l1: 10
l2: 0
@countResolve = null
@countReject = null
spyOn(ThreadCountsStore, '_fetchCountForCategory').andCallFake =>
new Promise (resolve, reject) =>
@countResolve = resolve
@countReject = reject
it "should call _fetchCountForCategory for the first category not already in the counts cache", ->
ThreadCountsStore._fetchCountsMissing()
calls = ThreadCountsStore._fetchCountForCategory.calls
expect(calls.length).toBe(1)
expect(calls[0].args[0]).toBe(ThreadCountsStore._categories[2])
it "should set the _deltas for the category it's counting back to zero", ->
ThreadCountsStore._fetchCountsMissing()
expect(ThreadCountsStore._deltas.l3).toBe(0)
describe "when the count promise finishes", ->
it "should add it to the count cache", ->
ThreadCountsStore._fetchCountsMissing()
advanceClock()
@countResolve(4)
advanceClock()
expect(ThreadCountsStore._counts.l3).toEqual(4)
it "should call _fetchCountsMissing again to populate the next missing count", ->
ThreadCountsStore._fetchCountsMissing()
spyOn(ThreadCountsStore, '_fetchCountsMissing')
advanceClock()
@countResolve(4)
advanceClock()
advanceClock(10001)
expect(ThreadCountsStore._fetchCountsMissing).toHaveBeenCalled()
describe "when deltas appear during a count", ->
it "should not set the count and count again in 10 seconds", ->
ThreadCountsStore._fetchCountsMissing()
spyOn(ThreadCountsStore, '_fetchCountsMissing')
advanceClock()
ThreadCountsStore._deltas.l3 = -1
@countResolve(4)
advanceClock()
expect(ThreadCountsStore._counts.l3).toBeUndefined()
expect(ThreadCountsStore._fetchCountsMissing).not.toHaveBeenCalled()
advanceClock(10001)
expect(ThreadCountsStore._fetchCountsMissing).toHaveBeenCalled()
describe "when a count fails", ->
it "should not immediately try to count any other categories", ->
spyOn(console, "warn")
ThreadCountsStore._fetchCountsMissing()
spyOn(ThreadCountsStore, '_fetchCountsMissing')
spyOn(console, 'error')
advanceClock()
@countReject(new Error("Oh man something really bad."))
advanceClock()
expect(console.warn).toHaveBeenCalled()
expect(ThreadCountsStore._fetchCountsMissing).not.toHaveBeenCalled()
describe "_fetchCountForCategory", ->
it "should make the appropriate category database query", ->
spyOn(DatabaseStore, 'count')
Matcher.muid = 0
ThreadCountsStore._fetchCountForCategory(new Category(id: 'l1', accountId: 'a1'))
Matcher.muid = 0
expect(DatabaseStore.count).toHaveBeenCalledWith(Thread, [
Thread.attributes.categories.contains('l1'),
Thread.attributes.accountId.equal('a1'),
Thread.attributes.unread.equal(true),
])
describe "_saveCounts", ->
beforeEach ->
ThreadCountsStore._counts =
'b': 3
'a': 5
ThreadCountsStore._deltas =
'a': -1
'c': 2
it "should merge the deltas into the counts and reset the deltas, ignoring any deltas for which the initial count has not been run", ->
ThreadCountsStore._saveCounts()
expect(ThreadCountsStore._counts).toEqual({
'b': 3
'a': 4
})
it "should persist the new counts to the database", ->
spyOn(DatabaseTransaction.prototype, 'persistJSONBlob')
runs =>
ThreadCountsStore._saveCounts()
waitsFor =>
DatabaseTransaction.prototype.persistJSONBlob.callCount > 0
runs =>
expect(DatabaseTransaction.prototype.persistJSONBlob).toHaveBeenCalledWith(ThreadCountsStore.JSONBlobKey, ThreadCountsStore._counts)
describe "CategoryDatabaseMutationObserver", ->
beforeEach ->
@queryResolves = []
@query = jasmine.createSpy('query').andCallFake =>
new Promise (resolve, reject) =>
@queryResolves.push(resolve)
@countsDidChange = jasmine.createSpy('countsDidChange')
@m = new ThreadCountsStore.CategoryDatabaseMutationObserver(@countsDidChange)
describe "given a set of modifying models", ->
scenarios = [
{
name: 'Persisting a three threads, two unread, all in all mail'
type: 'persist',
threads: [threadA, threadB, threadC],
beforePersistQueryResults: [
{id: threadA.id, catId: category1.id},
{id: threadA.id, catId: category3.id},
{id: threadA.id, catId: category5.id},
{id: threadB.id, catId: category2.id},
{id: threadB.id, catId: category5.id},
{id: threadB.id, catId: category3.id},
{id: threadC.id, catId: category5.id},
]
beforePersistExpected: {
l1: -1,
l3: -2,
l2: -1,
l5: -3
}
afterPersistExpected: {
l3: -1,
l5: -1,
l2: -1,
l4: 1,
}
},
{
name: 'Unpersisting a normal set of threads, all in all mail'
type: 'unpersist',
threads: [threadA, threadB, threadC],
beforePersistQueryResults: [
{id: threadA.id, catId: category1.id},
{id: threadA.id, catId: category3.id},
{id: threadA.id, catId: category5.id},
{id: threadB.id, catId: category2.id},
{id: threadB.id, catId: category5.id},
{id: threadB.id, catId: category3.id},
{id: threadC.id, catId: category5.id},
]
beforePersistExpected: {
l1: -1,
l3: -2,
l2: -1,
l5: -3
}
afterPersistExpected: {
l1: -1,
l5: -3,
l3: -2,
l2: -1
}
},
{
name: 'Thread D going from inbox to trash'
type: 'persist',
threads: [threadD],
beforePersistQueryResults: [
{id: threadD.id, catId: category1.id},
{id: threadD.id, catId: category3.id},
{id: threadD.id, catId: category4.id},
]
beforePersistExpected: {
l1: -1,
l3: -1,
l4: -1
}
afterPersistExpected: {
l1: -1,
l3: -1,
l4: -1,
}
},
{
name: 'Thread E going from trash to inbox'
type: 'persist',
threads: [threadE],
beforePersistQueryResults: [
]
beforePersistExpected: {
}
afterPersistExpected: {
l1: 1,
l5: 1
}
},
]
scenarios.forEach ({name, type, threads, beforePersistQueryResults, beforePersistExpected, afterPersistExpected}) ->
it "should call countsDidChange with the category membership deltas (#{name})", ->
beforePromise = @m.beforeDatabaseChange(@query, {
type: type
objects: threads,
objectIds: _.pluck(threads, 'id'),
objectClass: Thread.name
})
expect(@query.callCount).toBe(1)
expect(@query.calls[0].args[0]).toEqual("SELECT `Thread`.id as id, `Thread-Category`.`value` as catId FROM `Thread` INNER JOIN `Thread-Category` ON `Thread`.`id` = `Thread-Category`.`id` WHERE `Thread`.id IN ('#{_.pluck(threads, 'id').join("','")}') AND `Thread`.unread = 1 AND `Thread`.in_all_mail = 1")
@queryResolves[0](beforePersistQueryResults)
waitsForPromise =>
beforePromise.then (result) =>
expect(result).toEqual({categories: beforePersistExpected})
@m.afterDatabaseChange(@query, {
type: type
objects: threads,
objectIds: _.pluck(threads, 'id'),
objectClass: Thread.name
}, result)
expect(@countsDidChange).toHaveBeenCalledWith(afterPersistExpected)