All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 46s
Build and Push Docker Images / build-frontend (push) Successful in 1m15s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-api (push) Successful in 21s
Build and Push Docker Images / build-central-services-admin (push) Successful in 21s
Build and Push Docker Images / build-docs (push) Successful in 1m21s
98 lines
4.5 KiB
JavaScript
98 lines
4.5 KiB
JavaScript
const { test } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const vm = require('node:vm')
|
|
const ts = require('typescript')
|
|
|
|
// Exercise the page's real handlers with delayed API responses and Vue ref stubs.
|
|
function setup() {
|
|
const requests = []
|
|
const notices = []
|
|
const context = vm.createContext({
|
|
ref: value => ({ value }),
|
|
computed: getter => ({ get value() { return getter() } }),
|
|
watch() {}, onMounted() {},
|
|
useRoute: () => ({ query: {} }), useRuntimeConfig: () => ({}),
|
|
useToast: () => ({ add: notice => notices.push(notice) }),
|
|
window: { confirm: () => true },
|
|
useNuxtApp: () => ({ $api: (url, options) => new Promise((resolve, reject) => requests.push({ url, options, resolve, reject })) }),
|
|
})
|
|
const source = fs.readFileSync(`${__dirname}/../pages/email/index.vue`, 'utf8')
|
|
.split('<script setup lang="ts">')[1].split('</script>')[0].replace(/^import .*$/gm, '')
|
|
vm.runInContext(ts.transpile(source, { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.None }), context)
|
|
const run = code => vm.runInContext(code, context)
|
|
run(`selectedAccountId.value='account'; messages.value=[{id:'a',mailboxPath:'INBOX',seen:false},{id:'b',mailboxPath:'INBOX',seen:false}]; mailboxes.value=[{path:'INBOX',name:'INBOX',exists:2,unseen:2},{path:'Archive',name:'Archive',exists:0,unseen:0}];`)
|
|
return { run, requests, notices }
|
|
}
|
|
|
|
test('whole bulk selection disappears before response; partial failure restores only failed mail and counters', async () => {
|
|
const { run, requests } = setup()
|
|
run(`toggleAllMessages(true); bulkMoveTarget.value='Archive'`)
|
|
const pending = run(`runBulkAction('move')`)
|
|
assert.equal(run('messages.value.length'), 0)
|
|
assert.equal(run('mailboxes.value[1].exists'), 2)
|
|
assert.equal(requests.length, 1)
|
|
requests[0].resolve({ destinationMailboxPath: 'Archive' })
|
|
await new Promise(setImmediate)
|
|
requests[1].reject(Error('offline'))
|
|
await pending
|
|
assert.equal(run('messages.value.map(m=>m.id).join()'), 'b')
|
|
assert.equal(run('checkedMessageIds.value.join()'), 'b')
|
|
assert.equal(run('mailboxes.value[0].unseen'), 1)
|
|
assert.equal(run('mailboxes.value[1].exists'), 1)
|
|
})
|
|
|
|
test('bulk read updates every mail immediately and rolls back individual failures', async () => {
|
|
const { run, requests } = setup()
|
|
run('toggleAllMessages(true)')
|
|
const pending = run(`runBulkAction('read')`)
|
|
assert.equal(run('messages.value.every(m=>m.seen)'), true)
|
|
assert.equal(run('mailboxes.value[0].unseen'), 0)
|
|
requests[0].reject(Error('offline'))
|
|
await new Promise(setImmediate)
|
|
requests[1].resolve({})
|
|
await pending
|
|
assert.equal(run('messages.value[0].seen'), false)
|
|
assert.equal(run('messages.value[1].seen'), true)
|
|
assert.equal(run('mailboxes.value[0].unseen'), 1)
|
|
})
|
|
|
|
test('single deletion is immediate and restores details on failure', async () => {
|
|
const { run, requests } = setup()
|
|
run(`selectedMessage.value={...messages.value[0],body:{text:'Body'}}`)
|
|
const pending = run('deleteSelectedMessage()')
|
|
assert.equal(run('messages.value.length'), 1)
|
|
assert.equal(run('selectedMessage.value'), null)
|
|
requests[0].reject(Error('offline'))
|
|
await pending
|
|
assert.equal(run('messages.value.map(m=>m.id).join()'), 'a,b')
|
|
assert.equal(run('selectedMessage.value.body.text'), 'Body')
|
|
})
|
|
|
|
test('late message response cannot replace newer selection; header and read state are immediate', async () => {
|
|
const { run, requests } = setup()
|
|
const first = run('selectMessage(messages.value[0])')
|
|
assert.equal(run('selectedMessage.value.id'), 'a')
|
|
assert.equal(run('messages.value[0].seen'), true)
|
|
const second = run('selectMessage(messages.value[1])')
|
|
const detailRequests = requests.filter(request => !request.options)
|
|
detailRequests[1].resolve({ id: 'b', body: { text: 'B' }, seen: false })
|
|
await second
|
|
detailRequests[0].resolve({ id: 'a', body: { text: 'A' }, seen: false })
|
|
await first
|
|
assert.equal(run('selectedMessage.value.id'), 'b')
|
|
assert.equal(run('selectedMessage.value.seen'), true)
|
|
assert.equal(run('loadingMessage.value'), false)
|
|
requests.filter(request => request.options).forEach(request => request.resolve({}))
|
|
})
|
|
|
|
test('archiving in archive leaves the message in place', async () => {
|
|
const { run, requests } = setup()
|
|
run(`messages.value[0].mailboxPath='Archive';selectedMessage.value=messages.value[0]`)
|
|
const pending = run('archiveSelectedMessage()')
|
|
assert.equal(run('messages.value.length'), 2)
|
|
requests[0].resolve({ mailboxPath: 'Archive' })
|
|
await pending
|
|
assert.equal(run('messages.value.length'), 2)
|
|
})
|