@lobehub/lobehub 2.0.0-next.24 → 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.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +1 -0
- package/e2e/src/features/discover/detail-pages.feature +95 -0
- package/e2e/src/features/discover/interactions.feature +113 -0
- package/e2e/src/steps/discover/detail-pages.steps.ts +295 -0
- package/e2e/src/steps/discover/interactions.steps.ts +451 -0
- package/package.json +1 -1
- package/packages/database/migrations/0043_add_ai_model_settings.sql +1 -0
- package/packages/database/migrations/meta/0043_snapshot.json +8419 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/core/migrations.json +10 -2
- package/packages/database/src/repositories/aiInfra/index.test.ts +198 -0
- package/packages/database/src/repositories/aiInfra/index.ts +2 -1
- package/packages/database/src/schemas/aiInfra.ts +2 -0
- package/src/app/[variants]/(main)/labs/page.tsx +9 -8
- package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +152 -0
- package/src/features/Conversation/Messages/Group/Actions/WithoutContentId.tsx +70 -0
- package/src/features/Conversation/Messages/Group/Actions/index.tsx +21 -0
- package/src/features/Conversation/Messages/Group/ContentBlock.tsx +91 -0
- package/src/features/Conversation/Messages/Group/EditState.tsx +51 -0
- package/src/features/Conversation/Messages/Group/Error/index.tsx +53 -0
- package/src/features/Conversation/Messages/Group/GroupChildren.tsx +73 -0
- package/src/features/Conversation/Messages/Group/MessageContent.tsx +39 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/BuiltinPluginTitle.tsx +49 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/Debug.tsx +70 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/PluginResult.tsx +34 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/PluginState.tsx +18 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/Settings.tsx +40 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/ToolTitle.tsx +92 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +176 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/Arguments/ObjectEntity.tsx +81 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/Arguments/ValueCell.tsx +43 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/Arguments/index.tsx +134 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/CustomRender.tsx +88 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/ErrorResponse.tsx +35 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/LoadingPlaceholder/index.tsx +29 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +66 -0
- package/src/features/Conversation/Messages/Group/Tool/Render/index.tsx +105 -0
- package/src/features/Conversation/Messages/Group/Tool/index.tsx +75 -0
- package/src/features/Conversation/Messages/Group/Tools.tsx +46 -0
- package/src/features/Conversation/Messages/Group/index.tsx +140 -0
- package/src/features/Conversation/Messages/index.tsx +12 -0
- package/src/features/Conversation/components/ShareMessageModal/ShareImage/Preview.tsx +2 -2
- package/src/services/chat/contextEngineering.ts +6 -5
- package/src/services/message/server.ts +10 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +309 -2
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +2 -22
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +272 -14
|
@@ -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.
|
|
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;
|