Skip to content

Commit 0d76d00

Browse files
committed
test: add tests
1 parent 374498c commit 0d76d00

File tree

10 files changed

+813
-84
lines changed

10 files changed

+813
-84
lines changed

bunfig.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
preload = ["./src/__tests__/setup.ts"]
33
coverageSkipTestFiles = true
44
coverageIgnoreSourcemaps = true
5-
coverageThreshold = { lines = 0.9, functions = 0.8, statements = 0.85 }
5+
# https://github.com/oven-sh/bun/issues/7662
6+
coverageThreshold = { functions = 0, lines = 0.8 }
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { beforeEach, describe, expect, it } from 'bun:test'
2+
import {
3+
NotImplementedException,
4+
UnsupportedFieldException,
5+
} from '@/exceptions'
6+
import { load } from 'cheerio'
7+
import { ExtractorPlugin } from '../abstract-extractor-plugin'
8+
import type { RecipeFields } from '../types/recipe.interface'
9+
10+
class MockExtractorPlugin extends ExtractorPlugin {
11+
name = 'MockExtractorPlugin'
12+
priority = 100
13+
14+
private supportedFields: Set<keyof RecipeFields>
15+
16+
constructor(supportedFields: (keyof RecipeFields)[] = []) {
17+
const $ = load('<html><body></body></html>')
18+
super($)
19+
this.supportedFields = new Set(supportedFields)
20+
}
21+
22+
supports(field: keyof RecipeFields): boolean {
23+
return this.supportedFields.has(field)
24+
}
25+
26+
extract<Key extends keyof RecipeFields>(field: Key): RecipeFields[Key] {
27+
if (!this.supports(field)) {
28+
throw new UnsupportedFieldException(field)
29+
}
30+
31+
// Mock extraction logic
32+
switch (field) {
33+
case 'title':
34+
return 'Mock Recipe Title' as RecipeFields[Key]
35+
case 'description':
36+
return 'Mock Recipe Description' as RecipeFields[Key]
37+
case 'ingredients':
38+
return new Set(['ingredient 1', 'ingredient 2']) as RecipeFields[Key]
39+
case 'instructions':
40+
return new Set(['step 1', 'step 2']) as RecipeFields[Key]
41+
case 'prepTime':
42+
return 15 as RecipeFields[Key]
43+
case 'cookTime':
44+
return 30 as RecipeFields[Key]
45+
case 'totalTime':
46+
return 45 as RecipeFields[Key]
47+
case 'yields':
48+
return '4 servings' as RecipeFields[Key]
49+
default:
50+
throw new NotImplementedException(field)
51+
}
52+
}
53+
}
54+
55+
class AsyncMockExtractorPlugin extends ExtractorPlugin {
56+
name = 'AsyncMockExtractorPlugin'
57+
priority = 100
58+
59+
constructor() {
60+
const $ = load('<html><body></body></html>')
61+
super($)
62+
}
63+
64+
supports(field: keyof RecipeFields): boolean {
65+
return ['title', 'description'].includes(field)
66+
}
67+
68+
async extract<Key extends keyof RecipeFields>(
69+
field: Key,
70+
): Promise<RecipeFields[Key]> {
71+
await new Promise((resolve) => setTimeout(resolve, 10))
72+
73+
if (!this.supports(field)) {
74+
throw new UnsupportedFieldException(field)
75+
}
76+
77+
switch (field) {
78+
case 'title':
79+
return 'Async Recipe Title' as RecipeFields[Key]
80+
case 'description':
81+
return 'Async Recipe Description' as RecipeFields[Key]
82+
default:
83+
throw new NotImplementedException(field)
84+
}
85+
}
86+
}
87+
88+
class ThrowingExtractorPlugin extends ExtractorPlugin {
89+
name = 'ThrowingExtractorPlugin'
90+
priority = 100
91+
92+
constructor() {
93+
const $ = load('<html><body></body></html>')
94+
super($)
95+
}
96+
97+
supports(field: keyof RecipeFields): boolean {
98+
return true
99+
}
100+
101+
extract<Key extends keyof RecipeFields>(field: Key): RecipeFields[Key] {
102+
throw new Error(`Extraction failed for field: ${String(field)}`)
103+
}
104+
}
105+
106+
describe('ExtractorPlugin', () => {
107+
let plugin: MockExtractorPlugin
108+
109+
beforeEach(() => {
110+
plugin = new MockExtractorPlugin([
111+
'title',
112+
'description',
113+
'ingredients',
114+
'prepTime',
115+
])
116+
})
117+
118+
describe('inheritance', () => {
119+
it('should extend AbstractPlugin', () => {
120+
expect(plugin).toBeInstanceOf(ExtractorPlugin)
121+
})
122+
123+
it('should have access to cheerio instance from parent', () => {
124+
expect(plugin.$).toBeDefined()
125+
expect(typeof plugin.$).toBe('function')
126+
})
127+
})
128+
129+
describe('supports method', () => {
130+
it('should return true for supported fields', () => {
131+
expect(plugin.supports('title')).toBe(true)
132+
expect(plugin.supports('description')).toBe(true)
133+
expect(plugin.supports('ingredients')).toBe(true)
134+
expect(plugin.supports('prepTime')).toBe(true)
135+
})
136+
137+
it('should return false for unsupported fields', () => {
138+
expect(plugin.supports('cookTime')).toBe(false)
139+
expect(plugin.supports('totalTime')).toBe(false)
140+
expect(plugin.supports('yields')).toBe(false)
141+
expect(plugin.supports('author')).toBe(false)
142+
})
143+
144+
it('should handle empty supported fields', () => {
145+
const emptyPlugin = new MockExtractorPlugin([])
146+
expect(emptyPlugin.supports('title')).toBe(false)
147+
expect(emptyPlugin.supports('description')).toBe(false)
148+
})
149+
150+
it('should handle all fields as supported', () => {
151+
const allFieldsPlugin = new MockExtractorPlugin([
152+
'title',
153+
'description',
154+
'ingredients',
155+
'instructions',
156+
'prepTime',
157+
'cookTime',
158+
'totalTime',
159+
'yields',
160+
])
161+
162+
expect(allFieldsPlugin.supports('title')).toBe(true)
163+
expect(allFieldsPlugin.supports('cookTime')).toBe(true)
164+
expect(allFieldsPlugin.supports('yields')).toBe(true)
165+
})
166+
})
167+
168+
describe('extract method', () => {
169+
it('should extract supported fields', () => {
170+
expect(plugin.extract('title')).toBe('Mock Recipe Title')
171+
expect(plugin.extract('description')).toBe('Mock Recipe Description')
172+
expect(plugin.extract('prepTime')).toBe(15)
173+
expect(plugin.extract('ingredients')).toEqual(
174+
new Set(['ingredient 1', 'ingredient 2']),
175+
)
176+
})
177+
178+
it('should throw error for unsupported fields', () => {
179+
expect(() => plugin.extract('cookTime')).toThrow(
180+
'Extraction not supported for field: cookTime',
181+
)
182+
expect(() => plugin.extract('totalTime')).toThrow(
183+
'Extraction not supported for field: totalTime',
184+
)
185+
})
186+
})
187+
188+
describe('async extraction', () => {
189+
let asyncPlugin: AsyncMockExtractorPlugin
190+
191+
beforeEach(() => {
192+
asyncPlugin = new AsyncMockExtractorPlugin()
193+
})
194+
195+
it('should handle async extraction', async () => {
196+
const title = await asyncPlugin.extract('title')
197+
expect(title).toBe('Async Recipe Title')
198+
const description = await asyncPlugin.extract('description')
199+
expect(description).toBe('Async Recipe Description')
200+
})
201+
202+
it('should throw error for unsupported fields in async mode', async () => {
203+
await expect(asyncPlugin.extract('cookTime')).rejects.toThrow(
204+
'Extraction not supported for field: cookTime',
205+
)
206+
})
207+
})
208+
209+
describe('error handling', () => {
210+
let throwingPlugin: ThrowingExtractorPlugin
211+
212+
beforeEach(() => {
213+
throwingPlugin = new ThrowingExtractorPlugin()
214+
})
215+
216+
it('should propagate extraction errors', () => {
217+
expect(() => throwingPlugin.extract('title')).toThrow(
218+
'Extraction failed for field: title',
219+
)
220+
expect(() => throwingPlugin.extract('description')).toThrow(
221+
'Extraction failed for field: description',
222+
)
223+
})
224+
})
225+
226+
describe('edge cases', () => {
227+
it('should throw on undefined extractor', () => {
228+
const plugin = new MockExtractorPlugin(['author'])
229+
expect(() => plugin.extract('author')).toThrow(
230+
'Method should be implemented: author',
231+
)
232+
})
233+
})
234+
})

0 commit comments

Comments
 (0)