@lobehub/lobehub 2.0.0-next.25 → 2.0.0-next.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,451 @@
1
+ import { Then, When } from '@cucumber/cucumber';
2
+ import { expect } from '@playwright/test';
3
+
4
+ import { CustomWorld } from '../../support/world';
5
+
6
+ // ============================================
7
+ // When Steps (Actions)
8
+ // ============================================
9
+
10
+ When('I type {string} in the search bar', async function (this: CustomWorld, searchText: string) {
11
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
12
+
13
+ const searchBar = this.page.locator('input[type="text"]').first();
14
+ await searchBar.waitFor({ state: 'visible', timeout: 120_000 });
15
+ await searchBar.fill(searchText);
16
+
17
+ // Store the search text for later assertions
18
+ this.testContext.searchText = searchText;
19
+ });
20
+
21
+ When('I wait for the search results to load', async function (this: CustomWorld) {
22
+ // Wait for network to be idle after typing
23
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
24
+ // Add a small delay to ensure UI updates
25
+ await this.page.waitForTimeout(500);
26
+ });
27
+
28
+ When('I click on a category in the category menu', async function (this: CustomWorld) {
29
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
30
+
31
+ // Find the category menu and click the first non-active category
32
+ const categoryItems = this.page.locator(
33
+ '[data-testid="category-menu"] button, [role="menu"] button, nav[aria-label*="categor" i] button',
34
+ );
35
+
36
+ // Wait for categories to be visible
37
+ await categoryItems.first().waitFor({ state: 'visible', timeout: 120_000 });
38
+
39
+ // Click the second category (skip "All" which is usually first)
40
+ const secondCategory = categoryItems.nth(1);
41
+ await secondCategory.click();
42
+
43
+ // Store the category for later verification
44
+ const categoryText = await secondCategory.textContent();
45
+ this.testContext.selectedCategory = categoryText?.trim();
46
+ });
47
+
48
+ When('I click on a category in the category filter', async function (this: CustomWorld) {
49
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
50
+
51
+ // Find the category filter and click a category
52
+ const categoryItems = this.page.locator(
53
+ '[data-testid="category-filter"] button, [data-testid="category-menu"] button',
54
+ );
55
+
56
+ // Wait for categories to be visible
57
+ await categoryItems.first().waitFor({ state: 'visible', timeout: 120_000 });
58
+
59
+ // Click the second category (skip "All" which is usually first)
60
+ const secondCategory = categoryItems.nth(1);
61
+ await secondCategory.click();
62
+
63
+ // Store the category for later verification
64
+ const categoryText = await secondCategory.textContent();
65
+ this.testContext.selectedCategory = categoryText?.trim();
66
+ });
67
+
68
+ When('I wait for the filtered results to load', async function (this: CustomWorld) {
69
+ // Wait for network to be idle after filtering
70
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
71
+ // Add a small delay to ensure UI updates
72
+ await this.page.waitForTimeout(500);
73
+ });
74
+
75
+ When('I click the next page button', async function (this: CustomWorld) {
76
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
77
+
78
+ // Find and click the next page button
79
+ const nextButton = this.page.locator(
80
+ 'button:has-text("Next"), button[aria-label*="next" i], .pagination button:last-child',
81
+ );
82
+
83
+ await nextButton.waitFor({ state: 'visible', timeout: 120_000 });
84
+ await nextButton.click();
85
+ });
86
+
87
+ When('I wait for the next page to load', async function (this: CustomWorld) {
88
+ // Wait for network to be idle after page change
89
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
90
+ // Add a small delay to ensure UI updates
91
+ await this.page.waitForTimeout(500);
92
+ });
93
+
94
+ When('I click on the first assistant card', async function (this: CustomWorld) {
95
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
96
+
97
+ const firstCard = this.page.locator('[data-testid="assistant-item"]').first();
98
+ await firstCard.waitFor({ state: 'visible', timeout: 120_000 });
99
+
100
+ // Store the current URL before clicking
101
+ this.testContext.previousUrl = this.page.url();
102
+
103
+ await firstCard.click();
104
+
105
+ // Wait for URL to change
106
+ await this.page.waitForFunction(
107
+ (previousUrl) => window.location.href !== previousUrl,
108
+ this.testContext.previousUrl,
109
+ { timeout: 120_000 },
110
+ );
111
+ });
112
+
113
+ When('I click on the first model card', async function (this: CustomWorld) {
114
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
115
+
116
+ const firstCard = this.page.locator('[data-testid="model-item"]').first();
117
+ await firstCard.waitFor({ state: 'visible', timeout: 120_000 });
118
+
119
+ // Store the current URL before clicking
120
+ this.testContext.previousUrl = this.page.url();
121
+
122
+ await firstCard.click();
123
+
124
+ // Wait for URL to change
125
+ await this.page.waitForFunction(
126
+ (previousUrl) => window.location.href !== previousUrl,
127
+ this.testContext.previousUrl,
128
+ { timeout: 120_000 },
129
+ );
130
+ });
131
+
132
+ When('I click on the first provider card', async function (this: CustomWorld) {
133
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
134
+
135
+ const firstCard = this.page.locator('[data-testid="provider-item"]').first();
136
+ await firstCard.waitFor({ state: 'visible', timeout: 120_000 });
137
+
138
+ // Store the current URL before clicking
139
+ this.testContext.previousUrl = this.page.url();
140
+
141
+ await firstCard.click();
142
+
143
+ // Wait for URL to change
144
+ await this.page.waitForFunction(
145
+ (previousUrl) => window.location.href !== previousUrl,
146
+ this.testContext.previousUrl,
147
+ { timeout: 120_000 },
148
+ );
149
+ });
150
+
151
+ When('I click on the first MCP card', async function (this: CustomWorld) {
152
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
153
+
154
+ const firstCard = this.page.locator('[data-testid="mcp-item"]').first();
155
+ await firstCard.waitFor({ state: 'visible', timeout: 120_000 });
156
+
157
+ // Store the current URL before clicking
158
+ this.testContext.previousUrl = this.page.url();
159
+
160
+ await firstCard.click();
161
+
162
+ // Wait for URL to change
163
+ await this.page.waitForFunction(
164
+ (previousUrl) => window.location.href !== previousUrl,
165
+ this.testContext.previousUrl,
166
+ { timeout: 120_000 },
167
+ );
168
+ });
169
+
170
+ When('I click on the sort dropdown', async function (this: CustomWorld) {
171
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
172
+
173
+ const sortDropdown = this.page
174
+ .locator(
175
+ '[data-testid="sort-dropdown"], select, button[aria-label*="sort" i], [role="combobox"]',
176
+ )
177
+ .first();
178
+
179
+ await sortDropdown.waitFor({ state: 'visible', timeout: 120_000 });
180
+ await sortDropdown.click();
181
+ });
182
+
183
+ When('I select a sort option', async function (this: CustomWorld) {
184
+ await this.page.waitForTimeout(500);
185
+
186
+ // Find and click a sort option (assuming dropdown opens a menu)
187
+ const sortOptions = this.page.locator('[role="option"], [role="menuitem"]');
188
+
189
+ // Wait for options to appear
190
+ await sortOptions.first().waitFor({ state: 'visible', timeout: 120_000 });
191
+
192
+ // Click the second option (skip the default/first one)
193
+ const secondOption = sortOptions.nth(1);
194
+ await secondOption.click();
195
+
196
+ // Store the option for later verification
197
+ const optionText = await secondOption.textContent();
198
+ this.testContext.selectedSortOption = optionText?.trim();
199
+ });
200
+
201
+ When('I wait for the sorted results to load', async function (this: CustomWorld) {
202
+ // Wait for network to be idle after sorting
203
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
204
+ // Add a small delay to ensure UI updates
205
+ await this.page.waitForTimeout(500);
206
+ });
207
+
208
+ When(
209
+ 'I click on the {string} link in the featured assistants section',
210
+ async function (this: CustomWorld, linkText: string) {
211
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
212
+
213
+ // Find the featured assistants section and the "more" link
214
+ const moreLink = this.page
215
+ .locator(`a:has-text("${linkText}"), button:has-text("${linkText}")`)
216
+ .first();
217
+
218
+ await moreLink.waitFor({ state: 'visible', timeout: 120_000 });
219
+ await moreLink.click();
220
+ },
221
+ );
222
+
223
+ When(
224
+ 'I click on the {string} link in the featured MCP tools section',
225
+ async function (this: CustomWorld, linkText: string) {
226
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
227
+
228
+ // Find the MCP section and the "more" link
229
+ // Since there might be multiple "more" links, we'll click the second one (MCP is after assistants)
230
+ const moreLinks = this.page.locator(
231
+ `a:has-text("${linkText}"), button:has-text("${linkText}")`,
232
+ );
233
+
234
+ // Wait for links to be visible
235
+ await moreLinks.first().waitFor({ state: 'visible', timeout: 120_000 });
236
+
237
+ // Click the second "more" link (for MCP section)
238
+ await moreLinks.nth(1).click();
239
+ },
240
+ );
241
+
242
+ When('I click on the first featured assistant card', async function (this: CustomWorld) {
243
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
244
+
245
+ const firstCard = this.page.locator('[data-testid="assistant-item"]').first();
246
+ await firstCard.waitFor({ state: 'visible', timeout: 120_000 });
247
+
248
+ // Store the current URL before clicking
249
+ this.testContext.previousUrl = this.page.url();
250
+
251
+ await firstCard.click();
252
+
253
+ // Wait for URL to change
254
+ await this.page.waitForFunction(
255
+ (previousUrl) => window.location.href !== previousUrl,
256
+ this.testContext.previousUrl,
257
+ { timeout: 120_000 },
258
+ );
259
+ });
260
+
261
+ // ============================================
262
+ // Then Steps (Assertions)
263
+ // ============================================
264
+
265
+ Then('I should see filtered assistant cards', async function (this: CustomWorld) {
266
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
267
+
268
+ const assistantItems = this.page.locator('[data-testid="assistant-item"]');
269
+
270
+ // Wait for at least one item to be visible
271
+ await expect(assistantItems.first()).toBeVisible({ timeout: 120_000 });
272
+
273
+ // Verify that at least one item exists
274
+ const count = await assistantItems.count();
275
+ expect(count).toBeGreaterThan(0);
276
+ });
277
+
278
+ Then(
279
+ 'I should see assistant cards filtered by the selected category',
280
+ async function (this: CustomWorld) {
281
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
282
+
283
+ const assistantItems = this.page.locator('[data-testid="assistant-item"]');
284
+
285
+ // Wait for at least one item to be visible
286
+ await expect(assistantItems.first()).toBeVisible({ timeout: 120_000 });
287
+
288
+ // Verify that at least one item exists
289
+ const count = await assistantItems.count();
290
+ expect(count).toBeGreaterThan(0);
291
+ },
292
+ );
293
+
294
+ Then('the URL should contain the category parameter', async function (this: CustomWorld) {
295
+ const currentUrl = this.page.url();
296
+ // Check if URL contains a category-related parameter
297
+ expect(
298
+ currentUrl.includes('category=') || currentUrl.includes('tag='),
299
+ `Expected URL to contain category parameter, but got: ${currentUrl}`,
300
+ ).toBeTruthy();
301
+ });
302
+
303
+ Then('I should see different assistant cards', async function (this: CustomWorld) {
304
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
305
+
306
+ const assistantItems = this.page.locator('[data-testid="assistant-item"]');
307
+
308
+ // Wait for at least one item to be visible
309
+ await expect(assistantItems.first()).toBeVisible({ timeout: 120_000 });
310
+
311
+ // Verify that at least one item exists
312
+ const count = await assistantItems.count();
313
+ expect(count).toBeGreaterThan(0);
314
+ });
315
+
316
+ Then('the URL should contain the page parameter', async function (this: CustomWorld) {
317
+ const currentUrl = this.page.url();
318
+ // Check if URL contains a page parameter
319
+ expect(
320
+ currentUrl.includes('page=') || currentUrl.includes('p='),
321
+ `Expected URL to contain page parameter, but got: ${currentUrl}`,
322
+ ).toBeTruthy();
323
+ });
324
+
325
+ Then('I should be navigated to the assistant detail page', async function (this: CustomWorld) {
326
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
327
+
328
+ const currentUrl = this.page.url();
329
+ // Verify that URL changed and contains /assistant/ followed by an identifier
330
+ const hasAssistantDetail = /\/discover\/assistant\/[^#?]+/.test(currentUrl);
331
+ const urlChanged = currentUrl !== this.testContext.previousUrl;
332
+
333
+ expect(
334
+ hasAssistantDetail && urlChanged,
335
+ `Expected to navigate to assistant detail page, but URL is: ${currentUrl} (previous: ${this.testContext.previousUrl})`,
336
+ ).toBeTruthy();
337
+ });
338
+
339
+ Then('I should see the assistant detail content', async function (this: CustomWorld) {
340
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
341
+
342
+ // Look for detail page elements (e.g., title, description, etc.)
343
+ const detailContent = this.page.locator('[data-testid="detail-content"], main, article').first();
344
+ await expect(detailContent).toBeVisible({ timeout: 120_000 });
345
+ });
346
+
347
+ Then('I should see model cards in the sorted order', async function (this: CustomWorld) {
348
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
349
+
350
+ const modelItems = this.page.locator('[data-testid="model-item"]');
351
+
352
+ // Wait for at least one item to be visible
353
+ await expect(modelItems.first()).toBeVisible({ timeout: 120_000 });
354
+
355
+ // Verify that at least one item exists
356
+ const count = await modelItems.count();
357
+ expect(count).toBeGreaterThan(0);
358
+ });
359
+
360
+ Then('I should be navigated to the model detail page', async function (this: CustomWorld) {
361
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
362
+
363
+ const currentUrl = this.page.url();
364
+ // Verify that URL changed and contains /model/ followed by an identifier
365
+ const hasModelDetail = /\/discover\/model\/[^#?]+/.test(currentUrl);
366
+ const urlChanged = currentUrl !== this.testContext.previousUrl;
367
+
368
+ expect(
369
+ hasModelDetail && urlChanged,
370
+ `Expected to navigate to model detail page, but URL is: ${currentUrl} (previous: ${this.testContext.previousUrl})`,
371
+ ).toBeTruthy();
372
+ });
373
+
374
+ Then('I should see the model detail content', async function (this: CustomWorld) {
375
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
376
+
377
+ // Look for detail page elements
378
+ const detailContent = this.page.locator('[data-testid="detail-content"], main, article').first();
379
+ await expect(detailContent).toBeVisible({ timeout: 120_000 });
380
+ });
381
+
382
+ Then('I should be navigated to the provider detail page', async function (this: CustomWorld) {
383
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
384
+
385
+ const currentUrl = this.page.url();
386
+ // Verify that URL changed and contains /provider/ followed by an identifier
387
+ const hasProviderDetail = /\/discover\/provider\/[^#?]+/.test(currentUrl);
388
+ const urlChanged = currentUrl !== this.testContext.previousUrl;
389
+
390
+ expect(
391
+ hasProviderDetail && urlChanged,
392
+ `Expected to navigate to provider detail page, but URL is: ${currentUrl} (previous: ${this.testContext.previousUrl})`,
393
+ ).toBeTruthy();
394
+ });
395
+
396
+ Then('I should see the provider detail content', async function (this: CustomWorld) {
397
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
398
+
399
+ // Look for detail page elements
400
+ const detailContent = this.page.locator('[data-testid="detail-content"], main, article').first();
401
+ await expect(detailContent).toBeVisible({ timeout: 120_000 });
402
+ });
403
+
404
+ Then(
405
+ 'I should see MCP cards filtered by the selected category',
406
+ async function (this: CustomWorld) {
407
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
408
+
409
+ const mcpItems = this.page.locator('[data-testid="mcp-item"]');
410
+
411
+ // Wait for at least one item to be visible
412
+ await expect(mcpItems.first()).toBeVisible({ timeout: 120_000 });
413
+
414
+ // Verify that at least one item exists
415
+ const count = await mcpItems.count();
416
+ expect(count).toBeGreaterThan(0);
417
+ },
418
+ );
419
+
420
+ Then('I should be navigated to the MCP detail page', async function (this: CustomWorld) {
421
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
422
+
423
+ const currentUrl = this.page.url();
424
+ // Verify that URL changed and contains /mcp/ followed by an identifier
425
+ const hasMcpDetail = /\/discover\/mcp\/[^#?]+/.test(currentUrl);
426
+ const urlChanged = currentUrl !== this.testContext.previousUrl;
427
+
428
+ expect(
429
+ hasMcpDetail && urlChanged,
430
+ `Expected to navigate to MCP detail page, but URL is: ${currentUrl} (previous: ${this.testContext.previousUrl})`,
431
+ ).toBeTruthy();
432
+ });
433
+
434
+ Then('I should see the MCP detail content', async function (this: CustomWorld) {
435
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
436
+
437
+ // Look for detail page elements
438
+ const detailContent = this.page.locator('[data-testid="detail-content"], main, article').first();
439
+ await expect(detailContent).toBeVisible({ timeout: 120_000 });
440
+ });
441
+
442
+ Then('I should be navigated to {string}', async function (this: CustomWorld, expectedPath: string) {
443
+ await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
444
+
445
+ const currentUrl = this.page.url();
446
+ // Verify that URL contains the expected path
447
+ expect(
448
+ currentUrl.includes(expectedPath),
449
+ `Expected URL to contain "${expectedPath}", but got: ${currentUrl}`,
450
+ ).toBeTruthy();
451
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.25",
3
+ "version": "2.0.0-next.26",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -0,0 +1 @@
1
+ ALTER TABLE "ai_models" ADD COLUMN IF NOT EXISTS "settings" jsonb DEFAULT '{}'::jsonb;