test: add service tests for DirectusClient, DirectusError, categories, files, listings, and notifications
This commit is contained in:
296
tests/services.test.js
Normal file
296
tests/services.test.js
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* Tests for Directus service modules
|
||||
*/
|
||||
|
||||
import { test, asyncTest, describe, assertEquals, assertTrue, assertFalse, assertDeepEquals, assertNotEquals } from './test-runner.js'
|
||||
import { buildCategoryTree } from '../js/services/directus/categories.js'
|
||||
import { getFileUrl, getThumbnailUrl } from '../js/services/directus/files.js'
|
||||
import { client } from '../js/services/directus/client.js'
|
||||
|
||||
// ── buildCategoryTree (pure function) ──
|
||||
|
||||
describe('buildCategoryTree', () => {
|
||||
const flatCategories = [
|
||||
{ id: '1', name: 'Electronics', parent: null },
|
||||
{ id: '2', name: 'Phones', parent: '1' },
|
||||
{ id: '3', name: 'Laptops', parent: '1' },
|
||||
{ id: '4', name: 'Clothing', parent: null },
|
||||
{ id: '5', name: 'Shirts', parent: '4' },
|
||||
{ id: '6', name: 'iPhones', parent: '2' }
|
||||
]
|
||||
|
||||
test('builds top-level nodes', () => {
|
||||
const tree = buildCategoryTree(flatCategories)
|
||||
assertEquals(tree.length, 2)
|
||||
assertEquals(tree[0].name, 'Electronics')
|
||||
assertEquals(tree[1].name, 'Clothing')
|
||||
})
|
||||
|
||||
test('nests children correctly', () => {
|
||||
const tree = buildCategoryTree(flatCategories)
|
||||
const electronics = tree[0]
|
||||
assertEquals(electronics.children.length, 2)
|
||||
assertEquals(electronics.children[0].name, 'Phones')
|
||||
assertEquals(electronics.children[1].name, 'Laptops')
|
||||
})
|
||||
|
||||
test('handles deep nesting', () => {
|
||||
const tree = buildCategoryTree(flatCategories)
|
||||
const phones = tree[0].children[0]
|
||||
assertEquals(phones.children.length, 1)
|
||||
assertEquals(phones.children[0].name, 'iPhones')
|
||||
})
|
||||
|
||||
test('leaf nodes have empty children array', () => {
|
||||
const tree = buildCategoryTree(flatCategories)
|
||||
const laptops = tree[0].children[1]
|
||||
assertDeepEquals(laptops.children, [])
|
||||
})
|
||||
|
||||
test('returns empty array for empty input', () => {
|
||||
const tree = buildCategoryTree([])
|
||||
assertDeepEquals(tree, [])
|
||||
})
|
||||
|
||||
test('handles categories with parent as object with id', () => {
|
||||
const cats = [
|
||||
{ id: '1', name: 'Root', parent: null },
|
||||
{ id: '2', name: 'Child', parent: { id: '1' } }
|
||||
]
|
||||
const tree = buildCategoryTree(cats)
|
||||
assertEquals(tree.length, 1)
|
||||
assertEquals(tree[0].children.length, 1)
|
||||
assertEquals(tree[0].children[0].name, 'Child')
|
||||
})
|
||||
|
||||
test('builds subtree from specific parent', () => {
|
||||
const tree = buildCategoryTree(flatCategories, '1')
|
||||
assertEquals(tree.length, 2)
|
||||
assertEquals(tree[0].name, 'Phones')
|
||||
assertEquals(tree[1].name, 'Laptops')
|
||||
})
|
||||
})
|
||||
|
||||
// ── getFileUrl / getThumbnailUrl (pure functions) ──
|
||||
|
||||
describe('getFileUrl', () => {
|
||||
test('returns null for falsy fileId', () => {
|
||||
assertEquals(getFileUrl(null), null)
|
||||
assertEquals(getFileUrl(''), null)
|
||||
assertEquals(getFileUrl(undefined), null)
|
||||
})
|
||||
|
||||
test('builds basic URL without options', () => {
|
||||
const url = getFileUrl('abc-123')
|
||||
assertEquals(url, 'https://api.dgray.io/assets/abc-123')
|
||||
})
|
||||
|
||||
test('appends width parameter', () => {
|
||||
const url = getFileUrl('abc-123', { width: 200 })
|
||||
assertTrue(url.includes('width=200'))
|
||||
})
|
||||
|
||||
test('appends multiple parameters', () => {
|
||||
const url = getFileUrl('abc-123', { width: 200, height: 150, fit: 'cover' })
|
||||
assertTrue(url.includes('width=200'))
|
||||
assertTrue(url.includes('height=150'))
|
||||
assertTrue(url.includes('fit=cover'))
|
||||
})
|
||||
|
||||
test('appends quality and format', () => {
|
||||
const url = getFileUrl('abc-123', { quality: 80, format: 'webp' })
|
||||
assertTrue(url.includes('quality=80'))
|
||||
assertTrue(url.includes('format=webp'))
|
||||
})
|
||||
|
||||
test('does not append undefined options', () => {
|
||||
const url = getFileUrl('abc-123', { width: 100 })
|
||||
assertFalse(url.includes('height'))
|
||||
assertFalse(url.includes('fit'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('getThumbnailUrl', () => {
|
||||
test('uses default size of 300', () => {
|
||||
const url = getThumbnailUrl('abc-123')
|
||||
assertTrue(url.includes('width=300'))
|
||||
assertTrue(url.includes('height=300'))
|
||||
})
|
||||
|
||||
test('uses custom size', () => {
|
||||
const url = getThumbnailUrl('abc-123', 150)
|
||||
assertTrue(url.includes('width=150'))
|
||||
assertTrue(url.includes('height=150'))
|
||||
})
|
||||
|
||||
test('uses cover fit and webp format', () => {
|
||||
const url = getThumbnailUrl('abc-123')
|
||||
assertTrue(url.includes('fit=cover'))
|
||||
assertTrue(url.includes('format=webp'))
|
||||
assertTrue(url.includes('quality=80'))
|
||||
})
|
||||
})
|
||||
|
||||
// ── Async service tests with mocked fetch ──
|
||||
|
||||
describe('listings service', () => {
|
||||
asyncTest('getListings calls correct endpoint', async () => {
|
||||
const originalFetch = window.fetch
|
||||
let capturedUrl = null
|
||||
|
||||
window.fetch = async (url, opts) => {
|
||||
capturedUrl = url
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ data: [{ id: 1 }], meta: {} })
|
||||
}
|
||||
}
|
||||
|
||||
const { getListings } = await import('../js/services/directus/listings.js')
|
||||
const result = await getListings()
|
||||
assertTrue(capturedUrl.includes('/items/listings'))
|
||||
assertTrue(Array.isArray(result.items))
|
||||
|
||||
window.fetch = originalFetch
|
||||
})
|
||||
|
||||
asyncTest('getListing fetches single item', async () => {
|
||||
const originalFetch = window.fetch
|
||||
let capturedUrl = null
|
||||
|
||||
window.fetch = async (url) => {
|
||||
capturedUrl = url
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ data: { id: 'test-id', title: 'Test' } })
|
||||
}
|
||||
}
|
||||
|
||||
const { getListing } = await import('../js/services/directus/listings.js')
|
||||
const result = await getListing('test-id')
|
||||
assertTrue(capturedUrl.includes('/items/listings/test-id'))
|
||||
assertEquals(result.title, 'Test')
|
||||
|
||||
window.fetch = originalFetch
|
||||
})
|
||||
|
||||
asyncTest('createListing sends POST', async () => {
|
||||
const originalFetch = window.fetch
|
||||
let capturedMethod = null
|
||||
let capturedBody = null
|
||||
|
||||
window.fetch = async (url, opts) => {
|
||||
capturedMethod = opts.method
|
||||
capturedBody = JSON.parse(opts.body)
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ data: { id: 'new-1', ...capturedBody } })
|
||||
}
|
||||
}
|
||||
|
||||
const { createListing } = await import('../js/services/directus/listings.js')
|
||||
const result = await createListing({ title: 'New Item', price: 100 })
|
||||
assertEquals(capturedMethod, 'POST')
|
||||
assertEquals(capturedBody.title, 'New Item')
|
||||
assertEquals(capturedBody.price, 100)
|
||||
|
||||
window.fetch = originalFetch
|
||||
})
|
||||
|
||||
asyncTest('deleteListing sends DELETE', async () => {
|
||||
const originalFetch = window.fetch
|
||||
let capturedMethod = null
|
||||
|
||||
window.fetch = async (url, opts) => {
|
||||
capturedMethod = opts.method
|
||||
return { ok: true, status: 204 }
|
||||
}
|
||||
|
||||
const { deleteListing } = await import('../js/services/directus/listings.js')
|
||||
await deleteListing('del-1')
|
||||
assertEquals(capturedMethod, 'DELETE')
|
||||
|
||||
window.fetch = originalFetch
|
||||
})
|
||||
})
|
||||
|
||||
describe('categories service (async)', () => {
|
||||
asyncTest('getCategory detects UUID format', async () => {
|
||||
const originalFetch = window.fetch
|
||||
let capturedUrl = null
|
||||
|
||||
window.fetch = async (url) => {
|
||||
capturedUrl = url
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ data: { id: '550e8400-e29b-41d4-a716-446655440000', name: 'Test' } })
|
||||
}
|
||||
}
|
||||
|
||||
const { getCategory } = await import('../js/services/directus/categories.js')
|
||||
await getCategory('550e8400-e29b-41d4-a716-446655440000')
|
||||
assertTrue(capturedUrl.includes('/items/categories/550e8400-e29b-41d4-a716-446655440000'))
|
||||
|
||||
window.fetch = originalFetch
|
||||
})
|
||||
|
||||
asyncTest('getCategory uses slug filter for non-UUID', async () => {
|
||||
const originalFetch = window.fetch
|
||||
let capturedUrl = null
|
||||
|
||||
window.fetch = async (url) => {
|
||||
capturedUrl = url
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ data: [{ id: '1', slug: 'electronics' }] })
|
||||
}
|
||||
}
|
||||
|
||||
const { getCategory } = await import('../js/services/directus/categories.js')
|
||||
await getCategory('electronics')
|
||||
assertTrue(capturedUrl.includes('/items/categories'))
|
||||
assertTrue(capturedUrl.includes('electronics'))
|
||||
assertFalse(capturedUrl.includes('/items/categories/electronics'))
|
||||
|
||||
window.fetch = originalFetch
|
||||
})
|
||||
})
|
||||
|
||||
describe('notifications service', () => {
|
||||
asyncTest('getUnreadCount parses aggregate response', async () => {
|
||||
const originalFetch = window.fetch
|
||||
|
||||
window.fetch = async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ data: [{ count: { id: '5' } }] })
|
||||
})
|
||||
|
||||
const { getUnreadCount } = await import('../js/services/directus/notifications.js')
|
||||
const count = await getUnreadCount('user-hash')
|
||||
assertEquals(count, 5)
|
||||
|
||||
window.fetch = originalFetch
|
||||
})
|
||||
|
||||
asyncTest('getUnreadCount returns 0 for empty response', async () => {
|
||||
const originalFetch = window.fetch
|
||||
|
||||
window.fetch = async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ data: [] })
|
||||
})
|
||||
|
||||
const { getUnreadCount } = await import('../js/services/directus/notifications.js')
|
||||
const count = await getUnreadCount('user-hash')
|
||||
assertEquals(count, 0)
|
||||
|
||||
window.fetch = originalFetch
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user