Skip to content

Commit 8e01986

Browse files
lethemanhlethemanh
authored andcommitted
feat: Implement unit test ✅
1 parent 9ef25cd commit 8e01986

8 files changed

Lines changed: 632 additions & 3 deletions

File tree

packages/cozy-search/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
"directory": "packages/cozy-search"
8484
},
8585
"scripts": {
86-
"build": "yarn build:clean && yarn build:types && babel --extensions .ts,.tsx,.js,.jsx --ignore '**/*.spec.tsx','**/*.spec.ts','**/*.d.ts' ./src -d ./dist --copy-files",
86+
"build": "yarn build:clean && yarn build:types && babel --extensions .ts,.tsx,.js,.jsx --ignore '**/*.spec.tsx','**/*.spec.ts','**/*.spec.js','**/*.spec.jsx','**/*.d.ts' ./src -d ./dist --copy-files",
8787
"build:clean": "rm -rf ./dist",
8888
"build:types": "tsc -p tsconfig-build.json",
8989
"build:watch": "yarn build --watch",
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { groupConversationsByDate } from './helpers'
2+
3+
describe('groupConversationsByDate', () => {
4+
let OriginalDate
5+
6+
beforeEach(() => {
7+
OriginalDate = global.Date
8+
global.Date = class extends OriginalDate {
9+
constructor(...args) {
10+
if (args.length) return new OriginalDate(...args)
11+
return new OriginalDate('2023-11-20T12:00:00Z')
12+
}
13+
}
14+
global.Date.now = jest.fn(() =>
15+
new OriginalDate('2023-11-20T12:00:00Z').getTime()
16+
)
17+
})
18+
19+
afterEach(() => {
20+
global.Date = OriginalDate
21+
})
22+
23+
it('returns empty groups for null or undefined input', () => {
24+
expect(groupConversationsByDate(null)).toEqual({ today: [], older: [] })
25+
expect(groupConversationsByDate(undefined)).toEqual({
26+
today: [],
27+
older: []
28+
})
29+
})
30+
31+
it('groups conversations into today and older', () => {
32+
const mockConversations = [
33+
{
34+
id: '1',
35+
cozyMetadata: {
36+
updatedAt: new OriginalDate(2023, 10, 20, 14, 0).toISOString()
37+
}
38+
}, // Today
39+
{
40+
id: '2',
41+
cozyMetadata: {
42+
updatedAt: new OriginalDate(2023, 10, 20, 8, 0).toISOString()
43+
}
44+
}, // Today
45+
{
46+
id: '3',
47+
cozyMetadata: {
48+
updatedAt: new OriginalDate(2023, 10, 19, 23, 59).toISOString()
49+
}
50+
}, // Older
51+
{
52+
id: '4',
53+
cozyMetadata: {
54+
updatedAt: new OriginalDate(2022, 0, 1, 12, 0).toISOString()
55+
}
56+
}, // Older
57+
{ id: '5' } // Missing cozyMetadata (Date.now() fallback -> Today)
58+
]
59+
60+
const result = groupConversationsByDate(mockConversations)
61+
62+
expect(result.today).toHaveLength(3)
63+
expect(result.today[0].id).toBe('1')
64+
expect(result.today[1].id).toBe('2')
65+
expect(result.today[2].id).toBe('5')
66+
67+
expect(result.older).toHaveLength(2)
68+
expect(result.older[0].id).toBe('3')
69+
expect(result.older[1].id).toBe('4')
70+
})
71+
})
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { StreamBridge } from './StreamBridge'
2+
3+
describe('StreamBridge', () => {
4+
let bridge: StreamBridge
5+
6+
beforeEach(() => {
7+
bridge = new StreamBridge()
8+
})
9+
10+
it('should create an async iterable iterator', () => {
11+
const iterator = bridge.createStream('convo_1')
12+
expect(iterator.next).toBeDefined()
13+
expect(iterator[Symbol.asyncIterator]).toBeDefined()
14+
})
15+
16+
it('should push deltas and yield them from the iterator', async () => {
17+
const iterator = bridge.createStream('convo_1')
18+
19+
bridge.onDelta('convo_1', 'Hello ')
20+
bridge.onDelta('convo_1', 'world!')
21+
22+
const first = await iterator.next()
23+
expect(first).toEqual({ value: 'Hello ', done: false })
24+
25+
const second = await iterator.next()
26+
expect(second).toEqual({ value: 'world!', done: false })
27+
})
28+
29+
it('should mark the stream as done when complete is called', async () => {
30+
const iterator = bridge.createStream('convo_1')
31+
32+
bridge.onDelta('convo_1', 'done chunk')
33+
bridge.onDone('convo_1')
34+
35+
const first = await iterator.next()
36+
expect(first).toEqual({ value: 'done chunk', done: false })
37+
38+
const second = await iterator.next()
39+
expect(second.done).toBe(true)
40+
})
41+
42+
it('should reject the iterator when an error occurs', async () => {
43+
const iterator = bridge.createStream('convo_1')
44+
const error = new Error('Socket disconnected')
45+
46+
bridge.onError('convo_1', error)
47+
48+
await expect(iterator.next()).rejects.toThrow('Socket disconnected')
49+
})
50+
51+
it('should call the cleanup callback and mark the stream complete on cleanup', async () => {
52+
const cleanupSpy = jest.fn()
53+
bridge.setCleanupCallback(cleanupSpy)
54+
55+
const iterator = bridge.createStream('convo_1')
56+
bridge.cleanup('convo_1')
57+
58+
expect(cleanupSpy).toHaveBeenCalledTimes(1)
59+
expect(bridge.hasStream('convo_1')).toBe(false)
60+
61+
// The iterator should be marked done
62+
const result = await iterator.next()
63+
expect(result.done).toBe(true)
64+
})
65+
66+
it('multiple unresolved next calls should reject to prevent concurrency issues', async () => {
67+
const iterator = bridge.createStream('convo_1')
68+
69+
// Call next twice concurrently
70+
const p1 = iterator.next()
71+
const p2 = iterator.next()
72+
73+
await expect(p2).rejects.toThrow(
74+
'StreamBridge: concurrent next() calls are not supported'
75+
)
76+
77+
// Fulfill the first one
78+
bridge.onDelta('convo_1', 'ok')
79+
const res1 = await p1
80+
expect(res1).toEqual({ value: 'ok', done: false })
81+
})
82+
})

packages/cozy-search/src/components/helpers.spec.js

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { sanitizeChatContent } from './helpers'
1+
import {
2+
sanitizeChatContent,
3+
formatConversationDate,
4+
getNameOfConversation,
5+
getDescriptionOfConversation
6+
} from './helpers'
7+
8+
jest.mock('cozy-flags', () => jest.fn(() => true), { virtual: true })
29

310
describe('sanitizeChatContent', () => {
411
it('should return empty string for empty content', () => {
@@ -41,3 +48,92 @@ describe('sanitizeChatContent', () => {
4148
expect(sanitizeChatContent(text)).toBe(text)
4249
})
4350
})
51+
52+
describe('formatConversationDate', () => {
53+
const mockT = jest.fn(key => key)
54+
const mockDate = new Date('2023-11-20T12:00:00Z')
55+
let OriginalDate
56+
let dateSpy
57+
58+
beforeEach(() => {
59+
OriginalDate = global.Date
60+
dateSpy = jest.spyOn(global, 'Date').mockImplementation(function (...args) {
61+
if (args.length) {
62+
return new OriginalDate(...args)
63+
}
64+
return mockDate
65+
})
66+
dateSpy.now = jest.fn(() => mockDate.getTime())
67+
})
68+
69+
afterEach(() => {
70+
dateSpy.mockRestore()
71+
mockT.mockClear()
72+
})
73+
74+
it('returns empty string for invalid dates', () => {
75+
expect(formatConversationDate(null, mockT, 'en-US')).toBe('')
76+
expect(formatConversationDate('not date', mockT, 'en-US')).toBe('')
77+
})
78+
79+
it('formats today as "Today, HH:mm"', () => {
80+
const today = new Date('2023-11-20T08:30:00Z').toISOString()
81+
const result = formatConversationDate(today, mockT, 'en-US')
82+
83+
expect(result).toMatch(/assistant\.time\.today/)
84+
expect(result).toMatch(/\d{1,2}:\d{2}/)
85+
})
86+
87+
it('formats yesterday as "Yesterday, HH:mm"', () => {
88+
const yesterday = new Date('2023-11-19T14:45:00Z').toISOString()
89+
const result = formatConversationDate(yesterday, mockT, 'en-US')
90+
91+
expect(result).toMatch(/assistant\.time\.yesterday/)
92+
expect(result).toMatch(/\d{1,2}:\d{2}/)
93+
})
94+
95+
it('formats older dates as formatted short date strings', () => {
96+
const older = new Date('2022-01-05T10:00:00Z').toISOString()
97+
const result = formatConversationDate(older, mockT, 'en-US')
98+
99+
expect(result).toContain('2022')
100+
expect(result).toContain('Jan')
101+
})
102+
})
103+
104+
describe('getNameOfConversation', () => {
105+
it('returns undefined if messages array is empty or missing', () => {
106+
expect(getNameOfConversation({})).toBeUndefined()
107+
expect(getNameOfConversation({ messages: [] })).toBeUndefined()
108+
expect(
109+
getNameOfConversation({ messages: [{ content: 'Hi' }] })
110+
).toBeUndefined()
111+
})
112+
113+
it('returns the content of the second to last message', () => {
114+
const convo = {
115+
messages: [
116+
{ role: 'user', content: 'What is the sum?' },
117+
{ role: 'assistant', content: 'It is 4' }
118+
]
119+
}
120+
expect(getNameOfConversation(convo)).toBe('What is the sum?')
121+
})
122+
})
123+
124+
describe('getDescriptionOfConversation', () => {
125+
it('returns undefined if messages array is empty or missing', () => {
126+
expect(getDescriptionOfConversation({})).toBeUndefined()
127+
expect(getDescriptionOfConversation({ messages: [] })).toBeUndefined()
128+
})
129+
130+
it('returns the content of the last message', () => {
131+
const convo = {
132+
messages: [
133+
{ role: 'user', content: 'What is the sum?' },
134+
{ role: 'assistant', content: 'It is 4' }
135+
]
136+
}
137+
expect(getDescriptionOfConversation(convo)).toBe('It is 4')
138+
})
139+
})

packages/cozy-search/src/components/queries.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ export const buildChatConversationsQuery = () => {
7777
.offsetBookmark(bookmark)
7878
.limitBy(FETCH_CONVERSATIONS_LIMIT),
7979
options: ({ bookmark, query = {} }) => ({
80-
as: `${CHAT_CONVERSATIONS_DOCTYPE}/recent-${bookmark || ''}-${JSON.stringify(query)}`,
80+
as: `${CHAT_CONVERSATIONS_DOCTYPE}/recent-${
81+
bookmark || ''
82+
}-${JSON.stringify(query)}`,
8183
fetchPolicy: defaultFetchPolicy
8284
})
8385
}

0 commit comments

Comments
 (0)