@lobehub/chat 1.141.6 โ 1.141.7
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/.github/PULL_REQUEST_TEMPLATE.md +26 -0
- package/.github/workflows/e2e.yml +6 -6
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/e2e/README.md +143 -0
- package/e2e/cucumber.config.js +20 -0
- package/e2e/package.json +24 -0
- package/e2e/src/features/discover/smoke.feature +11 -0
- package/e2e/src/features/routes/core-routes.feature +43 -0
- package/e2e/src/steps/common/navigation.steps.ts +36 -0
- package/e2e/src/steps/discover/smoke.steps.ts +34 -0
- package/e2e/src/steps/hooks.ts +69 -0
- package/e2e/src/steps/routes/routes.steps.ts +41 -0
- package/e2e/src/support/webServer.ts +96 -0
- package/e2e/src/support/world.ts +76 -0
- package/e2e/tsconfig.json +19 -0
- package/package.json +6 -3
- package/packages/const/src/layoutTokens.ts +1 -1
- package/packages/database/src/models/__tests__/session.test.ts +108 -0
- package/packages/database/src/models/session.ts +41 -1
- package/packages/model-bank/src/aiModels/groq.ts +0 -17
- package/packages/model-bank/src/aiModels/novita.ts +2 -60
- package/packages/model-bank/src/aiModels/siliconcloud.ts +116 -17
- package/pnpm-workspace.yaml +1 -0
- package/src/app/[variants]/(main)/discover/(list)/assistant/features/List/Item.tsx +1 -0
- package/src/app/[variants]/(main)/discover/DiscoverRouter.tsx +12 -10
- package/src/app/[variants]/(main)/discover/[[...path]]/page.tsx +7 -6
- package/src/app/[variants]/(main)/discover/features/Search.tsx +1 -0
- package/src/components/Loading/index.ts +1 -0
- package/src/features/AgentSetting/AgentModal/index.tsx +262 -35
- package/src/features/ChatInput/ActionBar/Params/Controls.tsx +261 -50
- package/src/features/ModelParamsControl/FrequencyPenalty.tsx +8 -3
- package/src/features/ModelParamsControl/PresencePenalty.tsx +8 -3
- package/src/features/ModelParamsControl/Temperature.tsx +8 -5
- package/src/features/ModelParamsControl/TopP.tsx +8 -3
- package/src/services/chat/index.ts +6 -0
- package/e2e/routes.spec.ts +0 -73
- package/playwright.config.ts +0 -35
|
@@ -12,10 +12,36 @@
|
|
|
12
12
|
- [ ] ๐ docs
|
|
13
13
|
- [ ] ๐จ chore
|
|
14
14
|
|
|
15
|
+
#### ๐ Related Issue
|
|
16
|
+
|
|
17
|
+
<!-- Link to the issue that is fixed by this PR -->
|
|
18
|
+
|
|
19
|
+
<!-- Example: Fixes #123, Closes #456, Related to #789 -->
|
|
20
|
+
|
|
15
21
|
#### ๐ Description of Change
|
|
16
22
|
|
|
17
23
|
<!-- Thank you for your Pull Request. Please provide a description above. -->
|
|
18
24
|
|
|
25
|
+
#### ๐งช How to Test
|
|
26
|
+
|
|
27
|
+
<!-- Please describe how you tested your changes -->
|
|
28
|
+
|
|
29
|
+
<!-- For AI features, please include test prompts or scenarios -->
|
|
30
|
+
|
|
31
|
+
- [ ] Tested locally
|
|
32
|
+
- [ ] Added/updated tests
|
|
33
|
+
- [ ] No tests needed
|
|
34
|
+
|
|
35
|
+
#### ๐ธ Screenshots / Videos
|
|
36
|
+
|
|
37
|
+
<!-- If this PR includes UI changes, please provide screenshots or videos -->
|
|
38
|
+
|
|
39
|
+
| Before | After |
|
|
40
|
+
| ------ | ----- |
|
|
41
|
+
| ... | ... |
|
|
42
|
+
|
|
19
43
|
#### ๐ Additional Information
|
|
20
44
|
|
|
21
45
|
<!-- Add any other context about the Pull Request here. -->
|
|
46
|
+
|
|
47
|
+
<!-- Breaking changes? Migration guide? Performance impact? -->
|
|
@@ -35,18 +35,18 @@ jobs:
|
|
|
35
35
|
PORT: 3010
|
|
36
36
|
run: bun run e2e
|
|
37
37
|
|
|
38
|
-
- name: Upload
|
|
38
|
+
- name: Upload Cucumber HTML report (on failure)
|
|
39
39
|
if: failure()
|
|
40
40
|
uses: actions/upload-artifact@v4
|
|
41
41
|
with:
|
|
42
|
-
name:
|
|
43
|
-
path:
|
|
42
|
+
name: cucumber-report
|
|
43
|
+
path: e2e/reports
|
|
44
44
|
if-no-files-found: ignore
|
|
45
45
|
|
|
46
|
-
- name: Upload
|
|
46
|
+
- name: Upload screenshots (on failure)
|
|
47
47
|
if: failure()
|
|
48
48
|
uses: actions/upload-artifact@v4
|
|
49
49
|
with:
|
|
50
|
-
name: test-
|
|
51
|
-
path:
|
|
50
|
+
name: test-screenshots
|
|
51
|
+
path: e2e/screenshots
|
|
52
52
|
if-no-files-found: ignore
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 1.141.7](https://github.com/lobehub/lobe-chat/compare/v1.141.6...v1.141.7)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2025-10-23**</sup>
|
|
8
|
+
|
|
9
|
+
#### ๐ Styles
|
|
10
|
+
|
|
11
|
+
- **misc**: Allow removal of `top_p` and similar request parameters.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### Styles
|
|
19
|
+
|
|
20
|
+
- **misc**: Allow removal of `top_p` and similar request parameters, closes [#9498](https://github.com/lobehub/lobe-chat/issues/9498) ([4c313ce](https://github.com/lobehub/lobe-chat/commit/4c313ce))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 1.141.6](https://github.com/lobehub/lobe-chat/compare/v1.141.5...v1.141.6)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2025-10-22**</sup>
|
package/changelog/v1.json
CHANGED
package/e2e/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# E2E Tests for LobeChat
|
|
2
|
+
|
|
3
|
+
This directory contains end-to-end (E2E) tests for LobeChat using Cucumber (BDD) and Playwright.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
````
|
|
8
|
+
e2e/
|
|
9
|
+
โโโ src/ # Source files
|
|
10
|
+
โ โโโ features/ # Gherkin feature files
|
|
11
|
+
โ โ โโโ discover/ # Discover page tests
|
|
12
|
+
โ โโโ steps/ # Step definitions
|
|
13
|
+
โ โ โโโ common/ # Reusable step definitions
|
|
14
|
+
โ โ โโโ discover/ # Discover-specific steps
|
|
15
|
+
โ โโโ support/ # Test support files
|
|
16
|
+
โ โโโ world.ts # Custom World context
|
|
17
|
+
โโโ reports/ # Test reports (generated)
|
|
18
|
+
โโโ cucumber.config.js # Cucumber configuration
|
|
19
|
+
โโโ tsconfig.json # TypeScript configuration
|
|
20
|
+
โโโ package.json # Dependencies and scripts
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- Node.js 20, 22, or >=24
|
|
25
|
+
- Dev server running on `http://localhost:3010` (or set `BASE_URL` env var)
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Install dependencies:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd e2e
|
|
33
|
+
pnpm install
|
|
34
|
+
````
|
|
35
|
+
|
|
36
|
+
Install Playwright browsers:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx playwright install chromium
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Running Tests
|
|
43
|
+
|
|
44
|
+
Run all tests:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm test
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Run tests in headed mode (see browser):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run test:headed
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run only smoke tests:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run test:smoke
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run discover tests:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run test:discover
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Environment Variables
|
|
69
|
+
|
|
70
|
+
- `BASE_URL`: Base URL for the application (default: `http://localhost:3010`)
|
|
71
|
+
- `PORT`: Port number (default: `3010`)
|
|
72
|
+
- `HEADLESS`: Run browser in headless mode (default: `true`, set to `false` to see browser)
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
HEADLESS=false BASE_URL=http://localhost:3000 npm run test:smoke
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Writing Tests
|
|
81
|
+
|
|
82
|
+
### Feature Files
|
|
83
|
+
|
|
84
|
+
Feature files are written in Gherkin syntax and placed in the `src/features/` directory:
|
|
85
|
+
|
|
86
|
+
```gherkin
|
|
87
|
+
@discover @smoke
|
|
88
|
+
Feature: Discover Smoke Tests
|
|
89
|
+
Critical path tests to ensure the discover module is functional
|
|
90
|
+
|
|
91
|
+
@DISCOVER-SMOKE-001 @P0
|
|
92
|
+
Scenario: Load discover assistant list page
|
|
93
|
+
Given I navigate to "/discover/assistant"
|
|
94
|
+
Then the page should load without errors
|
|
95
|
+
And I should see the page body
|
|
96
|
+
And I should see the search bar
|
|
97
|
+
And I should see assistant cards
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Step Definitions
|
|
101
|
+
|
|
102
|
+
Step definitions are TypeScript files in the `src/steps/` directory that implement the steps from feature files:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { Given, Then } from '@cucumber/cucumber';
|
|
106
|
+
import { expect } from '@playwright/test';
|
|
107
|
+
|
|
108
|
+
import { CustomWorld } from '../../support/world';
|
|
109
|
+
|
|
110
|
+
Given('I navigate to {string}', async function (this: CustomWorld, path: string) {
|
|
111
|
+
await this.page.goto(path);
|
|
112
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Test Reports
|
|
117
|
+
|
|
118
|
+
After running tests, HTML and JSON reports are generated in the `reports/` directory:
|
|
119
|
+
|
|
120
|
+
- `reports/cucumber-report.html` - Human-readable HTML report
|
|
121
|
+
- `reports/cucumber-report.json` - Machine-readable JSON report
|
|
122
|
+
|
|
123
|
+
## Troubleshooting
|
|
124
|
+
|
|
125
|
+
### Browser not found
|
|
126
|
+
|
|
127
|
+
If you see errors about missing browser executables:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx playwright install chromium
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Port already in use
|
|
134
|
+
|
|
135
|
+
Make sure the dev server is running on the expected port (3010 by default), or set `PORT` or `BASE_URL` environment variable.
|
|
136
|
+
|
|
137
|
+
### Test timeout
|
|
138
|
+
|
|
139
|
+
Increase timeout in `cucumber.config.js` or `src/steps/hooks.ts`:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
setDefaultTimeout(120000); // 2 minutes
|
|
143
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @type {import('@cucumber/cucumber').IConfiguration}
|
|
3
|
+
*/
|
|
4
|
+
export default {
|
|
5
|
+
format: [
|
|
6
|
+
'progress-bar',
|
|
7
|
+
'html:reports/cucumber-report.html',
|
|
8
|
+
'json:reports/cucumber-report.json',
|
|
9
|
+
],
|
|
10
|
+
formatOptions: {
|
|
11
|
+
snippetInterface: 'async-await',
|
|
12
|
+
},
|
|
13
|
+
parallel: process.env.CI ? 1 : 4,
|
|
14
|
+
paths: ['src/features/**/*.feature'],
|
|
15
|
+
publishQuiet: true,
|
|
16
|
+
require: ['src/steps/**/*.ts', 'src/support/**/*.ts'],
|
|
17
|
+
requireModule: ['tsx/cjs'],
|
|
18
|
+
retry: 0,
|
|
19
|
+
timeout: 120_000,
|
|
20
|
+
};
|
package/e2e/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lobechat/e2e-tests",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "E2E tests for LobeChat using Cucumber and Playwright",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "cucumber-js --config cucumber.config.js",
|
|
8
|
+
"test:discover": "cucumber-js --config cucumber.config.js src/features/discover/",
|
|
9
|
+
"test:headed": "HEADLESS=false cucumber-js --config cucumber.config.js",
|
|
10
|
+
"test:routes": "cucumber-js --config cucumber.config.js --tags '@routes'",
|
|
11
|
+
"test:routes:ci": "cucumber-js --config cucumber.config.js --tags '@routes and not @ci-skip'",
|
|
12
|
+
"test:smoke": "cucumber-js --config cucumber.config.js --tags '@smoke'"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@cucumber/cucumber": "^12.2.0",
|
|
16
|
+
"@playwright/test": "^1.56.1",
|
|
17
|
+
"playwright": "^1.56.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.10.5",
|
|
21
|
+
"tsx": "^4.20.6",
|
|
22
|
+
"typescript": "^5.7.3"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
@discover @smoke
|
|
2
|
+
Feature: Discover Smoke Tests
|
|
3
|
+
Critical path tests to ensure the discover module is functional
|
|
4
|
+
|
|
5
|
+
@DISCOVER-SMOKE-001 @P0
|
|
6
|
+
Scenario: Load discover assistant list page
|
|
7
|
+
Given I navigate to "/discover/assistant"
|
|
8
|
+
Then the page should load without errors
|
|
9
|
+
And I should see the page body
|
|
10
|
+
And I should see the search bar
|
|
11
|
+
And I should see assistant cards
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
@routes @smoke
|
|
2
|
+
Feature: Core Routes Accessibility
|
|
3
|
+
As a user
|
|
4
|
+
I want all core application routes to be accessible
|
|
5
|
+
So that I can navigate the application without errors
|
|
6
|
+
|
|
7
|
+
Background:
|
|
8
|
+
Given the application is running
|
|
9
|
+
|
|
10
|
+
@ROUTES-001 @P0
|
|
11
|
+
Scenario Outline: Access core routes without errors
|
|
12
|
+
When I navigate to "<route>"
|
|
13
|
+
Then the response status should be less than 400
|
|
14
|
+
And the page should load without errors
|
|
15
|
+
And I should see the page body
|
|
16
|
+
And the page title should not contain "error" or "not found"
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
| route |
|
|
20
|
+
| / |
|
|
21
|
+
| /chat |
|
|
22
|
+
| /discover |
|
|
23
|
+
| /files |
|
|
24
|
+
| /repos |
|
|
25
|
+
|
|
26
|
+
@ROUTES-002 @P0
|
|
27
|
+
Scenario Outline: Access settings routes without errors
|
|
28
|
+
When I navigate to "/settings?active=<tab>"
|
|
29
|
+
Then the response status should be less than 400
|
|
30
|
+
And the page should load without errors
|
|
31
|
+
And I should see the page body
|
|
32
|
+
And the page title should not contain "error" or "not found"
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
| tab |
|
|
36
|
+
| about |
|
|
37
|
+
| agent |
|
|
38
|
+
| hotkey |
|
|
39
|
+
| provider |
|
|
40
|
+
| proxy |
|
|
41
|
+
| storage |
|
|
42
|
+
| system-agent |
|
|
43
|
+
| tts |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Given, Then } from '@cucumber/cucumber';
|
|
2
|
+
import { expect } from '@playwright/test';
|
|
3
|
+
|
|
4
|
+
import { CustomWorld } from '../../support/world';
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// Given Steps (Preconditions)
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
Given('I navigate to {string}', async function (this: CustomWorld, path: string) {
|
|
11
|
+
const response = await this.page.goto(path, { waitUntil: 'commit' });
|
|
12
|
+
this.testContext.lastResponse = response;
|
|
13
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// Then Steps (Assertions)
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
Then('the page should load without errors', async function (this: CustomWorld) {
|
|
21
|
+
// Check for no JavaScript errors
|
|
22
|
+
expect(this.testContext.jsErrors).toHaveLength(0);
|
|
23
|
+
|
|
24
|
+
// Check page didn't navigate to error page
|
|
25
|
+
const url = this.page.url();
|
|
26
|
+
expect(url).not.toMatch(/\/404|\/error|not-found/i);
|
|
27
|
+
|
|
28
|
+
// Check no error title
|
|
29
|
+
const title = await this.page.title();
|
|
30
|
+
expect(title).not.toMatch(/not found|error/i);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
Then('I should see the page body', async function (this: CustomWorld) {
|
|
34
|
+
const body = this.page.locator('body');
|
|
35
|
+
await expect(body).toBeVisible();
|
|
36
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Then } from '@cucumber/cucumber';
|
|
2
|
+
import { expect } from '@playwright/test';
|
|
3
|
+
|
|
4
|
+
import { CustomWorld } from '../../support/world';
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// Then Steps (Assertions)
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
Then('I should see the search bar', async function (this: CustomWorld) {
|
|
11
|
+
// Wait for network to be idle to ensure Suspense components are loaded
|
|
12
|
+
await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
|
|
13
|
+
|
|
14
|
+
// The SearchBar component from @lobehub/ui may not pass through data-testid
|
|
15
|
+
// Try to find the input element within the search component
|
|
16
|
+
const searchBar = this.page.locator('input[type="text"]').first();
|
|
17
|
+
await expect(searchBar).toBeVisible({ timeout: 120_000 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
Then('I should see assistant cards', async function (this: CustomWorld) {
|
|
21
|
+
// Wait for content to load
|
|
22
|
+
await this.page.waitForLoadState('networkidle', { timeout: 120_000 });
|
|
23
|
+
|
|
24
|
+
// After migrating to SPA (react-router), links use relative paths like /assistant/:id
|
|
25
|
+
// Look for assistant items by data-testid instead of href
|
|
26
|
+
const assistantItems = this.page.locator('[data-testid="assistant-item"]');
|
|
27
|
+
|
|
28
|
+
// Wait for at least one item to be visible
|
|
29
|
+
await expect(assistantItems.first()).toBeVisible({ timeout: 120_000 });
|
|
30
|
+
|
|
31
|
+
// Check we have multiple items
|
|
32
|
+
const count = await assistantItems.count();
|
|
33
|
+
expect(count).toBeGreaterThan(0);
|
|
34
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { After, AfterAll, Before, BeforeAll, Status, setDefaultTimeout } from '@cucumber/cucumber';
|
|
2
|
+
|
|
3
|
+
import { startWebServer, stopWebServer } from '../support/webServer';
|
|
4
|
+
import { CustomWorld } from '../support/world';
|
|
5
|
+
|
|
6
|
+
// Set default timeout for all steps to 120 seconds
|
|
7
|
+
setDefaultTimeout(120_000);
|
|
8
|
+
|
|
9
|
+
BeforeAll({ timeout: 120_000 }, async function () {
|
|
10
|
+
console.log('๐ Starting E2E test suite...');
|
|
11
|
+
|
|
12
|
+
const PORT = process.env.PORT ? Number(process.env.PORT) : 3010;
|
|
13
|
+
const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
|
|
14
|
+
|
|
15
|
+
console.log(`Base URL: ${BASE_URL}`);
|
|
16
|
+
|
|
17
|
+
// Start web server if not using external BASE_URL
|
|
18
|
+
if (!process.env.BASE_URL) {
|
|
19
|
+
await startWebServer({
|
|
20
|
+
command: 'npm run dev',
|
|
21
|
+
port: PORT,
|
|
22
|
+
reuseExistingServer: !process.env.CI,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
Before(async function (this: CustomWorld, { pickle }) {
|
|
28
|
+
await this.init();
|
|
29
|
+
|
|
30
|
+
const testId = pickle.tags.find((tag) => tag.name.startsWith('@DISCOVER-'));
|
|
31
|
+
console.log(`\n๐ Running: ${pickle.name}${testId ? ` (${testId.name.replace('@', '')})` : ''}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
After(async function (this: CustomWorld, { pickle, result }) {
|
|
35
|
+
const testId = pickle.tags
|
|
36
|
+
.find((tag) => tag.name.startsWith('@DISCOVER-'))
|
|
37
|
+
?.name.replace('@', '');
|
|
38
|
+
|
|
39
|
+
if (result?.status === Status.FAILED) {
|
|
40
|
+
const screenshot = await this.takeScreenshot(`${testId || 'failure'}-${Date.now()}`);
|
|
41
|
+
this.attach(screenshot, 'image/png');
|
|
42
|
+
|
|
43
|
+
const html = await this.page.content();
|
|
44
|
+
this.attach(html, 'text/html');
|
|
45
|
+
|
|
46
|
+
if (this.testContext.jsErrors.length > 0) {
|
|
47
|
+
const errors = this.testContext.jsErrors.map((e) => e.message).join('\n');
|
|
48
|
+
this.attach(`JavaScript Errors:\n${errors}`, 'text/plain');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(`โ Failed: ${pickle.name}`);
|
|
52
|
+
if (result.message) {
|
|
53
|
+
console.log(` Error: ${result.message}`);
|
|
54
|
+
}
|
|
55
|
+
} else if (result?.status === Status.PASSED) {
|
|
56
|
+
console.log(`โ
Passed: ${pickle.name}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await this.cleanup();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
AfterAll(async function () {
|
|
63
|
+
console.log('\n๐ Test suite completed');
|
|
64
|
+
|
|
65
|
+
// Stop web server if we started it
|
|
66
|
+
if (!process.env.BASE_URL && process.env.CI) {
|
|
67
|
+
await stopWebServer();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Given, Then } from '@cucumber/cucumber';
|
|
2
|
+
import { expect } from '@playwright/test';
|
|
3
|
+
|
|
4
|
+
import { CustomWorld } from '../../support/world';
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// Given Steps (Preconditions)
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
Given('the application is running', async function (this: CustomWorld) {
|
|
11
|
+
// This is a placeholder step to indicate that the app should be running
|
|
12
|
+
// The actual server startup is handled outside the test (in CI or locally)
|
|
13
|
+
// We just verify we can reach the base URL
|
|
14
|
+
const response = await this.page.goto('/');
|
|
15
|
+
expect(response).toBeTruthy();
|
|
16
|
+
// Store the response for later assertions
|
|
17
|
+
this.testContext.lastResponse = response;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// ============================================
|
|
21
|
+
// Then Steps (Assertions)
|
|
22
|
+
// ============================================
|
|
23
|
+
|
|
24
|
+
Then(
|
|
25
|
+
'the response status should be less than {int}',
|
|
26
|
+
async function (this: CustomWorld, maxStatus: number) {
|
|
27
|
+
const status = this.testContext.lastResponse?.status() ?? 0;
|
|
28
|
+
expect(status, `Expected status < ${maxStatus}, but got ${status}`).toBeLessThan(maxStatus);
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
Then(
|
|
33
|
+
'the page title should not contain {string} or {string}',
|
|
34
|
+
async function (this: CustomWorld, text1: string, text2: string) {
|
|
35
|
+
const title = await this.page.title();
|
|
36
|
+
const regex = new RegExp(`${text1}|${text2}`, 'i');
|
|
37
|
+
expect(title, `Page title "${title}" should not contain "${text1}" or "${text2}"`).not.toMatch(
|
|
38
|
+
regex,
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type ChildProcess, exec } from 'node:child_process';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
let serverProcess: ChildProcess | null = null;
|
|
5
|
+
let serverStartPromise: Promise<void> | null = null;
|
|
6
|
+
|
|
7
|
+
interface WebServerOptions {
|
|
8
|
+
command: string;
|
|
9
|
+
env?: Record<string, string>;
|
|
10
|
+
port: number;
|
|
11
|
+
reuseExistingServer?: boolean;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function isServerRunning(port: number): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(`http://localhost:${port}/chat`, {
|
|
18
|
+
method: 'HEAD',
|
|
19
|
+
});
|
|
20
|
+
return response.ok;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function startWebServer(options: WebServerOptions): Promise<void> {
|
|
27
|
+
const { command, port, timeout = 120_000, env = {}, reuseExistingServer = true } = options;
|
|
28
|
+
|
|
29
|
+
// If server is already being started by another worker, wait for it
|
|
30
|
+
if (serverStartPromise) {
|
|
31
|
+
console.log(`โณ Waiting for server to start (started by another worker)...`);
|
|
32
|
+
return serverStartPromise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if server is already running
|
|
36
|
+
if (reuseExistingServer && (await isServerRunning(port))) {
|
|
37
|
+
console.log(`โ
Reusing existing server on port ${port}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create a promise for the server startup and store it
|
|
42
|
+
serverStartPromise = (async () => {
|
|
43
|
+
console.log(`๐ Starting web server: ${command}`);
|
|
44
|
+
|
|
45
|
+
// Get the project root directory (parent of e2e folder)
|
|
46
|
+
const projectRoot = resolve(__dirname, '../../..');
|
|
47
|
+
|
|
48
|
+
// Start the server process
|
|
49
|
+
serverProcess = exec(command, {
|
|
50
|
+
cwd: projectRoot,
|
|
51
|
+
env: {
|
|
52
|
+
...process.env,
|
|
53
|
+
ENABLE_AUTH_PROTECTION: '0',
|
|
54
|
+
ENABLE_OIDC: '0',
|
|
55
|
+
NEXT_PUBLIC_ENABLE_CLERK_AUTH: '0',
|
|
56
|
+
NEXT_PUBLIC_ENABLE_NEXT_AUTH: '0',
|
|
57
|
+
NODE_OPTIONS: '--max-old-space-size=6144',
|
|
58
|
+
PORT: String(port),
|
|
59
|
+
...env,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Forward server output to console for debugging
|
|
64
|
+
serverProcess.stdout?.on('data', (data) => {
|
|
65
|
+
console.log(`[server] ${data}`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
serverProcess.stderr?.on('data', (data) => {
|
|
69
|
+
console.error(`[server] ${data}`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Wait for server to be ready
|
|
73
|
+
const startTime = Date.now();
|
|
74
|
+
while (!(await isServerRunning(port))) {
|
|
75
|
+
if (Date.now() - startTime > timeout) {
|
|
76
|
+
throw new Error(`Server failed to start within ${timeout}ms`);
|
|
77
|
+
}
|
|
78
|
+
await new Promise((resolve) => {
|
|
79
|
+
setTimeout(resolve, 1000);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`โ
Web server is ready on port ${port}`);
|
|
84
|
+
})();
|
|
85
|
+
|
|
86
|
+
return serverStartPromise;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function stopWebServer(): Promise<void> {
|
|
90
|
+
if (serverProcess) {
|
|
91
|
+
console.log('๐ Stopping web server...');
|
|
92
|
+
serverProcess.kill();
|
|
93
|
+
serverProcess = null;
|
|
94
|
+
serverStartPromise = null;
|
|
95
|
+
}
|
|
96
|
+
}
|