plum-e2e 1.0.10 → 1.1.0
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/.claude/settings.local.json +16 -1
- package/.vscode/settings.json +10 -0
- package/README.md +151 -37
- package/backend/_scaffold/features/LoginPage.feature +45 -3
- package/backend/_scaffold/pages/HomepagePage.ts +7 -0
- package/backend/_scaffold/pages/LoginPage.ts +37 -13
- package/backend/_scaffold/step_definitions/HomepageSteps.ts +6 -0
- package/backend/_scaffold/step_definitions/LoginSteps.ts +30 -4
- package/backend/_scaffold/utils/browser.ts +33 -0
- package/backend/_scaffold/utils/hooks.ts +8 -29
- package/backend/_scaffold/utils/utils.ts +3 -9
- package/backend/config/scripts/create-settings.js +7 -14
- package/backend/config/scripts/create-step.mjs +268 -0
- package/backend/config/scripts/generate-report.js +31 -75
- package/backend/config/scripts/run-tests.js +19 -4
- package/backend/package-lock.json +56 -641
- package/backend/package.json +4 -1
- package/backend/routes/reports.routes.js +6 -10
- package/backend/services/envService.js +4 -10
- package/backend/services/reportService.js +70 -20
- package/backend/services/testService.js +99 -24
- package/backend/tsconfig.json +2 -2
- package/backend/websockets/socketHandler.js +12 -6
- package/bin/plum.js +49 -3
- package/frontend/package-lock.json +436 -135
- package/frontend/package.json +1 -1
- package/frontend/src/app.css +241 -6
- package/frontend/src/app.html +14 -1
- package/frontend/src/lib/api/reports.js +68 -0
- package/frontend/src/lib/api/schedules.js +64 -0
- package/frontend/src/lib/api/tests.js +41 -0
- package/frontend/src/lib/components/layout/Nav.svelte +304 -0
- package/frontend/src/lib/components/layout/PageShell.svelte +28 -0
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +378 -0
- package/frontend/src/lib/components/ui/Badge.svelte +63 -0
- package/frontend/src/lib/components/ui/Button.svelte +117 -0
- package/frontend/src/lib/components/ui/Modal.svelte +140 -0
- package/frontend/src/lib/components/ui/Pagination.svelte +100 -0
- package/frontend/src/lib/components/ui/Terminal.svelte +100 -0
- package/frontend/src/lib/stores/runner.js +55 -0
- package/frontend/src/lib/stores/theme.js +47 -0
- package/frontend/src/routes/+layout.svelte +7 -12
- package/frontend/src/routes/+page.svelte +690 -142
- package/frontend/src/routes/reports/+page.svelte +395 -125
- package/frontend/src/routes/reports/[slug]/+page.svelte +749 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +267 -303
- package/frontend/svelte.config.js +1 -4
- package/frontend/tailwind.config.js +2 -23
- package/package.json +2 -2
- package/backend/_scaffold/utils/world.ts +0 -25
- package/frontend/src/routes/components/Navigation.svelte +0 -53
|
@@ -6,7 +6,22 @@
|
|
|
6
6
|
"Bash(git commit -m ':*)",
|
|
7
7
|
"Bash(git push:*)",
|
|
8
8
|
"Bash(gh pr:*)",
|
|
9
|
-
"Bash(node:*)"
|
|
9
|
+
"Bash(node:*)",
|
|
10
|
+
"Bash(curl *)",
|
|
11
|
+
"Bash(git pull *)",
|
|
12
|
+
"Bash(npm install *)",
|
|
13
|
+
"Bash(code --list-extensions)",
|
|
14
|
+
"Bash(git branch *)",
|
|
15
|
+
"Bash(git commit -m ' *)",
|
|
16
|
+
"mcp__ide__getDiagnostics",
|
|
17
|
+
"Bash(git rm *)",
|
|
18
|
+
"Bash(npm test *)",
|
|
19
|
+
"Bash(npm run *)",
|
|
20
|
+
"Bash(git *)",
|
|
21
|
+
"Bash(python3 -c \"import sys; print\\(sys.stdin.read\\(\\)[:200]\\)\")",
|
|
22
|
+
"Bash(python3 -c ' *)",
|
|
23
|
+
"Bash(grep -rl \"^<!--$\" /Users/silverlunah/Projects/plum/frontend/src --include=\"*.svelte\")",
|
|
24
|
+
"Bash(python3 *)"
|
|
10
25
|
]
|
|
11
26
|
}
|
|
12
27
|
}
|
package/README.md
CHANGED
|
@@ -7,60 +7,174 @@
|
|
|
7
7
|
|
|
8
8
|
## Welcome to Plum!
|
|
9
9
|
|
|
10
|
-
Plum
|
|
10
|
+
Plum is a ready-to-use E2E test automation environment that combines [Playwright](https://playwright.dev) and [Cucumber](https://cucumber.io). Write tests in [Gherkin](https://cucumber.io/docs/gherkin/) format, run them from the CLI or through a UI, and view reports — all in one place.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
You can view, run, and schedule tests in a simple UI. You can even view the history of your runs in the reports page!
|
|
15
|
-
|
|
16
|
-
**_Pre-requisite:_**
|
|
17
|
-
|
|
18
|
-
1. Install Docker and ensure the Docker daemon is running.
|
|
12
|
+
---
|
|
19
13
|
|
|
20
14
|
## For Users
|
|
21
15
|
|
|
22
|
-
People
|
|
16
|
+
> People who want to use Plum as a test environment for their website.
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
### Prerequisites
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
3. Go inside the folder you created `cd my-test-folder`
|
|
29
|
-
4. Run `plum init`
|
|
30
|
-
1. This will initialize Plum and will create your base files:
|
|
31
|
-
1. `\tests` folder: This include sample test cases for [SauceLabs](https://www.saucedemo.com/v1/)
|
|
32
|
-
2. `.env` file: Your starting .env file. You can set the BASE_URL to your own site after you're done with the scaffold tests.
|
|
33
|
-
5. There are two ways to start testing:
|
|
34
|
-
1. By running the server. Run:<br/> `plum start` to start the server
|
|
35
|
-
1. Access the UI at: http://localhost:5173/
|
|
36
|
-
2. Without running the server. Recommended while you're writing your tests. Run:<br/> `plum dev <@test-id>`. If no test ID is included, it will run all tests
|
|
20
|
+
- [Node.js](https://nodejs.org)
|
|
21
|
+
- [Docker](https://www.docker.com) (required for `plum start`)
|
|
37
22
|
|
|
38
|
-
|
|
23
|
+
### Setup
|
|
39
24
|
|
|
40
|
-
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g plum-e2e
|
|
27
|
+
mkdir my-tests && cd my-tests
|
|
28
|
+
plum init
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`plum init` creates the following in your project directory and installs the [Cucumber VS Code extension](https://marketplace.visualstudio.com/items?itemName=cucumberopen.cucumber-official) for step definition linking:
|
|
41
32
|
|
|
42
33
|
<pre>
|
|
43
34
|
╠═ tests
|
|
44
|
-
║ ╠═ features
|
|
45
|
-
║ ╠═ step_definitions
|
|
46
|
-
║ ╠═ pages
|
|
47
|
-
║ ╚═ utils
|
|
48
|
-
|
|
35
|
+
║ ╠═ features : Gherkin feature files — your test cases
|
|
36
|
+
║ ╠═ step_definitions : TypeScript step implementations
|
|
37
|
+
║ ╠═ pages : Page Object Models
|
|
38
|
+
║ ╚═ utils : Shared utilities (browser setup, constants, etc.)
|
|
39
|
+
╠═ .vscode
|
|
40
|
+
║ ╚═ settings.json : Cucumber extension config (step definition linking)
|
|
41
|
+
╚═ .env : Environment config (BASE_URL, IS_HEADLESS)
|
|
49
42
|
</pre>
|
|
50
43
|
|
|
51
|
-
|
|
44
|
+
### Commands
|
|
45
|
+
|
|
46
|
+
| Command | Description |
|
|
47
|
+
| ----------------------------- | -------------------------------------------------------------------------- |
|
|
48
|
+
| `plum init` | Scaffold the `tests/` folder and `.env` file in your project directory |
|
|
49
|
+
| `plum dev [tag]` | Run tests locally without Docker. Pass a tag to filter (e.g. `@test-1`) |
|
|
50
|
+
| `plum dev --parallel N [tag]` | Run tests in parallel with N workers (e.g. `plum dev --parallel 4`) |
|
|
51
|
+
| `plum start` | Start the full stack via Docker and open the UI at `http://localhost:5173` |
|
|
52
|
+
| `plum create-step` | Interactive prompt to generate a step definition and page object |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
### Writing a Test
|
|
57
|
+
|
|
58
|
+
Tests follow a three-layer structure: **Feature → Step Definition → Page Object**.
|
|
59
|
+
|
|
60
|
+
#### 1. Feature File
|
|
61
|
+
|
|
62
|
+
Create a `.feature` file in `tests/features/`. Tags are used to filter which tests to run.
|
|
63
|
+
|
|
64
|
+
```gherkin
|
|
65
|
+
# tests/features/Login.feature
|
|
66
|
+
|
|
67
|
+
@suite-login
|
|
68
|
+
Feature: Login
|
|
69
|
+
|
|
70
|
+
@test-login-1
|
|
71
|
+
Scenario: User can log in with valid credentials
|
|
72
|
+
Given I am on the login page
|
|
73
|
+
When I log in with valid credentials
|
|
74
|
+
Then I should see the dashboard
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### 2. Page Object Model
|
|
78
|
+
|
|
79
|
+
Create a `.ts` file in `tests/pages/`. Methods are **static** — they use `page()` internally so you never need to pass a page instance around.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// tests/pages/LoginPage.ts
|
|
83
|
+
|
|
84
|
+
import { page } from '../utils/browser';
|
|
85
|
+
|
|
86
|
+
export class LoginPage {
|
|
87
|
+
static async goToLoginPage() {
|
|
88
|
+
await page().goto(process.env.BASE_URL as string);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static async login(email: string, password: string) {
|
|
92
|
+
await page().fill('#email', email);
|
|
93
|
+
await page().fill('#password', password);
|
|
94
|
+
await page().click('button[type="submit"]');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static async verifyDashboardVisible() {
|
|
98
|
+
await page().waitForSelector('#dashboard');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### 3. Step Definition
|
|
104
|
+
|
|
105
|
+
Create a `.ts` file in `tests/step_definitions/`. Just import the page and call its static methods directly.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// tests/step_definitions/LoginSteps.ts
|
|
109
|
+
|
|
110
|
+
import { Given, When, Then } from '@cucumber/cucumber';
|
|
111
|
+
import { LoginPage } from '../pages/LoginPage';
|
|
112
|
+
|
|
113
|
+
Given('I am on the login page', async () => {
|
|
114
|
+
await LoginPage.goToLoginPage();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
When('I log in with valid credentials', async () => {
|
|
118
|
+
await LoginPage.login('user@example.com', 'password');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
Then('I should see the dashboard', async () => {
|
|
122
|
+
await LoginPage.verifyDashboardVisible();
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### Running your test
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
plum dev @test-login-1 # run a single scenario
|
|
130
|
+
plum dev @suite-login # run the whole suite
|
|
131
|
+
plum dev # run all tests
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## For Developers
|
|
137
|
+
|
|
138
|
+
> People who want to contribute to or develop Plum itself.
|
|
139
|
+
|
|
140
|
+
### Setup
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
git clone https://github.com/silverlunah/plum.git
|
|
144
|
+
cd plum
|
|
145
|
+
npm run init
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`npm run init` installs all dependencies across the root, backend, and frontend.
|
|
149
|
+
|
|
150
|
+
### Backend Local Commands
|
|
151
|
+
|
|
152
|
+
Run these from the `backend/` directory:
|
|
153
|
+
|
|
154
|
+
| Command | Description |
|
|
155
|
+
| -------------------------- | ---------------------------------------------------------------- |
|
|
156
|
+
| `npm run init` | Create `.env` and copy scaffold into `backend/tests/` |
|
|
157
|
+
| `npm test [-- tag]` | Run tests from `backend/tests/` directly |
|
|
158
|
+
| `npm test -- --parallel N` | Run tests in parallel with N workers |
|
|
159
|
+
| `npm run create-step` | Interactive prompt to generate a step definition and page object |
|
|
52
160
|
|
|
53
|
-
|
|
54
|
-
|
|
161
|
+
```bash
|
|
162
|
+
cd backend
|
|
163
|
+
npm run init # sets up .env and tests/ for local development
|
|
164
|
+
npm test # run all tests
|
|
165
|
+
npm test -- @test-1 # run a specific test by tag
|
|
166
|
+
npm test -- --parallel 4 # run tests in parallel
|
|
167
|
+
npm run create-step # generate a new step and page interactively
|
|
168
|
+
```
|
|
55
169
|
|
|
56
|
-
|
|
170
|
+
### Docker
|
|
57
171
|
|
|
58
|
-
|
|
172
|
+
```bash
|
|
173
|
+
npm run docker:up # build and start all services
|
|
174
|
+
npm run docker:down # stop all services
|
|
175
|
+
```
|
|
59
176
|
|
|
60
|
-
|
|
61
|
-
2. `cd plum`
|
|
62
|
-
3. Initialize the project by:<br/>`npm run init`
|
|
63
|
-
4. Check if its running:<br/> `docker compose up --build -d`
|
|
177
|
+
---
|
|
64
178
|
|
|
65
179
|
## Other
|
|
66
180
|
|
|
@@ -1,6 +1,48 @@
|
|
|
1
|
-
@suite-
|
|
1
|
+
@suite-login
|
|
2
2
|
Feature: Demo Sauce Login
|
|
3
3
|
|
|
4
|
+
# Background runs before every scenario in this file
|
|
5
|
+
Background:
|
|
6
|
+
Given I am in Demo Sauce Login page
|
|
7
|
+
|
|
8
|
+
# ── Basic Scenario ─────────────────────────────────────────────────────────
|
|
4
9
|
@test-1
|
|
5
|
-
Scenario:
|
|
6
|
-
|
|
10
|
+
Scenario: User can log in with valid credentials
|
|
11
|
+
When I enter "standard_user" in username field
|
|
12
|
+
And I enter "secret_sauce" in password field
|
|
13
|
+
And I click on the login button
|
|
14
|
+
Then I should be navigated to the products page
|
|
15
|
+
|
|
16
|
+
@test-2
|
|
17
|
+
Scenario: User cannot log in with invalid credentials
|
|
18
|
+
When I enter "invalid_user" in username field
|
|
19
|
+
And I enter "invalid_password" in password field
|
|
20
|
+
And I click on the login button
|
|
21
|
+
Then the login should fail
|
|
22
|
+
|
|
23
|
+
# ── Scenario Outline ───────────────────────────────────────────────────────
|
|
24
|
+
# Runs once per row in the Examples table.
|
|
25
|
+
# Use <placeholder> in steps to reference column headers.
|
|
26
|
+
@test-3
|
|
27
|
+
Scenario Outline: User login attempts with different credentials
|
|
28
|
+
When I enter "<username>" in username field
|
|
29
|
+
And I enter "<password>" in password field
|
|
30
|
+
And I click on the login button
|
|
31
|
+
Then the login outcome should be "<outcome>"
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
| username | password | outcome |
|
|
35
|
+
| standard_user | secret_sauce | success |
|
|
36
|
+
| locked_out_user | secret_sauce | failure |
|
|
37
|
+
| invalid_user | wrong_pass | failure |
|
|
38
|
+
|
|
39
|
+
# ── Data Table ─────────────────────────────────────────────────────────────
|
|
40
|
+
# Pass structured data directly into a step.
|
|
41
|
+
# Access rows in your step definition via dataTable.hashes().
|
|
42
|
+
@test-4
|
|
43
|
+
Scenario: User can log in using a data table
|
|
44
|
+
When I fill in the login form:
|
|
45
|
+
| field | value |
|
|
46
|
+
| username | standard_user |
|
|
47
|
+
| password | secret_sauce |
|
|
48
|
+
Then I should be navigated to the products page
|
|
@@ -1,24 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import test from '@playwright/test';
|
|
2
|
+
import { page } from '../utils/browser';
|
|
3
3
|
import { SAMPLE_CONSTANT } from '../utils/constants';
|
|
4
4
|
import { Utils } from '../utils/utils';
|
|
5
5
|
|
|
6
6
|
export class LoginPage {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
static async goToLoginPage() {
|
|
8
|
+
console.log(SAMPLE_CONSTANT);
|
|
9
|
+
await Utils.goToPage(process.env.BASE_URL as string);
|
|
10
|
+
await page().waitForTimeout(3000);
|
|
11
|
+
}
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
this.utils = new Utils(this.page);
|
|
13
|
+
static async skipTest() {
|
|
14
|
+
test.skip();
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
async
|
|
16
|
-
|
|
17
|
-
await this.utils.goToPage(process.env.BASE_URL as string);
|
|
18
|
-
await this.page.waitForTimeout(3000);
|
|
17
|
+
static async iEnterUsername(username: string) {
|
|
18
|
+
await page().fill('#user-name', username);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
async
|
|
22
|
-
|
|
21
|
+
static async iEnterPassword(password: string) {
|
|
22
|
+
await page().fill('#password', password);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static async iClickOnTheLoginButton() {
|
|
26
|
+
await page().click('#login-button');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static async fillLoginForm(fields: { field: string; value: string }[]) {
|
|
30
|
+
for (const { field, value } of fields) {
|
|
31
|
+
if (field === 'username') await page().fill('#user-name', value);
|
|
32
|
+
if (field === 'password') await page().fill('#password', value);
|
|
33
|
+
}
|
|
34
|
+
await page().click('#login-button');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static async verifyLoginOutcome(outcome: string) {
|
|
38
|
+
if (outcome === 'success') {
|
|
39
|
+
await page().waitForSelector('.title');
|
|
40
|
+
} else {
|
|
41
|
+
await page().waitForSelector('.error-message-container');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static async verifyLoginFailed() {
|
|
46
|
+
await page().waitForSelector('.error-message-container');
|
|
23
47
|
}
|
|
24
48
|
}
|
|
@@ -1,9 +1,35 @@
|
|
|
1
|
-
import { Given } from '@cucumber/cucumber';
|
|
1
|
+
import { Given, When, Then, DataTable } from '@cucumber/cucumber';
|
|
2
|
+
import { LoginPage } from '../pages/LoginPage';
|
|
2
3
|
|
|
3
|
-
Given('I am in Demo Sauce Login page', async
|
|
4
|
-
await
|
|
4
|
+
Given('I am in Demo Sauce Login page', async () => {
|
|
5
|
+
await LoginPage.goToLoginPage();
|
|
5
6
|
});
|
|
6
7
|
|
|
7
|
-
Given('I fail this test', async
|
|
8
|
+
Given('I fail this test', async () => {
|
|
8
9
|
throw new Error('err');
|
|
9
10
|
});
|
|
11
|
+
|
|
12
|
+
When('I enter {string} in username field', async (username: string) => {
|
|
13
|
+
await LoginPage.iEnterUsername(username);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
When('I enter {string} in password field', async (password: string) => {
|
|
17
|
+
await LoginPage.iEnterPassword(password);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
When('I click on the login button', async () => {
|
|
21
|
+
await LoginPage.iClickOnTheLoginButton();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
When('I fill in the login form:', async (dataTable: DataTable) => {
|
|
25
|
+
const fields = dataTable.hashes() as { field: string; value: string }[];
|
|
26
|
+
await LoginPage.fillLoginForm(fields);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
Then('the login outcome should be {string}', async (outcome: string) => {
|
|
30
|
+
await LoginPage.verifyLoginOutcome(outcome);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
Then('the login should fail', async () => {
|
|
34
|
+
await LoginPage.verifyLoginFailed();
|
|
35
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { chromium, Browser, BrowserContext, Page } from 'playwright';
|
|
2
|
+
|
|
3
|
+
let _browser: Browser;
|
|
4
|
+
let _context: BrowserContext;
|
|
5
|
+
let _page: Page;
|
|
6
|
+
|
|
7
|
+
export const page = (): Page => _page;
|
|
8
|
+
|
|
9
|
+
export async function setup(): Promise<void> {
|
|
10
|
+
const isHeadless = process.env.IS_HEADLESS?.toLowerCase() !== 'false';
|
|
11
|
+
_browser = await chromium.launch({ headless: isHeadless });
|
|
12
|
+
_context = await _browser.newContext();
|
|
13
|
+
_page = await _context.newPage();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function teardown(
|
|
17
|
+
attach: (data: Buffer, mime: string) => Promise<void>,
|
|
18
|
+
failed: boolean
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
if (failed && _page) {
|
|
21
|
+
const screenshotDir = 'reports/screenshots';
|
|
22
|
+
const fs = await import('fs');
|
|
23
|
+
const path = await import('path');
|
|
24
|
+
if (!fs.existsSync(screenshotDir)) {
|
|
25
|
+
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
const screenshotPath = path.join(screenshotDir, `screenshot_${Date.now()}.png`);
|
|
28
|
+
await _page.screenshot({ path: screenshotPath });
|
|
29
|
+
const screenshotData = fs.readFileSync(screenshotPath);
|
|
30
|
+
await attach(screenshotData, 'image/png');
|
|
31
|
+
}
|
|
32
|
+
await _browser?.close();
|
|
33
|
+
}
|
|
@@ -1,36 +1,15 @@
|
|
|
1
|
-
import { Before, After,
|
|
2
|
-
import {
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { CustomWorld } from './world';
|
|
1
|
+
import { Before, After, ITestCaseHookParameter } from '@cucumber/cucumber';
|
|
2
|
+
import { setup, teardown } from './browser';
|
|
6
3
|
import dotenv from 'dotenv';
|
|
7
4
|
|
|
8
5
|
dotenv.config();
|
|
9
|
-
setWorldConstructor(CustomWorld);
|
|
10
6
|
|
|
11
|
-
Before(async
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this.context = await this.browser.newContext();
|
|
16
|
-
this.page = await this.context.newPage();
|
|
17
|
-
|
|
18
|
-
this.initPages(this.page);
|
|
7
|
+
Before(async ({ pickle }: ITestCaseHookParameter) => {
|
|
8
|
+
const tags = pickle.tags.map((t) => t.name).join(' ');
|
|
9
|
+
console.log(`\n▶ ${pickle.name}${tags ? ` ${tags}` : ''}`);
|
|
10
|
+
await setup();
|
|
19
11
|
});
|
|
20
12
|
|
|
21
|
-
After(async function (
|
|
22
|
-
|
|
23
|
-
const screenshotDir = path.join('reports', 'screenshots');
|
|
24
|
-
if (!fs.existsSync(screenshotDir)) {
|
|
25
|
-
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const screenshotPath = path.join(screenshotDir, `screenshot_${Date.now()}.png`);
|
|
29
|
-
await this.page.screenshot({ path: screenshotPath });
|
|
30
|
-
|
|
31
|
-
const screenshotData = fs.readFileSync(screenshotPath);
|
|
32
|
-
await this.attach(screenshotData, 'image/png');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
await this.browser?.close();
|
|
13
|
+
After(async function (scenario: ITestCaseHookParameter) {
|
|
14
|
+
await teardown(this.attach.bind(this), scenario.result?.status === 'FAILED');
|
|
36
15
|
});
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { page } from './browser';
|
|
2
2
|
|
|
3
3
|
export class Utils {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
constructor(page: Page) {
|
|
7
|
-
this.page = page;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async goToPage(url: string) {
|
|
11
|
-
await this.page.goto(url);
|
|
4
|
+
static async goToPage(url: string) {
|
|
5
|
+
await page().goto(url);
|
|
12
6
|
}
|
|
13
7
|
}
|
|
@@ -17,18 +17,14 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
-
const fse = require('fs-extra');
|
|
20
|
+
const fse = require('fs-extra');
|
|
21
|
+
const pc = require('picocolors');
|
|
21
22
|
|
|
22
|
-
// Path to the settings.json file inside the backend/config directory
|
|
23
23
|
const settingsFilePath = path.join(process.cwd(), 'config', 'settings.json');
|
|
24
24
|
const scaffoldFolderPath = path.join(process.cwd(), '_scaffold');
|
|
25
25
|
const userTestsFolderPath = path.join(process.cwd(), 'tests');
|
|
26
26
|
|
|
27
|
-
// Check if the settings.json file exists
|
|
28
27
|
if (!fs.existsSync(settingsFilePath)) {
|
|
29
|
-
console.log('⚠️ settings.json not found. Creating it with default values...');
|
|
30
|
-
|
|
31
|
-
// Default content for settings.json
|
|
32
28
|
const settingsContent = JSON.stringify(
|
|
33
29
|
{
|
|
34
30
|
reportsHistory: 20,
|
|
@@ -42,19 +38,16 @@ if (!fs.existsSync(settingsFilePath)) {
|
|
|
42
38
|
null,
|
|
43
39
|
2
|
|
44
40
|
);
|
|
45
|
-
|
|
46
|
-
// Write the settings to the settings.json file
|
|
47
41
|
fs.writeFileSync(settingsFilePath, settingsContent, 'utf8');
|
|
48
|
-
console.log('
|
|
42
|
+
console.log(pc.green('✓') + ' settings.json created with default values.');
|
|
49
43
|
} else {
|
|
50
|
-
console.log('
|
|
44
|
+
console.log(pc.yellow('⚠') + ' settings.json already exists. Skipping creation.');
|
|
51
45
|
}
|
|
52
46
|
|
|
53
|
-
// Check if the tests folder exists, if not, copy the scaffold folder as tests
|
|
54
47
|
if (!fs.existsSync(userTestsFolderPath)) {
|
|
55
|
-
console.log('
|
|
48
|
+
console.log(pc.cyan('↳') + ' tests/ not found. Creating from scaffold...');
|
|
56
49
|
fse.copySync(scaffoldFolderPath, userTestsFolderPath);
|
|
57
|
-
console.log('
|
|
50
|
+
console.log(pc.green('✓') + ' tests/ created from scaffold.');
|
|
58
51
|
} else {
|
|
59
|
-
console.log('
|
|
52
|
+
console.log(pc.yellow('⚠') + ' tests/ already exists. Skipping creation.');
|
|
60
53
|
}
|