test: add service tests for DirectusClient, DirectusError, categories, files, listings, and notifications
This commit is contained in:
323
tests/client.test.js
Normal file
323
tests/client.test.js
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
/**
|
||||||
|
* Tests for js/services/directus/client.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, asyncTest, describe, assertEquals, assertTrue, assertFalse, assertThrows, assertDeepEquals, assertNotEquals } from './test-runner.js'
|
||||||
|
import { client, DirectusError, setPersist, getPersist } from '../js/services/directus/client.js'
|
||||||
|
|
||||||
|
// ── DirectusError ──
|
||||||
|
|
||||||
|
describe('DirectusError', () => {
|
||||||
|
test('creates error with correct properties', () => {
|
||||||
|
const err = new DirectusError(404, 'Not found', { foo: 'bar' })
|
||||||
|
assertEquals(err.status, 404)
|
||||||
|
assertEquals(err.message, 'Not found')
|
||||||
|
assertEquals(err.name, 'DirectusError')
|
||||||
|
assertDeepEquals(err.data, { foo: 'bar' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('extends Error', () => {
|
||||||
|
const err = new DirectusError(500, 'fail')
|
||||||
|
assertTrue(err instanceof Error)
|
||||||
|
assertTrue(err instanceof DirectusError)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('defaults data to empty object', () => {
|
||||||
|
const err = new DirectusError(500, 'fail')
|
||||||
|
assertDeepEquals(err.data, {})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAuthError returns true for 401', () => {
|
||||||
|
assertTrue(new DirectusError(401, 'Unauthorized').isAuthError())
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAuthError returns true for 403', () => {
|
||||||
|
assertTrue(new DirectusError(403, 'Forbidden').isAuthError())
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAuthError returns false for other codes', () => {
|
||||||
|
assertFalse(new DirectusError(404, 'Not found').isAuthError())
|
||||||
|
assertFalse(new DirectusError(500, 'Server error').isAuthError())
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isNotFound returns true for 404', () => {
|
||||||
|
assertTrue(new DirectusError(404, 'Not found').isNotFound())
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isNotFound returns false for other codes', () => {
|
||||||
|
assertFalse(new DirectusError(401, 'Unauthorized').isNotFound())
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isValidationError returns true for 400', () => {
|
||||||
|
assertTrue(new DirectusError(400, 'Bad request').isValidationError())
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isValidationError returns false for other codes', () => {
|
||||||
|
assertFalse(new DirectusError(404, 'Not found').isValidationError())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── buildQueryString ──
|
||||||
|
|
||||||
|
describe('DirectusClient.buildQueryString', () => {
|
||||||
|
test('builds simple key-value pairs', () => {
|
||||||
|
const qs = client.buildQueryString({ limit: 10, offset: 5 })
|
||||||
|
assertTrue(qs.includes('limit=10'))
|
||||||
|
assertTrue(qs.includes('offset=5'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('joins array fields with comma', () => {
|
||||||
|
const qs = client.buildQueryString({ fields: ['id', 'title', 'status'] })
|
||||||
|
assertTrue(qs.includes('fields=id%2Ctitle%2Cstatus') || qs.includes('fields=id,title,status'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('joins array sort with comma', () => {
|
||||||
|
const qs = client.buildQueryString({ sort: ['-date_created', 'title'] })
|
||||||
|
assertTrue(qs.includes('sort='))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('stringifies object values as JSON', () => {
|
||||||
|
const filter = { status: { _eq: 'published' } }
|
||||||
|
const qs = client.buildQueryString({ filter })
|
||||||
|
assertTrue(qs.includes('filter='))
|
||||||
|
assertTrue(qs.includes('published'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('skips null and undefined values', () => {
|
||||||
|
const qs = client.buildQueryString({ a: null, b: undefined, c: 'value' })
|
||||||
|
assertFalse(qs.includes('a='))
|
||||||
|
assertFalse(qs.includes('b='))
|
||||||
|
assertTrue(qs.includes('c=value'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns empty string for empty params', () => {
|
||||||
|
assertEquals(client.buildQueryString({}), '')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Persist toggle ──
|
||||||
|
|
||||||
|
describe('setPersist / getPersist', () => {
|
||||||
|
test('defaults to false after module load', () => {
|
||||||
|
// Note: may have been set by loadTokens() — just verify type
|
||||||
|
assertEquals(typeof getPersist(), 'boolean')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('setPersist changes value', () => {
|
||||||
|
const original = getPersist()
|
||||||
|
setPersist(true)
|
||||||
|
assertEquals(getPersist(), true)
|
||||||
|
setPersist(false)
|
||||||
|
assertEquals(getPersist(), false)
|
||||||
|
setPersist(original)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Token management ──
|
||||||
|
|
||||||
|
describe('DirectusClient token management', () => {
|
||||||
|
test('clearTokens resets all token state', () => {
|
||||||
|
client.clearTokens()
|
||||||
|
assertEquals(client.accessToken, null)
|
||||||
|
assertEquals(client.refreshToken, null)
|
||||||
|
assertEquals(client.tokenExpiry, null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAuthenticated returns false without token', () => {
|
||||||
|
client.clearTokens()
|
||||||
|
assertFalse(client.isAuthenticated())
|
||||||
|
})
|
||||||
|
|
||||||
|
test('saveTokens stores tokens and sets expiry', () => {
|
||||||
|
client.saveTokens('test-access', 'test-refresh', 900000)
|
||||||
|
assertEquals(client.accessToken, 'test-access')
|
||||||
|
assertEquals(client.refreshToken, 'test-refresh')
|
||||||
|
assertTrue(client.tokenExpiry > Date.now())
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
client.clearTokens()
|
||||||
|
sessionStorage.removeItem('dgray_auth')
|
||||||
|
localStorage.removeItem('dgray_auth')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAuthenticated returns true with valid token', () => {
|
||||||
|
client.saveTokens('valid-token', 'refresh', 900000)
|
||||||
|
assertTrue(client.isAuthenticated())
|
||||||
|
|
||||||
|
client.clearTokens()
|
||||||
|
sessionStorage.removeItem('dgray_auth')
|
||||||
|
localStorage.removeItem('dgray_auth')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isAuthenticated returns false when token expired', () => {
|
||||||
|
client.accessToken = 'expired'
|
||||||
|
client.tokenExpiry = Date.now() - 10000
|
||||||
|
assertFalse(client.isAuthenticated())
|
||||||
|
|
||||||
|
client.clearTokens()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('loadTokens restores from storage', () => {
|
||||||
|
const authData = JSON.stringify({
|
||||||
|
accessToken: 'stored-access',
|
||||||
|
refreshToken: 'stored-refresh',
|
||||||
|
expiry: Date.now() + 600000
|
||||||
|
})
|
||||||
|
sessionStorage.setItem('dgray_auth', authData)
|
||||||
|
|
||||||
|
client.loadTokens()
|
||||||
|
assertEquals(client.accessToken, 'stored-access')
|
||||||
|
assertEquals(client.refreshToken, 'stored-refresh')
|
||||||
|
|
||||||
|
client.clearTokens()
|
||||||
|
sessionStorage.removeItem('dgray_auth')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('loadTokens clears on invalid JSON', () => {
|
||||||
|
sessionStorage.setItem('dgray_auth', 'not-json')
|
||||||
|
client.loadTokens()
|
||||||
|
assertEquals(client.accessToken, null)
|
||||||
|
|
||||||
|
sessionStorage.removeItem('dgray_auth')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── HTTP request with mocked fetch ──
|
||||||
|
|
||||||
|
describe('DirectusClient.request', () => {
|
||||||
|
asyncTest('makes GET request with auth header', async () => {
|
||||||
|
const originalFetch = window.fetch
|
||||||
|
|
||||||
|
client.accessToken = 'mock-token'
|
||||||
|
let capturedHeaders = null
|
||||||
|
|
||||||
|
window.fetch = async (url, opts) => {
|
||||||
|
capturedHeaders = opts.headers
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({ data: { id: 1 } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await client.request('/items/test', { method: 'GET' })
|
||||||
|
assertEquals(capturedHeaders['Authorization'], 'Bearer mock-token')
|
||||||
|
assertEquals(result.data.id, 1)
|
||||||
|
|
||||||
|
window.fetch = originalFetch
|
||||||
|
client.clearTokens()
|
||||||
|
})
|
||||||
|
|
||||||
|
asyncTest('throws DirectusError on non-ok response', async () => {
|
||||||
|
const originalFetch = window.fetch
|
||||||
|
|
||||||
|
window.fetch = async () => ({
|
||||||
|
ok: false,
|
||||||
|
status: 404,
|
||||||
|
json: async () => ({ errors: [{ message: 'Item not found' }] })
|
||||||
|
})
|
||||||
|
|
||||||
|
let caught = null
|
||||||
|
try {
|
||||||
|
await client.request('/items/missing')
|
||||||
|
} catch (e) {
|
||||||
|
caught = e
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(caught instanceof DirectusError)
|
||||||
|
assertEquals(caught.status, 404)
|
||||||
|
assertTrue(caught.isNotFound())
|
||||||
|
|
||||||
|
window.fetch = originalFetch
|
||||||
|
})
|
||||||
|
|
||||||
|
asyncTest('throws DirectusError with status 0 on network error', async () => {
|
||||||
|
const originalFetch = window.fetch
|
||||||
|
|
||||||
|
window.fetch = async () => { throw new TypeError('Failed to fetch') }
|
||||||
|
|
||||||
|
let caught = null
|
||||||
|
try {
|
||||||
|
await client.request('/items/test')
|
||||||
|
} catch (e) {
|
||||||
|
caught = e
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(caught instanceof DirectusError)
|
||||||
|
assertEquals(caught.status, 0)
|
||||||
|
assertEquals(caught.message, 'Network error')
|
||||||
|
|
||||||
|
window.fetch = originalFetch
|
||||||
|
})
|
||||||
|
|
||||||
|
asyncTest('returns null for 204 No Content', async () => {
|
||||||
|
const originalFetch = window.fetch
|
||||||
|
|
||||||
|
window.fetch = async () => ({
|
||||||
|
ok: true,
|
||||||
|
status: 204
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await client.request('/items/test', { method: 'DELETE' })
|
||||||
|
assertEquals(result, null)
|
||||||
|
|
||||||
|
window.fetch = originalFetch
|
||||||
|
})
|
||||||
|
|
||||||
|
asyncTest('retries on 429 rate limit', async () => {
|
||||||
|
const originalFetch = window.fetch
|
||||||
|
let callCount = 0
|
||||||
|
|
||||||
|
window.fetch = async () => {
|
||||||
|
callCount++
|
||||||
|
if (callCount === 1) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: 429,
|
||||||
|
headers: { get: () => '0' },
|
||||||
|
json: async () => ({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({ data: 'ok' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await client.request('/items/test')
|
||||||
|
assertEquals(callCount, 2)
|
||||||
|
assertEquals(result.data, 'ok')
|
||||||
|
|
||||||
|
window.fetch = originalFetch
|
||||||
|
})
|
||||||
|
|
||||||
|
asyncTest('does not retry 401 on auth endpoints', async () => {
|
||||||
|
const originalFetch = window.fetch
|
||||||
|
client.refreshToken = 'some-token'
|
||||||
|
let callCount = 0
|
||||||
|
|
||||||
|
window.fetch = async () => {
|
||||||
|
callCount++
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: 401,
|
||||||
|
json: async () => ({ errors: [{ message: 'Unauthorized' }] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let caught = null
|
||||||
|
try {
|
||||||
|
await client.request('/auth/login')
|
||||||
|
} catch (e) {
|
||||||
|
caught = e
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(caught instanceof DirectusError)
|
||||||
|
assertEquals(caught.status, 401)
|
||||||
|
assertEquals(callCount, 1)
|
||||||
|
|
||||||
|
window.fetch = originalFetch
|
||||||
|
client.clearTokens()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -93,17 +93,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { renderResults } from './test-runner.js'
|
import { renderResults, runAsyncTests } from './test-runner.js'
|
||||||
|
|
||||||
const container = document.getElementById('results')
|
const container = document.getElementById('results')
|
||||||
|
|
||||||
// Run all test files
|
// Run all test files
|
||||||
async function runTests() {
|
async function runTests() {
|
||||||
try {
|
try {
|
||||||
|
// Sync tests
|
||||||
await import('./helpers.test.js')
|
await import('./helpers.test.js')
|
||||||
await import('./i18n.test.js')
|
await import('./i18n.test.js')
|
||||||
await import('./router.test.js')
|
await import('./router.test.js')
|
||||||
|
|
||||||
|
// Service tests (sync + async)
|
||||||
|
await import('./client.test.js')
|
||||||
|
await import('./services.test.js')
|
||||||
|
|
||||||
|
// Run queued async tests
|
||||||
|
await runAsyncTests()
|
||||||
|
|
||||||
// Clear loading message and render results
|
// Clear loading message and render results
|
||||||
container.querySelector('.loading').remove()
|
container.querySelector('.loading').remove()
|
||||||
renderResults(container)
|
renderResults(container)
|
||||||
|
|||||||
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
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -107,6 +107,52 @@ export function assertDeepEquals(actual, expected, message = '') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert inequality
|
||||||
|
* @param {*} actual
|
||||||
|
* @param {*} expected
|
||||||
|
* @param {string} [message]
|
||||||
|
*/
|
||||||
|
export function assertNotEquals(actual, expected, message = '') {
|
||||||
|
if (actual === expected) {
|
||||||
|
throw new Error(
|
||||||
|
message || `Expected values to differ, but both are ${JSON.stringify(actual)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const asyncQueue = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an async test
|
||||||
|
* @param {string} name - Test name
|
||||||
|
* @param {Function} fn - Async test function
|
||||||
|
*/
|
||||||
|
export function asyncTest(name, fn) {
|
||||||
|
asyncQueue.push({ name, fn })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run all queued async tests sequentially
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function runAsyncTests() {
|
||||||
|
for (const { name, fn } of asyncQueue) {
|
||||||
|
try {
|
||||||
|
await fn()
|
||||||
|
results.passed++
|
||||||
|
results.tests.push({ name, status: 'passed' })
|
||||||
|
console.log(`✓ ${name}`)
|
||||||
|
} catch (error) {
|
||||||
|
results.failed++
|
||||||
|
results.tests.push({ name, status: 'failed', error: error.message })
|
||||||
|
console.error(`✗ ${name}`)
|
||||||
|
console.error(` ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
asyncQueue.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get test results
|
* Get test results
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
|
|||||||
Reference in New Issue
Block a user