mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-12 12:40:08 +08:00
30c58e90a6
Summary: Add concept of "final" to Query, clean up internals Tiny bug fixes RxJs Observables! WIP Test Plan: Run tests Reviewers: evan, juan Reviewed By: juan Differential Revision: https://phab.nylas.com/D2319
296 lines
11 KiB
CoffeeScript
296 lines
11 KiB
CoffeeScript
Actions = require '../../src/flux/actions'
|
|
DatabaseStore = require '../../src/flux/stores/database-store'
|
|
TaskQueue = require '../../src/flux/stores/task-queue'
|
|
Task = require '../../src/flux/tasks/task'
|
|
|
|
{APIError,
|
|
OfflineError,
|
|
TimeoutError} = require '../../src/flux/errors'
|
|
|
|
class TaskSubclassA extends Task
|
|
constructor: (val) -> @aProp = val; super
|
|
|
|
class TaskSubclassB extends Task
|
|
constructor: (val) -> @bProp = val; super
|
|
|
|
describe "TaskQueue", ->
|
|
|
|
makeUnstartedTask = (task) ->
|
|
task
|
|
|
|
makeProcessing = (task) ->
|
|
task.queueState.isProcessing = true
|
|
task
|
|
|
|
beforeEach ->
|
|
@task = new Task()
|
|
@unstartedTask = makeUnstartedTask(new Task())
|
|
@processingTask = makeProcessing(new Task())
|
|
|
|
afterEach ->
|
|
# Flush any throttled or debounced updates
|
|
advanceClock(1000)
|
|
|
|
describe "restoreQueue", ->
|
|
it "should fetch the queue from the database, reset flags and start processing", ->
|
|
queue = [@processingTask, @unstartedTask]
|
|
spyOn(DatabaseStore, 'findJSONBlob').andCallFake => Promise.resolve(queue)
|
|
spyOn(TaskQueue, '_processQueue')
|
|
|
|
waitsForPromise =>
|
|
TaskQueue._restoreQueue().then =>
|
|
expect(TaskQueue._queue).toEqual(queue)
|
|
expect(@processingTask.queueState.isProcessing).toEqual(false)
|
|
expect(TaskQueue._processQueue).toHaveBeenCalled()
|
|
|
|
describe "findTask", ->
|
|
beforeEach ->
|
|
@subclassA = new TaskSubclassA()
|
|
@subclassB1 = new TaskSubclassB("B1")
|
|
@subclassB2 = new TaskSubclassB("B2")
|
|
TaskQueue._queue = [@subclassA, @subclassB1, @subclassB2]
|
|
|
|
it "accepts type as a string", ->
|
|
expect(TaskQueue.findTask('TaskSubclassB', {bProp: 'B1'})).toEqual(@subclassB1)
|
|
|
|
it "accepts type as a class", ->
|
|
expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B1'})).toEqual(@subclassB1)
|
|
|
|
it "works without a set of match criteria", ->
|
|
expect(TaskQueue.findTask(TaskSubclassA)).toEqual(@subclassA)
|
|
|
|
it "only returns a task that matches the criteria", ->
|
|
expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B1'})).toEqual(@subclassB1)
|
|
expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B2'})).toEqual(@subclassB2)
|
|
expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B3'})).toEqual(null)
|
|
|
|
describe "enqueue", ->
|
|
beforeEach ->
|
|
spyOn(@unstartedTask, 'runLocal').andCallFake =>
|
|
@unstartedTask.queueState.localComplete = true
|
|
Promise.resolve()
|
|
|
|
it "makes sure you've queued a real task", ->
|
|
expect( -> TaskQueue.enqueue("asamw")).toThrow()
|
|
|
|
it "adds it to the queue", ->
|
|
spyOn(TaskQueue, '_processQueue').andCallFake ->
|
|
TaskQueue.enqueue(@unstartedTask)
|
|
advanceClock()
|
|
expect(TaskQueue._queue.length).toBe(1)
|
|
|
|
it "immediately calls runLocal", ->
|
|
TaskQueue.enqueue(@unstartedTask)
|
|
expect(@unstartedTask.runLocal).toHaveBeenCalled()
|
|
|
|
it "notifies the queue should be processed", ->
|
|
spyOn(TaskQueue, "_processQueue").andCallThrough()
|
|
spyOn(TaskQueue, "_processTask")
|
|
|
|
TaskQueue.enqueue(@unstartedTask)
|
|
advanceClock()
|
|
advanceClock()
|
|
expect(TaskQueue._processQueue).toHaveBeenCalled()
|
|
expect(TaskQueue._processTask).toHaveBeenCalledWith(@unstartedTask)
|
|
expect(TaskQueue._processTask.calls.length).toBe(1)
|
|
|
|
it "throws an exception if the task does not have a queueState", ->
|
|
task = new TaskSubclassA()
|
|
task.queueState = undefined
|
|
expect( => TaskQueue.enqueue(task)).toThrow()
|
|
|
|
it "throws an exception if the task does not have an ID", ->
|
|
task = new TaskSubclassA()
|
|
task.id = undefined
|
|
expect( => TaskQueue.enqueue(task)).toThrow()
|
|
|
|
it "dequeues obsolete tasks", ->
|
|
task = new TaskSubclassA()
|
|
spyOn(TaskQueue, '_dequeueObsoleteTasks').andCallFake ->
|
|
TaskQueue.enqueue(task)
|
|
expect(TaskQueue._dequeueObsoleteTasks).toHaveBeenCalled()
|
|
|
|
describe "_dequeueObsoleteTasks", ->
|
|
it "should dequeue tasks based on `shouldDequeueOtherTask`", ->
|
|
class KillsTaskA extends Task
|
|
shouldDequeueOtherTask: (other) -> other instanceof TaskSubclassA
|
|
performRemote: -> new Promise (resolve, reject) ->
|
|
|
|
otherTask = new Task()
|
|
otherTask.queueState.localComplete = true
|
|
obsoleteTask = new TaskSubclassA()
|
|
obsoleteTask.queueState.localComplete = true
|
|
replacementTask = new KillsTaskA()
|
|
replacementTask.queueState.localComplete = true
|
|
|
|
spyOn(TaskQueue, 'dequeue').andCallThrough()
|
|
TaskQueue._queue = [obsoleteTask, otherTask]
|
|
TaskQueue._dequeueObsoleteTasks(replacementTask)
|
|
expect(TaskQueue._queue.length).toBe(1)
|
|
expect(obsoleteTask.queueState.status).toBe Task.Status.Continue
|
|
expect(obsoleteTask.queueState.debugStatus).toBe Task.DebugStatus.DequeuedObsolete
|
|
expect(TaskQueue.dequeue).toHaveBeenCalledWith(obsoleteTask)
|
|
expect(TaskQueue.dequeue.calls.length).toBe(1)
|
|
|
|
describe "dequeue", ->
|
|
beforeEach ->
|
|
TaskQueue._queue = [@unstartedTask, @processingTask]
|
|
|
|
it "grabs the task by object", ->
|
|
found = TaskQueue._resolveTaskArgument(@unstartedTask)
|
|
expect(found).toBe @unstartedTask
|
|
|
|
it "grabs the task by id", ->
|
|
found = TaskQueue._resolveTaskArgument(@unstartedTask.id)
|
|
expect(found).toBe @unstartedTask
|
|
|
|
it "throws an error if the task isn't found", ->
|
|
expect( -> TaskQueue.dequeue("bad")).toThrow()
|
|
|
|
describe "with an unstarted task", ->
|
|
it "moves it from the queue", ->
|
|
TaskQueue.dequeue(@unstartedTask)
|
|
expect(TaskQueue._queue.length).toBe(1)
|
|
expect(TaskQueue._completed.length).toBe(1)
|
|
|
|
it "notifies the queue has been updated", ->
|
|
spyOn(TaskQueue, "_processQueue")
|
|
TaskQueue.dequeue(@unstartedTask)
|
|
advanceClock(20)
|
|
advanceClock()
|
|
expect(TaskQueue._processQueue).toHaveBeenCalled()
|
|
expect(TaskQueue._processQueue.calls.length).toBe(1)
|
|
|
|
describe "with a processing task", ->
|
|
it "calls cancel() to allow the task to resolve or reject from runRemote()", ->
|
|
spyOn(@processingTask, 'cancel')
|
|
TaskQueue.dequeue(@processingTask)
|
|
expect(@processingTask.cancel).toHaveBeenCalled()
|
|
expect(TaskQueue._queue.length).toBe(2)
|
|
expect(TaskQueue._completed.length).toBe(0)
|
|
|
|
describe "process Task", ->
|
|
it "doesn't process processing tasks", ->
|
|
spyOn(@processingTask, "runRemote").andCallFake -> Promise.resolve()
|
|
TaskQueue._processTask(@processingTask)
|
|
expect(@processingTask.runRemote).not.toHaveBeenCalled()
|
|
|
|
it "doesn't process blocked tasks", ->
|
|
class BlockedByTaskA extends Task
|
|
isDependentTask: (other) -> other instanceof TaskSubclassA
|
|
|
|
taskA = new TaskSubclassA()
|
|
otherTask = new Task()
|
|
blockedByTaskA = new BlockedByTaskA()
|
|
|
|
taskA.queueState.localComplete = true
|
|
otherTask.queueState.localComplete = true
|
|
blockedByTaskA.queueState.localComplete = true
|
|
|
|
spyOn(taskA, "runRemote").andCallFake -> new Promise (resolve, reject) ->
|
|
spyOn(blockedByTaskA, "runRemote").andCallFake -> Promise.resolve()
|
|
|
|
TaskQueue._queue = [taskA, otherTask, blockedByTaskA]
|
|
TaskQueue._processQueue()
|
|
|
|
advanceClock()
|
|
|
|
expect(TaskQueue._queue.length).toBe(2)
|
|
expect(taskA.runRemote).toHaveBeenCalled()
|
|
expect(blockedByTaskA.runRemote).not.toHaveBeenCalled()
|
|
|
|
it "doesn't block itself, even if the isDependentTask method is implemented naively", ->
|
|
class BlockingTask extends Task
|
|
isDependentTask: (other) -> other instanceof BlockingTask
|
|
|
|
blockedTask = new BlockingTask()
|
|
spyOn(blockedTask, "runRemote").andCallFake -> Promise.resolve()
|
|
|
|
TaskQueue.enqueue(blockedTask)
|
|
advanceClock()
|
|
blockedTask.runRemote.callCount > 0
|
|
|
|
it "sets the processing bit", ->
|
|
spyOn(@unstartedTask, "runRemote").andCallFake -> Promise.resolve()
|
|
task = new Task()
|
|
task.queueState.localComplete = true
|
|
TaskQueue._queue = [task]
|
|
TaskQueue._processTask(task)
|
|
expect(task.queueState.isProcessing).toBe true
|
|
|
|
describe "handling task runRemote task errors", ->
|
|
spyAACallback = jasmine.createSpy("onDependentTaskError")
|
|
spyBBRemote = jasmine.createSpy("performRemote")
|
|
spyBBCallback = jasmine.createSpy("onDependentTaskError")
|
|
spyCCRemote = jasmine.createSpy("performRemote")
|
|
spyCCCallback = jasmine.createSpy("onDependentTaskError")
|
|
|
|
beforeEach ->
|
|
testError = new Error("Test Error")
|
|
@testError = testError
|
|
class TaskAA extends Task
|
|
onDependentTaskError: spyAACallback
|
|
performRemote: ->
|
|
# We reject instead of `throw` because jasmine thinks this
|
|
# `throw` is in the context of the test instead of the context
|
|
# of the calling promise in task-queue.coffee
|
|
return Promise.reject(testError)
|
|
|
|
class TaskBB extends Task
|
|
isDependentTask: (other) -> other instanceof TaskAA
|
|
onDependentTaskError: spyBBCallback
|
|
performRemote: spyBBRemote
|
|
|
|
class TaskCC extends Task
|
|
isDependentTask: (other) -> other instanceof TaskBB
|
|
onDependentTaskError: (task, err) ->
|
|
spyCCCallback(task, err)
|
|
return Task.DO_NOT_DEQUEUE_ME
|
|
performRemote: spyCCRemote
|
|
|
|
@taskAA = new TaskAA
|
|
@taskAA.queueState.localComplete = true
|
|
@taskBB = new TaskBB
|
|
@taskBB.queueState.localComplete = true
|
|
@taskCC = new TaskCC
|
|
@taskCC.queueState.localComplete = true
|
|
|
|
spyOn(TaskQueue, 'trigger')
|
|
|
|
# Don't keep processing the queue
|
|
spyOn(TaskQueue, '_updateSoon')
|
|
|
|
it "catches the error and dequeues the task", ->
|
|
spyOn(TaskQueue, 'dequeue')
|
|
waitsForPromise =>
|
|
TaskQueue._processTask(@taskAA).then =>
|
|
expect(TaskQueue.dequeue).toHaveBeenCalledWith(@taskAA)
|
|
expect(spyAACallback).not.toHaveBeenCalled()
|
|
expect(@taskAA.queueState.remoteError.message).toBe "Test Error"
|
|
|
|
it "calls `onDependentTaskError` on dependent tasks", ->
|
|
spyOn(TaskQueue, 'dequeue').andCallThrough()
|
|
TaskQueue._queue = [@taskAA, @taskBB, @taskCC]
|
|
waitsForPromise =>
|
|
TaskQueue._processTask(@taskAA).then =>
|
|
expect(TaskQueue.dequeue.calls.length).toBe 2
|
|
# NOTE: The recursion goes depth-first. The leafs are called
|
|
# first
|
|
expect(TaskQueue.dequeue.calls[0].args[0]).toBe @taskBB
|
|
expect(TaskQueue.dequeue.calls[1].args[0]).toBe @taskAA
|
|
expect(spyAACallback).not.toHaveBeenCalled()
|
|
expect(spyBBCallback).toHaveBeenCalledWith(@taskAA, @testError)
|
|
expect(@taskAA.queueState.remoteError.message).toBe "Test Error"
|
|
expect(@taskBB.queueState.status).toBe Task.Status.Continue
|
|
expect(@taskBB.queueState.debugStatus).toBe Task.DebugStatus.DequeuedDependency
|
|
|
|
it "dequeues all dependent tasks except those that return `Task.DO_NOT_DEQUEUE_ME` from their callbacks", ->
|
|
spyOn(TaskQueue, 'dequeue').andCallThrough()
|
|
TaskQueue._queue = [@taskAA, @taskBB, @taskCC]
|
|
waitsForPromise =>
|
|
TaskQueue._processTask(@taskAA).then =>
|
|
expect(TaskQueue._queue).toEqual [@taskCC]
|
|
expect(spyCCCallback).toHaveBeenCalledWith(@taskBB, @testError)
|
|
expect(@taskCC.queueState.status).toBe null
|
|
expect(@taskCC.queueState.debugStatus).toBe Task.DebugStatus.JustConstructed
|
|
|