@sdods/cli 0.2.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/LICENSE +17 -0
- package/bin/sdods.js +19 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +219 -0
- package/dist/commands/analyze.d.ts +20 -0
- package/dist/commands/analyze.js +216 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.js +97 -0
- package/dist/commands/browsers.d.ts +17 -0
- package/dist/commands/browsers.js +103 -0
- package/dist/commands/completion.d.ts +17 -0
- package/dist/commands/completion.js +143 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.js +79 -0
- package/dist/commands/coverage.d.ts +3 -0
- package/dist/commands/coverage.js +65 -0
- package/dist/commands/data.d.ts +7 -0
- package/dist/commands/data.js +188 -0
- package/dist/commands/db.d.ts +3 -0
- package/dist/commands/db.js +241 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +323 -0
- package/dist/commands/env.d.ts +3 -0
- package/dist/commands/env.js +65 -0
- package/dist/commands/features.d.ts +3 -0
- package/dist/commands/features.js +57 -0
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +96 -0
- package/dist/commands/har.d.ts +3 -0
- package/dist/commands/har.js +124 -0
- package/dist/commands/heal.d.ts +4 -0
- package/dist/commands/heal.js +63 -0
- package/dist/commands/init.d.ts +38 -0
- package/dist/commands/init.js +363 -0
- package/dist/commands/insights.d.ts +7 -0
- package/dist/commands/insights.js +127 -0
- package/dist/commands/integrations.d.ts +14 -0
- package/dist/commands/integrations.js +306 -0
- package/dist/commands/lint.d.ts +3 -0
- package/dist/commands/lint.js +74 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.js +190 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.js +133 -0
- package/dist/commands/proposals.d.ts +3 -0
- package/dist/commands/proposals.js +106 -0
- package/dist/commands/record.d.ts +3 -0
- package/dist/commands/record.js +174 -0
- package/dist/commands/report.d.ts +3 -0
- package/dist/commands/report.js +255 -0
- package/dist/commands/run.d.ts +41 -0
- package/dist/commands/run.js +369 -0
- package/dist/commands/schedule.d.ts +3 -0
- package/dist/commands/schedule.js +337 -0
- package/dist/commands/serve.d.ts +3 -0
- package/dist/commands/serve.js +38 -0
- package/dist/commands/steps.d.ts +8 -0
- package/dist/commands/steps.js +67 -0
- package/dist/commands/tokens.d.ts +3 -0
- package/dist/commands/tokens.js +110 -0
- package/dist/commands/trace.d.ts +10 -0
- package/dist/commands/trace.js +104 -0
- package/dist/commands/upgrade.d.ts +14 -0
- package/dist/commands/upgrade.js +115 -0
- package/dist/commands/users.d.ts +3 -0
- package/dist/commands/users.js +183 -0
- package/dist/commands/watch.d.ts +7 -0
- package/dist/commands/watch.js +116 -0
- package/dist/commands/workspace.d.ts +8 -0
- package/dist/commands/workspace.js +83 -0
- package/dist/context.d.ts +17 -0
- package/dist/context.js +22 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/program.d.ts +7 -0
- package/dist/program.js +90 -0
- package/dist/templates/project.d.ts +10 -0
- package/dist/templates/project.js +115 -0
- package/dist/ui.d.ts +16 -0
- package/dist/ui.js +67 -0
- package/package.json +61 -0
- package/templates/.claude/skills/sdods-record/SKILL.md +75 -0
- package/templates/.claude/skills/sdods-run/SKILL.md +54 -0
- package/templates/.env.example +62 -0
- package/templates/docker-compose.yml +40 -0
- package/templates/projects/demo-shop/.env.example +4 -0
- package/templates/projects/demo-shop/data/common/posts.yaml +11 -0
- package/templates/projects/demo-shop/data/common/products.json +8 -0
- package/templates/projects/demo-shop/data/common/users.csv +7 -0
- package/templates/projects/demo-shop/data/factories.ts +16 -0
- package/templates/projects/demo-shop/data/staging/users.csv +6 -0
- package/templates/projects/demo-shop/envs/local.yaml +17 -0
- package/templates/projects/demo-shop/envs/staging.yaml +23 -0
- package/templates/projects/demo-shop/features/__screenshots__/demo-shop--ui--chromium/darwin/inventory.png +0 -0
- package/templates/projects/demo-shop/features/api/posts.feature +90 -0
- package/templates/projects/demo-shop/features/auth/data-driven-login.feature +17 -0
- package/templates/projects/demo-shop/features/auth/login.feature +32 -0
- package/templates/projects/demo-shop/features/auth/user-pool.feature +20 -0
- package/templates/projects/demo-shop/features/cart/cart.feature +33 -0
- package/templates/projects/demo-shop/features/hybrid/seed-then-verify.feature +28 -0
- package/templates/projects/demo-shop/features/inventory/inventory.feature +38 -0
- package/templates/projects/demo-shop/features/inventory/network-intercept.feature +29 -0
- package/templates/projects/demo-shop/features/inventory/visual.feature +10 -0
- package/templates/projects/demo-shop/har/staging/cart.har +1 -0
- package/templates/projects/demo-shop/har/staging/cart.har.json +11 -0
- package/templates/projects/demo-shop/har/staging/login.har +1 -0
- package/templates/projects/demo-shop/har/staging/login.har.json +11 -0
- package/templates/projects/demo-shop/har/staging/posts.api.har +2061 -0
- package/templates/projects/demo-shop/har/staging/posts.har.json +8 -0
- package/templates/projects/demo-shop/har/staging/products.har +1 -0
- package/templates/projects/demo-shop/har/staging/products.har.json +11 -0
- package/templates/projects/demo-shop/har/staging/site.api.har +148 -0
- package/templates/projects/demo-shop/har/staging/site.har +1 -0
- package/templates/projects/demo-shop/har/staging/site.har.json +11 -0
- package/templates/projects/demo-shop/pages/CartPage.ts +20 -0
- package/templates/projects/demo-shop/pages/InventoryPage.ts +103 -0
- package/templates/projects/demo-shop/pages/LoginPage.ts +42 -0
- package/templates/projects/demo-shop/schemas/post.schema.json +13 -0
- package/templates/projects/demo-shop/sdods.project.yaml +185 -0
- package/templates/projects/demo-shop/steps/auth.ts +7 -0
- package/templates/projects/demo-shop/steps/fixtures.ts +39 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"log": {
|
|
3
|
+
"version": "1.2",
|
|
4
|
+
"creator": {
|
|
5
|
+
"name": "AutoMax",
|
|
6
|
+
"version": "0.1.0"
|
|
7
|
+
},
|
|
8
|
+
"entries": [
|
|
9
|
+
{
|
|
10
|
+
"startedDateTime": "2026-09-04T00:55:07.421Z",
|
|
11
|
+
"time": 236,
|
|
12
|
+
"request": {
|
|
13
|
+
"method": "GET",
|
|
14
|
+
"url": "https://www.saucedemo.com/",
|
|
15
|
+
"httpVersion": "HTTP/1.1",
|
|
16
|
+
"headers": [
|
|
17
|
+
{
|
|
18
|
+
"name": "accept",
|
|
19
|
+
"value": "application/json"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"queryString": [],
|
|
23
|
+
"cookies": [],
|
|
24
|
+
"headersSize": -1,
|
|
25
|
+
"bodySize": -1
|
|
26
|
+
},
|
|
27
|
+
"response": {
|
|
28
|
+
"status": 200,
|
|
29
|
+
"statusText": "OK",
|
|
30
|
+
"httpVersion": "HTTP/1.1",
|
|
31
|
+
"headers": [
|
|
32
|
+
{
|
|
33
|
+
"name": "connection",
|
|
34
|
+
"value": "keep-alive"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "content-length",
|
|
38
|
+
"value": "726"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "server",
|
|
42
|
+
"value": "GitHub.com"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "content-type",
|
|
46
|
+
"value": "text/html; charset=utf-8"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "last-modified",
|
|
50
|
+
"value": "Wed, 08 Jul 2026 12:11:17 GMT"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "access-control-allow-origin",
|
|
54
|
+
"value": "*"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "etag",
|
|
58
|
+
"value": "W/\"6a4e3e65-545\""
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "expires",
|
|
62
|
+
"value": "Sun, 23 Aug 2026 21:06:33 GMT"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "cache-control",
|
|
66
|
+
"value": "max-age=600"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "content-encoding",
|
|
70
|
+
"value": "gzip"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "x-proxy-cache",
|
|
74
|
+
"value": "HIT"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "x-github-request-id",
|
|
78
|
+
"value": "B198:1ADFD4:310AAF2:373C471:6A8D2B9B"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "x-github-edge-region",
|
|
82
|
+
"value": "iad"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "accept-ranges",
|
|
86
|
+
"value": "bytes"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "date",
|
|
90
|
+
"value": "Fri, 04 Sep 2026 00:55:07 GMT"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "via",
|
|
94
|
+
"value": "1.1 varnish"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "age",
|
|
98
|
+
"value": "301"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "x-served-by",
|
|
102
|
+
"value": "cache-iad-kiad7000122-IAD"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "x-cache",
|
|
106
|
+
"value": "HIT"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "x-cache-hits",
|
|
110
|
+
"value": "1"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "x-timer",
|
|
114
|
+
"value": "S1788483308.683248,VS0,VE4"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "vary",
|
|
118
|
+
"value": "Accept-Encoding"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "x-fastly-request-id",
|
|
122
|
+
"value": "f14bf48c0af6e153d4f431cc7e44a43c573c8d6e"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"cookies": [],
|
|
126
|
+
"content": {
|
|
127
|
+
"size": 1349,
|
|
128
|
+
"mimeType": "text/html; charset=utf-8",
|
|
129
|
+
"text": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"/favicon.ico\" />\n <meta name=\"robots\" content=\"noindex\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"theme-color\" content=\"#ffffff\" />\n <meta name=\"description\" content=\"Sauce Labs Swag Labs app\" />\n <link rel=\"apple-touch-icon\" href=\"/icon-192x192.png\" />\n <link rel=\"manifest\" href=\"/manifest.json\"/>\n\n <!-- Single Page Apps for GitHub Pages -->\n <script type=\"text/javascript\">\n // https://github.com/rafgraph/spa-github-pages\n (function(l) {\n if (l.search[1] === '/' ) {\n var decoded = l.search.slice(1).split('&').map(function(s) {\n return s.replace(/~and~/g, '&')\n }).join('?');\n window.history.replaceState(null, null,\n l.pathname.slice(0, -1) + decoded + l.hash\n );\n }\n }(window.location))\n </script>\n\n <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=DM+Sans:wght@400;500\">\n <title>Swag Labs</title>\n <script type=\"module\" crossorigin src=\"/assets/index-XyuNVFOR.js\"></script>\n <link rel=\"stylesheet\" crossorigin href=\"/assets/index-Co7SA-g_.css\">\n</head>\n<body>\n <noscript>You need to enable JavaScript to run this app.</noscript>\n <div id=\"root\"></div>\n</body>\n</html>\n\n"
|
|
130
|
+
},
|
|
131
|
+
"redirectURL": "",
|
|
132
|
+
"headersSize": -1,
|
|
133
|
+
"bodySize": -1
|
|
134
|
+
},
|
|
135
|
+
"cache": {},
|
|
136
|
+
"timings": {
|
|
137
|
+
"send": 0,
|
|
138
|
+
"wait": 236,
|
|
139
|
+
"receive": 0
|
|
140
|
+
},
|
|
141
|
+
"_sdods": {
|
|
142
|
+
"key": "GET https://www.saucedemo.com/",
|
|
143
|
+
"recordedAt": "2026-09-04T00:55:07.669Z"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"log":{"version":"1.2","creator":{"name":"Playwright","version":"1.62.1"},"browser":{"name":"chromium","version":"151.0.7922.34"},"entries":[]}}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { expect } from '@playwright/test';
|
|
2
|
+
import { Fixture, Then } from 'playwright-bdd/decorators';
|
|
3
|
+
import { BasePage } from '@sdods/core/pages';
|
|
4
|
+
import type { test } from '../steps/fixtures.js';
|
|
5
|
+
|
|
6
|
+
@Fixture<typeof test>('cartPage')
|
|
7
|
+
export class CartPage extends BasePage {
|
|
8
|
+
readonly items = this.page.locator('[data-test="inventory-item"]');
|
|
9
|
+
readonly names = this.page.locator('[data-test="inventory-item-name"]');
|
|
10
|
+
|
|
11
|
+
@Then('the cart should list {string}')
|
|
12
|
+
async assertListed(product: string) {
|
|
13
|
+
await expect(this.names.filter({ hasText: this.render(product) })).toHaveCount(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Then('the cart should contain {int} item(s)')
|
|
17
|
+
async assertCount(count: number) {
|
|
18
|
+
await expect(this.items).toHaveCount(count);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { expect } from '@playwright/test';
|
|
2
|
+
import { Fixture, Then, When } from 'playwright-bdd/decorators';
|
|
3
|
+
import { BasePage } from '@sdods/core/pages';
|
|
4
|
+
import type { test } from '../steps/fixtures.js';
|
|
5
|
+
|
|
6
|
+
@Fixture<typeof test>('inventoryPage')
|
|
7
|
+
export class InventoryPage extends BasePage {
|
|
8
|
+
readonly title = this.h(this.page.locator('.title'), {
|
|
9
|
+
description: 'page title',
|
|
10
|
+
text: 'Products',
|
|
11
|
+
testId: 'title',
|
|
12
|
+
});
|
|
13
|
+
readonly items = this.page.locator('[data-test="inventory-item"]');
|
|
14
|
+
readonly names = this.page.locator('[data-test="inventory-item-name"]');
|
|
15
|
+
readonly prices = this.page.locator('[data-test="inventory-item-price"]');
|
|
16
|
+
readonly sort = this.h(this.page.locator('[data-test="product-sort-container"]'), {
|
|
17
|
+
description: 'sort dropdown',
|
|
18
|
+
role: 'combobox',
|
|
19
|
+
testId: 'product-sort-container',
|
|
20
|
+
});
|
|
21
|
+
readonly cartBadge = this.page.locator('[data-test="shopping-cart-badge"]');
|
|
22
|
+
readonly cartLink = this.h(this.page.locator('[data-test="shopping-cart-link"]'), {
|
|
23
|
+
description: 'cart link',
|
|
24
|
+
testId: 'shopping-cart-link',
|
|
25
|
+
role: 'link',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
private slug(product: string) {
|
|
29
|
+
return this.render(product)
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9]+/g, '-');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Then('I should be on the inventory page')
|
|
35
|
+
async assertOnPage() {
|
|
36
|
+
await this.expectUrlContains('/inventory.html');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Then('the inventory title should be {string}')
|
|
40
|
+
async assertTitle(text: string) {
|
|
41
|
+
await this.title.expectText(text);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Then('there should be {int} products listed')
|
|
45
|
+
async assertCount(count: number) {
|
|
46
|
+
await expect(this.items).toHaveCount(count);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@When('I add {string} to the cart')
|
|
50
|
+
async addToCart(product: string) {
|
|
51
|
+
await this.heal.click(this.page.locator(`[data-test="add-to-cart-${this.slug(product)}"]`), {
|
|
52
|
+
description: `add ${this.render(product)} to cart`,
|
|
53
|
+
role: 'button',
|
|
54
|
+
name: 'Add to cart',
|
|
55
|
+
testId: `add-to-cart-${this.slug(product)}`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@When('I remove {string} from the cart')
|
|
60
|
+
async removeFromCart(product: string) {
|
|
61
|
+
await this.heal.click(this.page.locator(`[data-test="remove-${this.slug(product)}"]`), {
|
|
62
|
+
description: `remove ${this.render(product)} from cart`,
|
|
63
|
+
role: 'button',
|
|
64
|
+
name: 'Remove',
|
|
65
|
+
testId: `remove-${this.slug(product)}`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Then('the cart badge should show {int} item(s)')
|
|
70
|
+
async assertBadge(count: number) {
|
|
71
|
+
await expect(this.cartBadge).toHaveText(String(count));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Then('the cart badge should be hidden')
|
|
75
|
+
async assertBadgeHidden() {
|
|
76
|
+
await expect(this.cartBadge).toHaveCount(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@When('I sort products by {string}')
|
|
80
|
+
async sortBy(label: string) {
|
|
81
|
+
await this.sort.selectOption(label);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Then('the product prices should be sorted ascending')
|
|
85
|
+
async assertPricesAscending() {
|
|
86
|
+
const prices = (await this.prices.allTextContents()).map((p) =>
|
|
87
|
+
Number(p.replace(/[^0-9.]/g, '')),
|
|
88
|
+
);
|
|
89
|
+
expect(prices).toEqual([...prices].sort((a, b) => a - b));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Then('the product names should be sorted descending')
|
|
93
|
+
async assertNamesDescending() {
|
|
94
|
+
const names = await this.names.allTextContents();
|
|
95
|
+
expect(names).toEqual([...names].sort((a, b) => b.localeCompare(a)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@When('I open the cart')
|
|
99
|
+
async openCart() {
|
|
100
|
+
await this.cartLink.click();
|
|
101
|
+
await this.expectUrlContains('/cart.html');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { expect } from '@playwright/test';
|
|
2
|
+
import { Fixture, Given, Then, When } from 'playwright-bdd/decorators';
|
|
3
|
+
import { BasePage } from '@sdods/core/pages';
|
|
4
|
+
import type { test } from '../steps/fixtures.js';
|
|
5
|
+
|
|
6
|
+
@Fixture<typeof test>('loginPage')
|
|
7
|
+
export class LoginPage extends BasePage {
|
|
8
|
+
readonly username = this.h(this.page.locator('#user-name'), {
|
|
9
|
+
description: 'username input',
|
|
10
|
+
placeholder: 'Username',
|
|
11
|
+
testId: 'username',
|
|
12
|
+
});
|
|
13
|
+
readonly password = this.h(this.page.locator('#password'), {
|
|
14
|
+
description: 'password input',
|
|
15
|
+
placeholder: 'Password',
|
|
16
|
+
testId: 'password',
|
|
17
|
+
});
|
|
18
|
+
readonly submit = this.h(this.page.locator('#login-button'), {
|
|
19
|
+
description: 'login button',
|
|
20
|
+
role: 'button',
|
|
21
|
+
name: 'Login',
|
|
22
|
+
testId: 'login-button',
|
|
23
|
+
});
|
|
24
|
+
readonly error = this.page.getByTestId('error');
|
|
25
|
+
|
|
26
|
+
@Given('I am on the login page')
|
|
27
|
+
async open() {
|
|
28
|
+
await this.goto('login');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@When('I login with {string} and {string}')
|
|
32
|
+
async login(username: string, password: string) {
|
|
33
|
+
await this.username.fill(this.render(username));
|
|
34
|
+
await this.password.fill(this.render(password));
|
|
35
|
+
await this.submit.click();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Then('I should see the login error {string}')
|
|
39
|
+
async assertError(message: string) {
|
|
40
|
+
await expect(this.error).toContainText(message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Post",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["id", "title", "userId"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"id": { "type": "integer", "minimum": 1 },
|
|
8
|
+
"userId": { "type": "integer", "minimum": 1 },
|
|
9
|
+
"title": { "type": "string", "minLength": 1 },
|
|
10
|
+
"body": { "type": "string" }
|
|
11
|
+
},
|
|
12
|
+
"additionalProperties": true
|
|
13
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# SDODS project definition. Validated by ProjectConfigSchema (@sdods/contracts).
|
|
2
|
+
slug: demo-shop
|
|
3
|
+
name: Demo Shop
|
|
4
|
+
description: >-
|
|
5
|
+
Reference project: SauceDemo UI, JSONPlaceholder API, hybrid API→UI chaining,
|
|
6
|
+
data-driven CSV logins, a user pool, HAR playback, visual/a11y/perf examples.
|
|
7
|
+
organization: sdods
|
|
8
|
+
workspace: default
|
|
9
|
+
layers: [ui, api, hybrid, recorded]
|
|
10
|
+
browsers: [chromium, firefox, webkit]
|
|
11
|
+
testIdAttribute: data-test # SauceDemo uses data-test="..."
|
|
12
|
+
|
|
13
|
+
# Modules: feature directories with their testing types and the tag every scenario carries.
|
|
14
|
+
modules:
|
|
15
|
+
- name: auth
|
|
16
|
+
title: Authentication
|
|
17
|
+
path: auth
|
|
18
|
+
layers: [ui]
|
|
19
|
+
testingTypes: [functional, smoke, regression, data-driven]
|
|
20
|
+
tags: ['@auth']
|
|
21
|
+
routes: [login]
|
|
22
|
+
- name: inventory
|
|
23
|
+
title: Inventory
|
|
24
|
+
path: inventory
|
|
25
|
+
layers: [ui]
|
|
26
|
+
testingTypes: [functional, regression, visual, accessibility]
|
|
27
|
+
tags: ['@inventory']
|
|
28
|
+
routes: [inventory]
|
|
29
|
+
- name: cart
|
|
30
|
+
title: Cart
|
|
31
|
+
path: cart
|
|
32
|
+
layers: [ui]
|
|
33
|
+
testingTypes: [functional, regression]
|
|
34
|
+
tags: ['@cart']
|
|
35
|
+
routes: [cart, checkout]
|
|
36
|
+
- name: posts-api
|
|
37
|
+
title: Posts API
|
|
38
|
+
path: api
|
|
39
|
+
layers: [api]
|
|
40
|
+
testingTypes: [functional, contract, smoke, regression]
|
|
41
|
+
tags: ['@posts']
|
|
42
|
+
endpoints: ['/posts', '/posts/{id}', '/posts/{id}/comments']
|
|
43
|
+
- name: hybrid
|
|
44
|
+
title: Hybrid flows
|
|
45
|
+
path: hybrid
|
|
46
|
+
layers: [hybrid]
|
|
47
|
+
testingTypes: [integration]
|
|
48
|
+
tags: ['@hybrid-demo']
|
|
49
|
+
|
|
50
|
+
# Processes: named run recipes (project ones override workspace defaults with the same name).
|
|
51
|
+
processes:
|
|
52
|
+
- name: pr-check
|
|
53
|
+
title: Pull request check
|
|
54
|
+
trigger: pr
|
|
55
|
+
tags: '@smoke'
|
|
56
|
+
browsers: [chromium]
|
|
57
|
+
harMode: replay
|
|
58
|
+
gates: { minPassRate: 100 }
|
|
59
|
+
- name: nightly-regression
|
|
60
|
+
title: Nightly regression
|
|
61
|
+
trigger: nightly
|
|
62
|
+
tags: '@regression'
|
|
63
|
+
browsers: [chromium, firefox, webkit]
|
|
64
|
+
notify: [github]
|
|
65
|
+
- name: release-gate
|
|
66
|
+
title: Release gate
|
|
67
|
+
trigger: release
|
|
68
|
+
tags: '@smoke or @regression'
|
|
69
|
+
browsers: [chromium, firefox, webkit]
|
|
70
|
+
failOnFlaky: true
|
|
71
|
+
gates: { minPassRate: 100, maxFlaky: 0, perfBudgets: true, a11y: true }
|
|
72
|
+
- name: api-contract
|
|
73
|
+
title: API contract
|
|
74
|
+
trigger: merge
|
|
75
|
+
layers: [api]
|
|
76
|
+
tags: '@contract or @smoke'
|
|
77
|
+
|
|
78
|
+
routes:
|
|
79
|
+
login: /
|
|
80
|
+
inventory: /inventory.html
|
|
81
|
+
cart: /cart.html
|
|
82
|
+
checkout: /checkout-step-one.html
|
|
83
|
+
|
|
84
|
+
tags:
|
|
85
|
+
suites: [smoke, regression, sanity]
|
|
86
|
+
extra: [mock, visual, a11y, perf, data-driven, pool, hybrid-demo, contract, flaky]
|
|
87
|
+
roles: [standard, problem, performance, locked]
|
|
88
|
+
|
|
89
|
+
envs:
|
|
90
|
+
default: staging
|
|
91
|
+
available: [local, staging]
|
|
92
|
+
|
|
93
|
+
data:
|
|
94
|
+
sources:
|
|
95
|
+
users: { type: csv, path: 'data/{env}/users.csv', fallback: data/common/users.csv }
|
|
96
|
+
products: { type: json, path: data/common/products.json }
|
|
97
|
+
posts: { type: yaml, path: 'data/{env}/posts.yaml', fallback: data/common/posts.yaml }
|
|
98
|
+
factories: ./data/factories.ts
|
|
99
|
+
userPool:
|
|
100
|
+
dataset: users
|
|
101
|
+
roleColumn: role
|
|
102
|
+
leaseStore: file
|
|
103
|
+
leaseTtlMs: 600000
|
|
104
|
+
|
|
105
|
+
auth:
|
|
106
|
+
strategy: form
|
|
107
|
+
storageState: true
|
|
108
|
+
maxAgeMinutes: 120
|
|
109
|
+
form:
|
|
110
|
+
loginPath: /
|
|
111
|
+
usernameSelector: '#user-name'
|
|
112
|
+
passwordSelector: '#password'
|
|
113
|
+
submitSelector: '#login-button'
|
|
114
|
+
readyUrl: /inventory.html
|
|
115
|
+
|
|
116
|
+
screenshots:
|
|
117
|
+
policy:
|
|
118
|
+
default: on-failure
|
|
119
|
+
'@smoke': scenario
|
|
120
|
+
'@regression': step
|
|
121
|
+
'@sanity': scenario
|
|
122
|
+
'@visual': visual
|
|
123
|
+
fullPage: false
|
|
124
|
+
mask: ['[data-test="shopping-cart-badge"]']
|
|
125
|
+
viewport: { width: 1280, height: 720 }
|
|
126
|
+
onlyOnFailure: false
|
|
127
|
+
|
|
128
|
+
heal:
|
|
129
|
+
enabled: true
|
|
130
|
+
primaryTimeoutMs: 3000
|
|
131
|
+
probeTimeoutMs: 1500
|
|
132
|
+
minScore: 0.6
|
|
133
|
+
|
|
134
|
+
timeouts: { test: 60000, expect: 10000, action: 15000, navigation: 30000, api: 15000 }
|
|
135
|
+
retries:
|
|
136
|
+
ci: 2
|
|
137
|
+
local: 0
|
|
138
|
+
byTag: { '@flaky': 2 }
|
|
139
|
+
|
|
140
|
+
perf:
|
|
141
|
+
budgets: { pageLoadMs: 4000, lcpMs: 3000, apiP95Ms: 1500 }
|
|
142
|
+
|
|
143
|
+
integrations:
|
|
144
|
+
github:
|
|
145
|
+
enabled: false
|
|
146
|
+
owner: siri1410
|
|
147
|
+
repo: SDODS
|
|
148
|
+
createIssueOnFailure: never
|
|
149
|
+
tokenEnv: GITHUB_TOKEN
|
|
150
|
+
jira:
|
|
151
|
+
enabled: false
|
|
152
|
+
projectKey: DEMO
|
|
153
|
+
createIssueOnFailure: never
|
|
154
|
+
|
|
155
|
+
mcp:
|
|
156
|
+
servers:
|
|
157
|
+
playwright:
|
|
158
|
+
transport: stdio
|
|
159
|
+
command: npx
|
|
160
|
+
args: [playwright, mcp, --headless]
|
|
161
|
+
|
|
162
|
+
agents:
|
|
163
|
+
models: { planner: claude-opus-5, generator: claude-opus-5, healer: claude-opus-5 }
|
|
164
|
+
budgetUsd: { default: 2, planner: 5 }
|
|
165
|
+
maxRunsPerJob: 3
|
|
166
|
+
|
|
167
|
+
schedules:
|
|
168
|
+
- name: smoke-hourly
|
|
169
|
+
cron: '0 * * * *'
|
|
170
|
+
timezone: UTC
|
|
171
|
+
tags: '@smoke'
|
|
172
|
+
browsers: [chromium]
|
|
173
|
+
harMode: replay
|
|
174
|
+
enabled: false
|
|
175
|
+
- name: nightly-matrix
|
|
176
|
+
cron: '0 2 * * *'
|
|
177
|
+
timezone: America/New_York
|
|
178
|
+
tags: '@regression'
|
|
179
|
+
browsers: [chromium, firefox, webkit]
|
|
180
|
+
enabled: false
|
|
181
|
+
|
|
182
|
+
reports:
|
|
183
|
+
cucumberHtml: false
|
|
184
|
+
allure: false
|
|
185
|
+
junit: false
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineAuth } from '@sdods/core/auth';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SauceDemo login strategy. Uses the `auth.form` selectors from sdods.project.yaml,
|
|
5
|
+
* producing a storageState that pool users reuse across scenarios.
|
|
6
|
+
*/
|
|
7
|
+
export const auth = defineAuth({ strategy: 'form' });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { test as base, createBdd } from '@sdods/core/fixtures';
|
|
2
|
+
import { LoginPage } from '../pages/LoginPage.js';
|
|
3
|
+
import { InventoryPage } from '../pages/InventoryPage.js';
|
|
4
|
+
import { CartPage } from '../pages/CartPage.js';
|
|
5
|
+
import { auth } from './auth.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Project test object: the SDODS merged fixtures plus this project's auth strategy and
|
|
9
|
+
* page objects. playwright-bdd imports this file (importTestFrom) for generated specs.
|
|
10
|
+
*/
|
|
11
|
+
export const test = base.extend<{
|
|
12
|
+
loginPage: LoginPage;
|
|
13
|
+
inventoryPage: InventoryPage;
|
|
14
|
+
cartPage: CartPage;
|
|
15
|
+
}>({
|
|
16
|
+
auth: [auth, { scope: 'worker', option: true }],
|
|
17
|
+
loginPage: async ({ pages }, use) => {
|
|
18
|
+
await use(pages.get(LoginPage));
|
|
19
|
+
},
|
|
20
|
+
inventoryPage: async ({ pages }, use) => {
|
|
21
|
+
await use(pages.get(InventoryPage));
|
|
22
|
+
},
|
|
23
|
+
cartPage: async ({ pages }, use) => {
|
|
24
|
+
await use(pages.get(CartPage));
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const {
|
|
29
|
+
Given,
|
|
30
|
+
When,
|
|
31
|
+
Then,
|
|
32
|
+
Step,
|
|
33
|
+
BeforeScenario,
|
|
34
|
+
AfterScenario,
|
|
35
|
+
BeforeStep,
|
|
36
|
+
AfterStep,
|
|
37
|
+
BeforeWorker,
|
|
38
|
+
AfterWorker,
|
|
39
|
+
} = createBdd(test);
|