@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.
Files changed (124) hide show
  1. package/LICENSE +17 -0
  2. package/bin/sdods.js +19 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/bin.d.ts +2 -0
  5. package/dist/bin.js +11 -0
  6. package/dist/commands/agent.d.ts +3 -0
  7. package/dist/commands/agent.js +219 -0
  8. package/dist/commands/analyze.d.ts +20 -0
  9. package/dist/commands/analyze.js +216 -0
  10. package/dist/commands/auth.d.ts +7 -0
  11. package/dist/commands/auth.js +97 -0
  12. package/dist/commands/browsers.d.ts +17 -0
  13. package/dist/commands/browsers.js +103 -0
  14. package/dist/commands/completion.d.ts +17 -0
  15. package/dist/commands/completion.js +143 -0
  16. package/dist/commands/config.d.ts +3 -0
  17. package/dist/commands/config.js +79 -0
  18. package/dist/commands/coverage.d.ts +3 -0
  19. package/dist/commands/coverage.js +65 -0
  20. package/dist/commands/data.d.ts +7 -0
  21. package/dist/commands/data.js +188 -0
  22. package/dist/commands/db.d.ts +3 -0
  23. package/dist/commands/db.js +241 -0
  24. package/dist/commands/doctor.d.ts +3 -0
  25. package/dist/commands/doctor.js +323 -0
  26. package/dist/commands/env.d.ts +3 -0
  27. package/dist/commands/env.js +65 -0
  28. package/dist/commands/features.d.ts +3 -0
  29. package/dist/commands/features.js +57 -0
  30. package/dist/commands/feedback.d.ts +7 -0
  31. package/dist/commands/feedback.js +96 -0
  32. package/dist/commands/har.d.ts +3 -0
  33. package/dist/commands/har.js +124 -0
  34. package/dist/commands/heal.d.ts +4 -0
  35. package/dist/commands/heal.js +63 -0
  36. package/dist/commands/init.d.ts +38 -0
  37. package/dist/commands/init.js +363 -0
  38. package/dist/commands/insights.d.ts +7 -0
  39. package/dist/commands/insights.js +127 -0
  40. package/dist/commands/integrations.d.ts +14 -0
  41. package/dist/commands/integrations.js +306 -0
  42. package/dist/commands/lint.d.ts +3 -0
  43. package/dist/commands/lint.js +74 -0
  44. package/dist/commands/mcp.d.ts +3 -0
  45. package/dist/commands/mcp.js +190 -0
  46. package/dist/commands/project.d.ts +3 -0
  47. package/dist/commands/project.js +133 -0
  48. package/dist/commands/proposals.d.ts +3 -0
  49. package/dist/commands/proposals.js +106 -0
  50. package/dist/commands/record.d.ts +3 -0
  51. package/dist/commands/record.js +174 -0
  52. package/dist/commands/report.d.ts +3 -0
  53. package/dist/commands/report.js +255 -0
  54. package/dist/commands/run.d.ts +41 -0
  55. package/dist/commands/run.js +369 -0
  56. package/dist/commands/schedule.d.ts +3 -0
  57. package/dist/commands/schedule.js +337 -0
  58. package/dist/commands/serve.d.ts +3 -0
  59. package/dist/commands/serve.js +38 -0
  60. package/dist/commands/steps.d.ts +8 -0
  61. package/dist/commands/steps.js +67 -0
  62. package/dist/commands/tokens.d.ts +3 -0
  63. package/dist/commands/tokens.js +110 -0
  64. package/dist/commands/trace.d.ts +10 -0
  65. package/dist/commands/trace.js +104 -0
  66. package/dist/commands/upgrade.d.ts +14 -0
  67. package/dist/commands/upgrade.js +115 -0
  68. package/dist/commands/users.d.ts +3 -0
  69. package/dist/commands/users.js +183 -0
  70. package/dist/commands/watch.d.ts +7 -0
  71. package/dist/commands/watch.js +116 -0
  72. package/dist/commands/workspace.d.ts +8 -0
  73. package/dist/commands/workspace.js +83 -0
  74. package/dist/context.d.ts +17 -0
  75. package/dist/context.js +22 -0
  76. package/dist/index.d.ts +3 -0
  77. package/dist/index.js +3 -0
  78. package/dist/program.d.ts +7 -0
  79. package/dist/program.js +90 -0
  80. package/dist/templates/project.d.ts +10 -0
  81. package/dist/templates/project.js +115 -0
  82. package/dist/ui.d.ts +16 -0
  83. package/dist/ui.js +67 -0
  84. package/package.json +61 -0
  85. package/templates/.claude/skills/sdods-record/SKILL.md +75 -0
  86. package/templates/.claude/skills/sdods-run/SKILL.md +54 -0
  87. package/templates/.env.example +62 -0
  88. package/templates/docker-compose.yml +40 -0
  89. package/templates/projects/demo-shop/.env.example +4 -0
  90. package/templates/projects/demo-shop/data/common/posts.yaml +11 -0
  91. package/templates/projects/demo-shop/data/common/products.json +8 -0
  92. package/templates/projects/demo-shop/data/common/users.csv +7 -0
  93. package/templates/projects/demo-shop/data/factories.ts +16 -0
  94. package/templates/projects/demo-shop/data/staging/users.csv +6 -0
  95. package/templates/projects/demo-shop/envs/local.yaml +17 -0
  96. package/templates/projects/demo-shop/envs/staging.yaml +23 -0
  97. package/templates/projects/demo-shop/features/__screenshots__/demo-shop--ui--chromium/darwin/inventory.png +0 -0
  98. package/templates/projects/demo-shop/features/api/posts.feature +90 -0
  99. package/templates/projects/demo-shop/features/auth/data-driven-login.feature +17 -0
  100. package/templates/projects/demo-shop/features/auth/login.feature +32 -0
  101. package/templates/projects/demo-shop/features/auth/user-pool.feature +20 -0
  102. package/templates/projects/demo-shop/features/cart/cart.feature +33 -0
  103. package/templates/projects/demo-shop/features/hybrid/seed-then-verify.feature +28 -0
  104. package/templates/projects/demo-shop/features/inventory/inventory.feature +38 -0
  105. package/templates/projects/demo-shop/features/inventory/network-intercept.feature +29 -0
  106. package/templates/projects/demo-shop/features/inventory/visual.feature +10 -0
  107. package/templates/projects/demo-shop/har/staging/cart.har +1 -0
  108. package/templates/projects/demo-shop/har/staging/cart.har.json +11 -0
  109. package/templates/projects/demo-shop/har/staging/login.har +1 -0
  110. package/templates/projects/demo-shop/har/staging/login.har.json +11 -0
  111. package/templates/projects/demo-shop/har/staging/posts.api.har +2061 -0
  112. package/templates/projects/demo-shop/har/staging/posts.har.json +8 -0
  113. package/templates/projects/demo-shop/har/staging/products.har +1 -0
  114. package/templates/projects/demo-shop/har/staging/products.har.json +11 -0
  115. package/templates/projects/demo-shop/har/staging/site.api.har +148 -0
  116. package/templates/projects/demo-shop/har/staging/site.har +1 -0
  117. package/templates/projects/demo-shop/har/staging/site.har.json +11 -0
  118. package/templates/projects/demo-shop/pages/CartPage.ts +20 -0
  119. package/templates/projects/demo-shop/pages/InventoryPage.ts +103 -0
  120. package/templates/projects/demo-shop/pages/LoginPage.ts +42 -0
  121. package/templates/projects/demo-shop/schemas/post.schema.json +13 -0
  122. package/templates/projects/demo-shop/sdods.project.yaml +185 -0
  123. package/templates/projects/demo-shop/steps/auth.ts +7 -0
  124. package/templates/projects/demo-shop/steps/fixtures.ts +39 -0
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "products",
3
+ "project": "demo-shop",
4
+ "env": "staging",
5
+ "urlGlob": "**/*",
6
+ "recordedAt": "2026-09-04T00:55:05.597Z",
7
+ "source": "run",
8
+ "scenarios": [
9
+ "Products are displayed"
10
+ ]
11
+ }
@@ -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,11 @@
1
+ {
2
+ "name": "site",
3
+ "project": "demo-shop",
4
+ "env": "staging",
5
+ "urlGlob": "**/*",
6
+ "recordedAt": "2026-09-04T00:55:07.403Z",
7
+ "source": "api",
8
+ "scenarios": [
9
+ "The site answers over HTTP"
10
+ ]
11
+ }
@@ -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);