/** * 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.kashilo.com/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 }) })