@sdods/cli 0.7.2 → 0.9.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/dist/.tsbuildinfo +1 -1
- package/dist/commands/baselines.d.ts +3 -0
- package/dist/commands/baselines.js +111 -0
- package/dist/commands/doctor.js +44 -2
- package/dist/commands/integrations.js +47 -1
- package/dist/commands/load.d.ts +3 -0
- package/dist/commands/load.js +95 -0
- package/dist/commands/matrix.d.ts +9 -0
- package/dist/commands/matrix.js +90 -0
- package/dist/commands/report.js +79 -4
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +119 -6
- package/dist/program.js +6 -0
- package/package.json +8 -8
- package/skills/sdods/SKILL.md +2 -1
- package/templates/projects/demo-shop/features/__screenshots__/demo-shop--ui--chromium/linux/inventory.png +0 -0
- package/templates/projects/demo-shop/features/__screenshots__/demo-shop--ui--firefox/linux/inventory.png +0 -0
- package/templates/projects/demo-shop/features/__screenshots__/demo-shop--ui--webkit/linux/inventory.png +0 -0
- package/templates/projects/demo-shop/features/auth/user-pool.feature +1 -1
- package/templates/projects/demo-shop/features/cart/performance.feature +14 -0
- package/templates/projects/demo-shop/features/inventory/accessibility.feature +12 -0
- package/templates/projects/demo-shop/features/inventory/visual.feature +2 -1
- package/templates/projects/demo-shop/load/posts.yaml +17 -0
- package/templates/projects/demo-shop/sdods.project.yaml +3 -0
package/dist/commands/run.js
CHANGED
|
@@ -3,13 +3,13 @@ import { isAbsolute, join, relative, resolve as resolvePath } from 'node:path';
|
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
5
|
import { RecordingModeSchema, newRunId, runFiles, } from '@sdods/contracts';
|
|
6
|
-
import { SdodsError, CLI_OVERRIDES_ENV, VERSION, formatFindings, lintProject, listGeneratedProjects, moduleByName, moduleDir, normalizeTagExpr, parseTagExpr, redactRunTraces, serializeCliOverrides, setupTierOf, } from '@sdods/core';
|
|
6
|
+
import { SdodsError, CLI_OVERRIDES_ENV, VERSION, checkPoolCapacity, collectGateEvidence, effectiveWorkers, evaluateGates, formatFindings, hasGates, lintProject, listGeneratedProjects, moduleByName, moduleDir, normalizeTagExpr, parseTagExpr, poolAccountsFor, poolDemandByRole, poolTooSmallError, redactRunTraces, serializeCliOverrides, setupTierOf, } from '@sdods/core';
|
|
7
7
|
import { analyzeChangeImpact } from '@sdods/mcp';
|
|
8
8
|
import { createContext } from '../context.js';
|
|
9
9
|
import { browserStatuses } from './browsers.js';
|
|
10
10
|
import { maybeNotify, notifyRun } from '../notify.js';
|
|
11
11
|
import { installedPlaywrightVersion, stepResultsWarning } from '../runner-compat.js';
|
|
12
|
-
import { collect, json, out, parseIntFlag, table, warn } from '../ui.js';
|
|
12
|
+
import { collect, json, out, parseIntFlag, renderError, table, warn } from '../ui.js';
|
|
13
13
|
const RECORDING_MODES = RecordingModeSchema.options.join(' | ');
|
|
14
14
|
/** `--trace`/`--video` take Playwright's modes; refuse anything else before specs are generated. */
|
|
15
15
|
function recordingMode(flag, value) {
|
|
@@ -37,6 +37,7 @@ function addRunOptions(cmd) {
|
|
|
37
37
|
.option('--device <name>', 'device name for mobile emulation, e.g. "iPhone 15"')
|
|
38
38
|
.option('--headed', 'run headed')
|
|
39
39
|
.option('-w, --workers <n>', 'parallel workers', parseIntFlag('workers'))
|
|
40
|
+
.option('--allow-pool-contention', 'run even when a @user: role has fewer pool accounts than the workers that need it')
|
|
40
41
|
.option('--shard <i/n>', 'shard, e.g. 1/3')
|
|
41
42
|
.option('--retries <n>', 'retries per test', parseIntFlag('retries'))
|
|
42
43
|
.option('--trace <mode>', `Playwright trace: ${RECORDING_MODES} (default: evidence.trace)`)
|
|
@@ -54,7 +55,7 @@ function addRunOptions(cmd) {
|
|
|
54
55
|
.option('--har-update', 'record/refresh HAR files for @har scenarios')
|
|
55
56
|
.option('--har-replay', 'replay HAR files for @har scenarios')
|
|
56
57
|
.option('--strict', 'with --har-replay: abort on any request not in the HAR (offline)')
|
|
57
|
-
.option('--update-snapshots', '
|
|
58
|
+
.option('--update-snapshots', 'overwrite visual baselines without review (prefer `sdods baselines diff` / `accept`)')
|
|
58
59
|
.option('--ui', 'interactive UI mode')
|
|
59
60
|
.option('--debug', 'step debugger')
|
|
60
61
|
.option('--list', 'list the run targets and tests that would run')
|
|
@@ -150,6 +151,9 @@ export async function runCommand(flags, cmd) {
|
|
|
150
151
|
if (stepResults)
|
|
151
152
|
warn(stepResults);
|
|
152
153
|
const runId = flags.runId ?? newRunId();
|
|
154
|
+
if (flags.updateSnapshots)
|
|
155
|
+
warn('--update-snapshots overwrites every compared baseline without showing the diff. ' +
|
|
156
|
+
`Prefer a normal run, then \`sdods baselines diff --run ${runId}\` and \`sdods baselines accept <name> --run ${runId}\`.`);
|
|
153
157
|
const cli = {
|
|
154
158
|
env: envName,
|
|
155
159
|
headed: flags.headed,
|
|
@@ -206,6 +210,59 @@ export async function runCommand(flags, cmd) {
|
|
|
206
210
|
if (result.warnings.length && ctx.opts.verbose)
|
|
207
211
|
process.stderr.write(formatFindings(result) + '\n');
|
|
208
212
|
}
|
|
213
|
+
// --since selects features by git range; resolved here so the pool check counts the same selection.
|
|
214
|
+
const impact = flags.since
|
|
215
|
+
? analyzeChangeImpact(cfg.project.root, ctx.rootDir, flags.since)
|
|
216
|
+
: undefined;
|
|
217
|
+
// #153: refuse, before generating anything, a run whose user pool cannot cover its workers.
|
|
218
|
+
// Listing runs nothing, and --ui / --debug run one scenario at a time by hand.
|
|
219
|
+
if (!flags.list && !flags.ui && !flags.debug) {
|
|
220
|
+
const poolCfg = cfg.project.data.userPool;
|
|
221
|
+
const accounts = poolCfg && poolCfg.mode !== 'shared' && poolCfg.leaseScope !== 'scenario'
|
|
222
|
+
? poolAccountsFor(cfg)
|
|
223
|
+
: undefined;
|
|
224
|
+
if (accounts) {
|
|
225
|
+
const moduleRels = modules.map((m) => relative(cfg.project.root, moduleDir(cfg.project.root, moduleByName(projectCfg, m)))
|
|
226
|
+
.replace(/\\/g, '/')
|
|
227
|
+
.concat('/'));
|
|
228
|
+
const impactedRels = impact ? new Set(impact.impacted.map((r) => r.feature)) : undefined;
|
|
229
|
+
const grep = flags.grep ? safeRegExp(flags.grep) : undefined;
|
|
230
|
+
const features = cfg.env.vars?.features;
|
|
231
|
+
const demand = poolDemandByRole(cfg.project, cfg.env.name, {
|
|
232
|
+
layers: layers.length ? layers : projectCfg.layers,
|
|
233
|
+
browsers: browsers.length ? browsers : projectCfg.browsers,
|
|
234
|
+
tags,
|
|
235
|
+
setupTags: setup?.tags,
|
|
236
|
+
flags: typeof features === 'string'
|
|
237
|
+
? features
|
|
238
|
+
.split(',')
|
|
239
|
+
.map((f) => f.trim())
|
|
240
|
+
.filter(Boolean)
|
|
241
|
+
: undefined,
|
|
242
|
+
quarantine: process.env.SDODS_QUARANTINE === 'run' ? 'run' : 'skip',
|
|
243
|
+
fullyParallel: proc?.fullyParallel ?? projectCfg.fullyParallel,
|
|
244
|
+
includeFeature: (rel) => (!featureRel || rel === featureRel) &&
|
|
245
|
+
(!moduleRels.length || moduleRels.some((m) => rel.startsWith(m))) &&
|
|
246
|
+
(!impactedRels || impactedRels.has(rel)),
|
|
247
|
+
includeScenario: (name) => (!flags.scenario || name.includes(flags.scenario)) &&
|
|
248
|
+
(!flags.grep || (grep?.test(name) ?? false)),
|
|
249
|
+
repeatEach: flags.repeatEach,
|
|
250
|
+
shardTotal: cfg.runtime.shard?.total,
|
|
251
|
+
});
|
|
252
|
+
const shortfalls = checkPoolCapacity({
|
|
253
|
+
pool: poolCfg,
|
|
254
|
+
accounts,
|
|
255
|
+
demand,
|
|
256
|
+
workers: effectiveWorkers(cfg.runtime.workers),
|
|
257
|
+
});
|
|
258
|
+
if (shortfalls.length) {
|
|
259
|
+
const error = poolTooSmallError(cfg, shortfalls, cfg.runtime.workers);
|
|
260
|
+
if (!flags.allowPoolContention)
|
|
261
|
+
throw error;
|
|
262
|
+
warn(`${error.message} Running anyway (--allow-pool-contention).`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
209
266
|
const selection = {
|
|
210
267
|
project: entry.slug,
|
|
211
268
|
env: cfg.env.name,
|
|
@@ -318,8 +375,7 @@ export async function runCommand(flags, cmd) {
|
|
|
318
375
|
// `analyzeChangeImpact` has existed for a while and was reachable only
|
|
319
376
|
// through MCP, so it could advise a human and could not select a run. This is
|
|
320
377
|
// the seam that was missing; the mapping itself is unchanged.
|
|
321
|
-
if (flags.since) {
|
|
322
|
-
const impact = analyzeChangeImpact(cfg.project.root, ctx.rootDir, flags.since);
|
|
378
|
+
if (flags.since && impact) {
|
|
323
379
|
if (impact.impacted.length === 0) {
|
|
324
380
|
// Deliberately NOT "run everything" and deliberately not a silent empty
|
|
325
381
|
// run. A run that registered zero scenarios exits 0 and looks identical
|
|
@@ -389,6 +445,29 @@ export async function runCommand(flags, cmd) {
|
|
|
389
445
|
warn(`trace redaction failed for ${relative(ctx.rootDir, e.file)}: ${e.error}`);
|
|
390
446
|
}
|
|
391
447
|
const summary = readSummary(runDir, runId, exitCode, Date.now() - started);
|
|
448
|
+
// Process gates are judged here, from the run that just finished, before anything publishes it.
|
|
449
|
+
// One shard of several holds only part of the run, so its pass rate and its a11y/perf evidence
|
|
450
|
+
// are not the run's; a sharded process is gated where the shards are merged, not per shard.
|
|
451
|
+
const shardTotal = cfg.runtime.shard?.total ?? 1;
|
|
452
|
+
let gates;
|
|
453
|
+
if (proc && hasGates(proc.gates) && exitCode !== 130) {
|
|
454
|
+
if (shardTotal > 1) {
|
|
455
|
+
warn(`process "${proc.name}" gates are not evaluated on shard ${cfg.runtime.shard?.current}/${shardTotal}: one shard is not the whole run.`);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
gates = evaluateGates({
|
|
459
|
+
process: proc.name,
|
|
460
|
+
gates: proc.gates,
|
|
461
|
+
totals: summary?.totals,
|
|
462
|
+
evidence: collectGateEvidence(runDir),
|
|
463
|
+
});
|
|
464
|
+
writeFileSync(join(runDir, runFiles.gates), JSON.stringify(gates, null, 2));
|
|
465
|
+
if (summary)
|
|
466
|
+
summary.gates = gates;
|
|
467
|
+
manifest.gatesPassed = gates.passed;
|
|
468
|
+
writeManifest();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
392
471
|
if (summary)
|
|
393
472
|
writeFileSync(join(runDir, runFiles.summary), JSON.stringify(summary, null, 2));
|
|
394
473
|
const dbConfigured = process.env.DB_DRIVER === 'postgres'
|
|
@@ -428,6 +507,13 @@ export async function runCommand(flags, cmd) {
|
|
|
428
507
|
writeManifest();
|
|
429
508
|
}
|
|
430
509
|
}
|
|
510
|
+
// A breached gate fails a run whose scenarios passed. Exit 1, as the exit-code reference says:
|
|
511
|
+
// a gate not met is a failure, not a configuration error.
|
|
512
|
+
if (gates && !gates.passed && finalExit === 0) {
|
|
513
|
+
finalExit = 1;
|
|
514
|
+
manifest.exitCode = finalExit;
|
|
515
|
+
writeManifest();
|
|
516
|
+
}
|
|
431
517
|
// Enabled integrations (check runs, PR comment, issues) act on the run here, the same way
|
|
432
518
|
// `sdods integrations notify --run-id` does. Failures are reported and never change the exit code.
|
|
433
519
|
const notify = await maybeNotify({
|
|
@@ -461,9 +547,20 @@ export async function runCommand(flags, cmd) {
|
|
|
461
547
|
out(pc.dim(`integrations: not notified (${notify.reason})`));
|
|
462
548
|
}
|
|
463
549
|
if (ctx.opts.json) {
|
|
464
|
-
json({ runId, runDir, exitCode: finalExit, summary, manifest, notify });
|
|
550
|
+
json({ runId, runDir, exitCode: finalExit, summary, manifest, notify, gates });
|
|
465
551
|
}
|
|
466
552
|
else {
|
|
553
|
+
if (gates) {
|
|
554
|
+
out('');
|
|
555
|
+
out(pc.bold(`gates · process ${gates.process}`));
|
|
556
|
+
table(gates.rows.map((r) => ({
|
|
557
|
+
gate: r.gate,
|
|
558
|
+
threshold: r.threshold,
|
|
559
|
+
actual: r.actual,
|
|
560
|
+
result: r.passed ? 'pass' : 'FAIL',
|
|
561
|
+
detail: r.detail ?? '',
|
|
562
|
+
})));
|
|
563
|
+
}
|
|
467
564
|
const t = summary?.totals;
|
|
468
565
|
out('');
|
|
469
566
|
out(`${finalExit === 0 ? pc.green('✔ passed') : finalExit === 130 ? pc.yellow('■ cancelled') : finalExit === 2 ? pc.yellow('■ nothing ran') : pc.red('✖ failed')} ${t ? `${t.passed} passed, ${t.failed} failed, ${t.skipped} skipped, ${t.flaky} flaky` : ''} ${pc.dim(`(${Math.round((Date.now() - started) / 1000)}s)`)}`);
|
|
@@ -471,6 +568,13 @@ export async function runCommand(flags, cmd) {
|
|
|
471
568
|
out(pc.dim(`html report: ${join(runDir, runFiles.htmlReport, 'index.html')}`));
|
|
472
569
|
out(pc.dim(`dashboard: ${join(runDir, runFiles.dashboard, 'index.html')}`));
|
|
473
570
|
}
|
|
571
|
+
if (gates && !gates.passed) {
|
|
572
|
+
const failed = gates.rows.filter((r) => !r.passed);
|
|
573
|
+
renderError(new SdodsError('GATE_FAILED', `Process "${gates.process}" did not meet ${failed.length} gate(s): ${failed.map((r) => `${r.gate} (${r.actual}, needs ${r.threshold})`).join('; ')}.`, {
|
|
574
|
+
hint: `The verdict is in ${join(runDir, runFiles.gates)}. Gates are set under processes[].gates in sdods.project.yaml.`,
|
|
575
|
+
docsPath: '/docs/guides/processes-and-testing-types',
|
|
576
|
+
}), Boolean(ctx.opts.json));
|
|
577
|
+
}
|
|
474
578
|
return finalExit;
|
|
475
579
|
}
|
|
476
580
|
/**
|
|
@@ -496,6 +600,15 @@ function projectFeaturePath(projectRoot, repoRoot, input) {
|
|
|
496
600
|
});
|
|
497
601
|
return rel;
|
|
498
602
|
}
|
|
603
|
+
/** `--grep` as the pool check reads it; a pattern Playwright would reject matches no scenario. */
|
|
604
|
+
function safeRegExp(pattern) {
|
|
605
|
+
try {
|
|
606
|
+
return new RegExp(pattern);
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
499
612
|
function escapeRe(s) {
|
|
500
613
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
501
614
|
}
|
package/dist/program.js
CHANGED
|
@@ -24,6 +24,7 @@ import { register as registerCoverage } from './commands/coverage.js';
|
|
|
24
24
|
import { register as registerBrowsers } from './commands/browsers.js';
|
|
25
25
|
import { register as registerWatch } from './commands/watch.js';
|
|
26
26
|
import { register as registerTrace } from './commands/trace.js';
|
|
27
|
+
import { register as registerBaselines } from './commands/baselines.js';
|
|
27
28
|
import { register as registerAgent } from './commands/agent.js';
|
|
28
29
|
import { register as registerProposals } from './commands/proposals.js';
|
|
29
30
|
import { register as registerMcp } from './commands/mcp.js';
|
|
@@ -36,6 +37,8 @@ import { register as registerIntegrations } from './commands/integrations.js';
|
|
|
36
37
|
import { register as registerCompletion } from './commands/completion.js';
|
|
37
38
|
import { register as registerUpgrade } from './commands/upgrade.js';
|
|
38
39
|
import { register as registerFeedback } from './commands/feedback.js';
|
|
40
|
+
import { register as registerLoad } from './commands/load.js';
|
|
41
|
+
import { register as registerMatrix } from './commands/matrix.js';
|
|
39
42
|
/**
|
|
40
43
|
* All commands are registered eagerly (cheap) but each module keeps heavy imports
|
|
41
44
|
* (server, agents, DB, browser runner) inside its action via dynamic import.
|
|
@@ -78,6 +81,7 @@ export function buildProgram() {
|
|
|
78
81
|
registerBrowsers(program);
|
|
79
82
|
registerWatch(program);
|
|
80
83
|
registerTrace(program);
|
|
84
|
+
registerBaselines(program);
|
|
81
85
|
registerAgent(program);
|
|
82
86
|
registerProposals(program);
|
|
83
87
|
registerMcp(program);
|
|
@@ -90,6 +94,8 @@ export function buildProgram() {
|
|
|
90
94
|
registerCompletion(program);
|
|
91
95
|
registerUpgrade(program);
|
|
92
96
|
registerFeedback(program);
|
|
97
|
+
registerLoad(program);
|
|
98
|
+
registerMatrix(program);
|
|
93
99
|
return program;
|
|
94
100
|
}
|
|
95
101
|
//# sourceMappingURL=program.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "SDODS command line: run, record, analyze, agents, MCP server, scheduler and the web server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SDODS <admin@sdods.com>",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"node": ">=22"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@sdods/contracts": "0.
|
|
28
|
-
"@sdods/core": "0.
|
|
27
|
+
"@sdods/contracts": "0.9.0",
|
|
28
|
+
"@sdods/core": "0.9.0",
|
|
29
29
|
"commander": "^15.0.0",
|
|
30
30
|
"execa": "^10.0.1",
|
|
31
31
|
"picocolors": "^1.1.1",
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
"zod": "^4.5.4",
|
|
35
35
|
"@playwright/test": "^1.63.0",
|
|
36
36
|
"playwright-core": "^1.63.0",
|
|
37
|
-
"@sdods/integrations": "0.
|
|
38
|
-
"@sdods/mcp": "0.
|
|
39
|
-
"@sdods/agents": "0.
|
|
40
|
-
"@sdods/db": "0.
|
|
37
|
+
"@sdods/integrations": "0.9.0",
|
|
38
|
+
"@sdods/mcp": "0.9.0",
|
|
39
|
+
"@sdods/agents": "0.9.0",
|
|
40
|
+
"@sdods/db": "0.9.0",
|
|
41
41
|
"csv-parse": "^7.0.2",
|
|
42
|
-
"@sdods/server": "0.
|
|
42
|
+
"@sdods/server": "0.9.0"
|
|
43
43
|
},
|
|
44
44
|
"homepage": "https://sdods.com",
|
|
45
45
|
"bugs": {
|
package/skills/sdods/SKILL.md
CHANGED
|
@@ -18,7 +18,7 @@ over shelling out.
|
|
|
18
18
|
|
|
19
19
|
- Every scenario carries exactly one layer tag (`@ui`, `@api`, `@hybrid`) and exactly one suite tag
|
|
20
20
|
(`@smoke`, `@regression`, `@sanity`). Optional: `@visual @a11y @perf @mock @data-driven @pool`,
|
|
21
|
-
and value tags `@user:<role> @data:<dataset> @har:<name> @jira:KEY @skip:<browser>`.
|
|
21
|
+
and value tags `@user:<role> @data:<dataset> @har:<name> @jira:KEY @skip:<browser> @matrix:<name> @req:<id>`, and per-scenario emulation `@locale:<bcp47> @theme:<light|dark|no-preference> @timezone:<IANA> @viewport:<W>x<H> @device:<name>`.
|
|
22
22
|
- Reuse existing steps first (`sdods steps list -p <slug>`, or `step_find`). Add a step only when no
|
|
23
23
|
phrasing fits, and name it in the same style.
|
|
24
24
|
- Locator priority: role + name > label > test id > placeholder > text > CSS. Never XPath.
|
|
@@ -37,6 +37,7 @@ over shelling out.
|
|
|
37
37
|
| Projects and hierarchy | `sdods project list`, `sdods workspace tree` |
|
|
38
38
|
| Resolved config, with where each value came from | `sdods config show -p <slug> -e <env> --explain` |
|
|
39
39
|
| Validate features | `sdods lint -p <slug>` |
|
|
40
|
+
| Role × surface matrix (`roles.matrix.yaml`) | `sdods matrix expand -p <slug>` · `--check` in CI |
|
|
40
41
|
| Run a slice | `sdods run -p <slug> -e <env> -l api` · `-l ui -b chromium -t @smoke` · `--process pr-check` |
|
|
41
42
|
| Results | `sdods report --last`, `sdods heal report --last`, `sdods trace --last` |
|
|
42
43
|
| Available steps | `sdods steps list -p <slug>` |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
@ui @auth @pool
|
|
2
2
|
Feature: User pool
|
|
3
|
-
Accounts are leased per
|
|
3
|
+
Accounts are leased per scenario from the pool dataset; login state is cached as storageState.
|
|
4
4
|
|
|
5
5
|
@sanity @user:standard
|
|
6
6
|
Scenario: A leased standard user starts logged in through cached storage state
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@ui @cart @perf
|
|
2
|
+
Feature: Cart performance
|
|
3
|
+
The @perf tag records the vitals of every document the scenario loads and judges them against
|
|
4
|
+
perf.budgets (pageLoadMs and lcpMs from the project, pageLoadMs overridden in envs/staging.yaml).
|
|
5
|
+
|
|
6
|
+
# WebKit does not implement largest-contentful-paint, and a vital a budget applies to that never
|
|
7
|
+
# fired fails the scenario rather than passing it, so this runs on Chromium and Firefox only.
|
|
8
|
+
@regression @skip:webkit
|
|
9
|
+
Scenario: The login and cart pages load within their budgets
|
|
10
|
+
Given I am on the login page
|
|
11
|
+
When I login with "standard_user" and "{{standardPassword}}"
|
|
12
|
+
And I add "{{defaultProduct}}" to the cart
|
|
13
|
+
And I navigate to the "cart" page
|
|
14
|
+
Then the cart should list "{{defaultProduct}}"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
@ui @inventory @a11y
|
|
2
|
+
Feature: Inventory accessibility
|
|
3
|
+
The @a11y tag audits the page each scenario ends on with axe (WCAG 2.x A and AA) and fails on
|
|
4
|
+
anything at or above a11y.failOn (serious by default). SauceDemo's login, inventory and cart
|
|
5
|
+
pages have no WCAG A/AA violation axe can detect, so nothing is scoped out with include/exclude.
|
|
6
|
+
|
|
7
|
+
@regression
|
|
8
|
+
Scenario: The inventory page has no serious accessibility violations
|
|
9
|
+
Given I am on the login page
|
|
10
|
+
When I login with "standard_user" and "{{standardPassword}}"
|
|
11
|
+
Then I should be on the inventory page
|
|
12
|
+
And there should be 6 products listed
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
@ui @inventory @visual
|
|
2
2
|
Feature: Inventory visual baseline
|
|
3
|
-
Pixel comparison against a per-browser baseline
|
|
3
|
+
Pixel comparison against a per-browser, per-platform baseline. Review a failure with
|
|
4
|
+
`sdods baselines diff` and accept it with `sdods baselines accept inventory --run <id>`.
|
|
4
5
|
|
|
5
6
|
@regression
|
|
6
7
|
Scenario: Inventory page matches its baseline
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Read-only load profile for `sdods load`. The staging environment points at a public sandbox
|
|
2
|
+
# (JSONPlaceholder) and deliberately does NOT opt in to load, so only `--dry-run` works against it.
|
|
3
|
+
description: Read posts and one post's comments
|
|
4
|
+
thinkTimeSeconds: 1
|
|
5
|
+
vus: 2
|
|
6
|
+
duration: 20s
|
|
7
|
+
thresholds:
|
|
8
|
+
http_req_duration: ['p(95)<1500']
|
|
9
|
+
http_req_failed: ['rate<0.01']
|
|
10
|
+
checks: ['rate>0.99']
|
|
11
|
+
requests:
|
|
12
|
+
- name: list posts
|
|
13
|
+
path: /posts
|
|
14
|
+
expect: { status: 200, bodyContains: userId }
|
|
15
|
+
- name: post comments
|
|
16
|
+
path: /posts/1/comments
|
|
17
|
+
expect: { status: 200 }
|
|
@@ -101,6 +101,9 @@ data:
|
|
|
101
101
|
roleColumn: role
|
|
102
102
|
leaseStore: file
|
|
103
103
|
leaseTtlMs: 600000
|
|
104
|
+
# One account per role after poolSize, and `release:check:live` runs three browsers in one
|
|
105
|
+
# invocation: release each account when its scenario ends so the browsers queue for it (#153).
|
|
106
|
+
leaseScope: scenario
|
|
104
107
|
|
|
105
108
|
auth:
|
|
106
109
|
strategy: form
|