@sdods/core 0.2.2 → 0.3.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/analyze/detectors.js +236 -24
- package/dist/analyze/index.d.ts +0 -1
- package/dist/analyze/index.js +0 -1
- package/dist/analyze/propose.js +80 -38
- package/dist/analyze/scan.js +19 -1
- package/dist/api/client.js +7 -1
- package/dist/auth/capture.js +19 -6
- package/dist/auth/index.js +23 -2
- package/dist/config/resolve.d.ts +13 -0
- package/dist/config/resolve.js +1 -0
- package/dist/config/tags.d.ts +29 -1
- package/dist/config/tags.js +46 -0
- package/dist/data/provider.js +5 -1
- package/dist/data/user-pool.js +35 -3
- package/dist/fixtures/api-context.d.ts +14 -1
- package/dist/fixtures/api-context.js +13 -0
- package/dist/fixtures/scenario.js +1 -4
- package/dist/fixtures/test.js +42 -1
- package/dist/fixtures/types.d.ts +2 -0
- package/dist/reporters/dashboard.d.ts +86 -0
- package/dist/reporters/dashboard.js +319 -61
- package/dist/shots/hooks.js +0 -10
- package/dist/steps/a11y.steps.d.ts +180 -0
- package/dist/steps/a11y.steps.js +598 -0
- package/dist/steps/api.steps.js +5 -1
- package/dist/steps/browser.steps.d.ts +27 -0
- package/dist/steps/browser.steps.js +653 -0
- package/dist/steps/clock.steps.d.ts +4 -0
- package/dist/steps/clock.steps.js +73 -0
- package/dist/steps/data.steps.js +50 -2
- package/dist/steps/db.steps.d.ts +5 -0
- package/dist/steps/db.steps.js +105 -0
- package/dist/steps/dom.steps.d.ts +2 -0
- package/dist/steps/dom.steps.js +583 -0
- package/dist/steps/iframe.steps.d.ts +2 -0
- package/dist/steps/iframe.steps.js +93 -0
- package/dist/steps/index.d.ts +10 -0
- package/dist/steps/index.js +10 -0
- package/dist/steps/net.steps.d.ts +63 -0
- package/dist/steps/net.steps.js +728 -0
- package/dist/steps/perf.steps.d.ts +248 -0
- package/dist/steps/perf.steps.js +514 -0
- package/dist/steps/tabs.steps.d.ts +5 -0
- package/dist/steps/tabs.steps.js +109 -0
- package/dist/steps/webhook.steps.d.ts +46 -0
- package/dist/steps/webhook.steps.js +129 -0
- package/package.json +3 -4
- package/dist/analyze/modules.d.ts +0 -74
- package/dist/analyze/modules.js +0 -353
- package/dist/config/playwright.d.ts +0 -37
- package/dist/config/playwright.js +0 -262
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import './params.js';
|
|
2
|
+
import type { Page, TestInfo } from '@playwright/test';
|
|
3
|
+
import type { EnvConfig, PerformanceMetrics } from '@sdods/contracts';
|
|
4
|
+
import type { ApiClient, HttpMethod } from '../api/client.js';
|
|
5
|
+
import type { ApiContext } from '../fixtures/api-context.js';
|
|
6
|
+
import type { ScenarioMeta } from '../fixtures/scenario.js';
|
|
7
|
+
/**
|
|
8
|
+
* Performance budgets.
|
|
9
|
+
*
|
|
10
|
+
* `PerfBudgetsSchema` (pageLoadMs / lcpMs / fcpMs / ttfbMs / apiP95Ms), the `perf.budgets` block in
|
|
11
|
+
* the project and env yaml, the `perfBudgets: true` release gate and `attachmentNames.perf` all
|
|
12
|
+
* existed before this file; nothing read any of them. These steps close that loop.
|
|
13
|
+
*
|
|
14
|
+
* Two rules run through every step here, both learned from hand-written project steps that got
|
|
15
|
+
* them wrong:
|
|
16
|
+
*
|
|
17
|
+
* 1. A BUDGET IS READ FROM CONFIG, NEVER FROM THE FEATURE FILE. A number typed into a `.feature`
|
|
18
|
+
* is a number that drifts from the environment it runs on — staging and a laptop do not share
|
|
19
|
+
* a page-load budget. `Then the response time should be under {int} ms` (api.steps.ts) is the
|
|
20
|
+
* literal-threshold form and stays exactly as it is; the budget-driven forms below are new
|
|
21
|
+
* patterns, not a widening of it. A local literal is still reachable where a scenario genuinely
|
|
22
|
+
* needs one — `should be under {int} ms` — so the escape hatch is visible in the step text.
|
|
23
|
+
* 2. AN UNMEASURED VITAL FAILS. LCP does not fire on a page with no contentful paint, and WebKit
|
|
24
|
+
* does not implement it at all; paint timings and the navigation entry can be missing too.
|
|
25
|
+
* Recording those as `0` makes every budget assertion pass — a whole perf module
|
|
26
|
+
* goes green while measuring nothing. So a vital that did not fire is recorded as `null`
|
|
27
|
+
* together with the reason, and asserting on it FAILS naming that reason. A real navigation
|
|
28
|
+
* cannot produce a genuine 0 ms vital (they are all offsets from navigation start), so treating
|
|
29
|
+
* "absent or 0" as "not measured" loses no honest signal.
|
|
30
|
+
*
|
|
31
|
+
* A budget that is not configured is likewise a hard `CONFIG_INVALID` naming the missing key, not
|
|
32
|
+
* a skip: silently skipping is the same vacuous green in a different costume.
|
|
33
|
+
*
|
|
34
|
+
* The two comparison operators are deliberately different and deliberately visible in the step
|
|
35
|
+
* text: `under {int} ms` is strict `<` (matching `the response time should be under {int} ms` in
|
|
36
|
+
* api.steps.ts), `within its configured budget` is `<=` — a budget is a ceiling you may sit on.
|
|
37
|
+
*/
|
|
38
|
+
/** The page vitals SDODS records. Each name is also its key in `perf.budgets`. */
|
|
39
|
+
export declare const VITAL_KEYS: readonly ["pageLoadMs", "lcpMs", "fcpMs", "ttfbMs"];
|
|
40
|
+
export type VitalKey = (typeof VITAL_KEYS)[number];
|
|
41
|
+
/** The `perf.budgets` key a sampled API latency distribution is judged against. */
|
|
42
|
+
export declare const API_P95_BUDGET_KEY = "apiP95Ms";
|
|
43
|
+
type BudgetLayer = {
|
|
44
|
+
budgets?: Record<string, number | undefined>;
|
|
45
|
+
} | undefined;
|
|
46
|
+
/** The slice of a resolved config these steps read. Nothing here writes config. */
|
|
47
|
+
export interface BudgetConfig {
|
|
48
|
+
project: {
|
|
49
|
+
perf?: BudgetLayer;
|
|
50
|
+
};
|
|
51
|
+
env: {
|
|
52
|
+
name?: string;
|
|
53
|
+
perf?: BudgetLayer;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Effective budgets, env overriding project per key.
|
|
58
|
+
*
|
|
59
|
+
* `resolveConfig` already deep-merges `env.perf` into `config.project.perf`, so on a resolved
|
|
60
|
+
* config the project side is the whole answer. Merging the env side again is idempotent and keeps
|
|
61
|
+
* this correct for a config assembled by hand. Non-numeric values are dropped rather than spread —
|
|
62
|
+
* an explicit `undefined` in the env layer would otherwise erase a real project budget and turn a
|
|
63
|
+
* configured assertion into an unconfigured one.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolvedBudgets(config: BudgetConfig): Record<string, number>;
|
|
66
|
+
/**
|
|
67
|
+
* The configured budget for one key, or a hard error naming it.
|
|
68
|
+
*
|
|
69
|
+
* PROVES the assertion that follows is gated on something real. A missing budget must never
|
|
70
|
+
* degrade to a skip or to `Infinity`: an unconfigured budget is the single failure mode that lets
|
|
71
|
+
* an entire perf suite report success while asserting nothing at all.
|
|
72
|
+
*/
|
|
73
|
+
export declare function requireBudget(config: BudgetConfig, key: string): number;
|
|
74
|
+
/** One `When I record the page vitals`. `null` in `vitals` means NOT MEASURED, never "0 ms". */
|
|
75
|
+
export interface VitalsRecording {
|
|
76
|
+
url: string;
|
|
77
|
+
vitals: Record<VitalKey, number | null>;
|
|
78
|
+
/** Why a vital is null, keyed by vital. Surfaced verbatim in the failure message. */
|
|
79
|
+
unmeasured: Partial<Record<VitalKey, string>>;
|
|
80
|
+
/** The contract shape written to `sdods/perf/<step>` and ingested into `steps.perf_json`. */
|
|
81
|
+
metrics: PerformanceMetrics;
|
|
82
|
+
}
|
|
83
|
+
export interface LatencySample {
|
|
84
|
+
status: number;
|
|
85
|
+
ms: number;
|
|
86
|
+
}
|
|
87
|
+
export interface LatencySampleSet {
|
|
88
|
+
method: string;
|
|
89
|
+
path: string;
|
|
90
|
+
samples: LatencySample[];
|
|
91
|
+
p95Ms: number;
|
|
92
|
+
}
|
|
93
|
+
interface PerfStore {
|
|
94
|
+
recordings: VitalsRecording[];
|
|
95
|
+
sampleSets: LatencySampleSet[];
|
|
96
|
+
}
|
|
97
|
+
export declare function perfStore(key: object): PerfStore;
|
|
98
|
+
/**
|
|
99
|
+
* Nearest-rank p95 — deterministic for a given set of samples, with no interpolation to argue
|
|
100
|
+
* about. Below 20 samples the nearest rank IS the maximum; the sample count travels in the
|
|
101
|
+
* attachment and in every failure message so a reader can see when "p95" means "slowest of five".
|
|
102
|
+
*
|
|
103
|
+
* Throws rather than returning 0 on an empty sample: a p95 of 0 slides under every budget.
|
|
104
|
+
*/
|
|
105
|
+
export declare function p95Of(msValues: readonly number[]): number;
|
|
106
|
+
/** What the page hands back. Every timing is `number | null`; null always means "did not fire". */
|
|
107
|
+
export interface RawVitals {
|
|
108
|
+
hasNavigationEntry: boolean;
|
|
109
|
+
navigationStartEpochMs: number | null;
|
|
110
|
+
pageLoadMs: number | null;
|
|
111
|
+
ttfbMs: number | null;
|
|
112
|
+
fcpMs: number | null;
|
|
113
|
+
lcpMs: number | null;
|
|
114
|
+
domContentLoadedMs: number | null;
|
|
115
|
+
totalResources: number;
|
|
116
|
+
totalResourceSizeKB: number;
|
|
117
|
+
lcpSupported: boolean;
|
|
118
|
+
fcpSupported: boolean;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Runs inside the page. Serialised by Playwright, so it closes over nothing but its argument — the
|
|
122
|
+
* type aliases above are erased at compile time and are safe to reference; the constants are not,
|
|
123
|
+
* which is why the settle window is passed in.
|
|
124
|
+
*/
|
|
125
|
+
export declare function collectRawVitals(settleMs: number): Promise<RawVitals>;
|
|
126
|
+
/**
|
|
127
|
+
* Turns the page's answer into a recording, capturing WHY a vital is null while the browser's
|
|
128
|
+
* capabilities are still in hand. By the time a `Then` step fails, "lcpMs was not measured" on its
|
|
129
|
+
* own sends the reader hunting for a bug in a page that is merely running in a browser that does
|
|
130
|
+
* not implement LCP (WebKit). Support is read from `PerformanceObserver.supportedEntryTypes`, never
|
|
131
|
+
* assumed from the browser name.
|
|
132
|
+
*/
|
|
133
|
+
export declare function toRecording(raw: RawVitals, url: string, browserName?: string): VitalsRecording;
|
|
134
|
+
interface VitalsFixtures {
|
|
135
|
+
page: Page;
|
|
136
|
+
config: BudgetConfig;
|
|
137
|
+
scenario: ScenarioMeta;
|
|
138
|
+
$bddContext: {
|
|
139
|
+
stepIndex: number;
|
|
140
|
+
};
|
|
141
|
+
$testInfo: TestInfo;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* PROVES that the current navigation produced real page vitals, and pins them to this step so a
|
|
145
|
+
* later assertion has something that can actually fail. Records LCP / FCP / TTFB / load plus the
|
|
146
|
+
* resource count and weight, as the `PerformanceMetrics` contract, under `sdods/perf/<step>` — the
|
|
147
|
+
* name `attachmentNames.perf` has always defined and the DB ingest has always read into
|
|
148
|
+
* `steps.perf_json`, and which nothing had ever written.
|
|
149
|
+
*
|
|
150
|
+
* TRAP: recording before anything navigated. `performance.getEntriesByType('navigation')` is empty
|
|
151
|
+
* on a page that has not loaded a document, so every vital comes back null. That is step misuse
|
|
152
|
+
* rather than a slow application, so it fails here as `RUN_FAILED` naming the fix, instead of as
|
|
153
|
+
* four confusing assertion failures later.
|
|
154
|
+
*/
|
|
155
|
+
export declare const recordPageVitals: ({ page, config, scenario, $bddContext, $testInfo, }: VitalsFixtures) => Promise<void>;
|
|
156
|
+
interface AssertFixtures {
|
|
157
|
+
scenario: ScenarioMeta;
|
|
158
|
+
config: BudgetConfig;
|
|
159
|
+
apiContext: ApiContext;
|
|
160
|
+
env: EnvConfig;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* PROVES a recorded vital sits inside the ceiling THIS environment declares. The budget is read
|
|
164
|
+
* from `perf.budgets` and is never a step argument: a threshold written into a feature file ships
|
|
165
|
+
* unchanged to every environment, which is exactly what a per-environment budget exists to prevent.
|
|
166
|
+
*
|
|
167
|
+
* Ordered so the cheapest fix surfaces first: forgot the recording → typo'd the vital name →
|
|
168
|
+
* budget missing from config → vital never fired → the comparison itself.
|
|
169
|
+
*/
|
|
170
|
+
export declare const assertVitalWithinBudget: ({ scenario, config, apiContext, env }: AssertFixtures, metricName: string) => Promise<void>;
|
|
171
|
+
/**
|
|
172
|
+
* PROVES a recorded vital sits under a limit this scenario owns. The visible escape hatch from the
|
|
173
|
+
* configured budget, for a page whose limit the environment-wide budget cannot express. Strictly
|
|
174
|
+
* `<`, matching `the response time should be under {int} ms`.
|
|
175
|
+
*/
|
|
176
|
+
export declare const assertVitalUnderThreshold: ({ scenario, apiContext, env }: AssertFixtures, metricName: string, maxMs: number) => Promise<void>;
|
|
177
|
+
/**
|
|
178
|
+
* PROVES a second navigation is not slower than the first — the caching / warm-path claim.
|
|
179
|
+
* Compares the last two recordings of one vital, so a scenario records, acts, and records again.
|
|
180
|
+
*
|
|
181
|
+
* Carries an explicit tolerance because both sides are live measurements: the zero-tolerance form
|
|
182
|
+
* (`plus 0 ms`) is available, but it is the author's decision in the feature file rather than a
|
|
183
|
+
* hidden fudge factor here. Fails outright with fewer than two recordings, because comparing a
|
|
184
|
+
* recording with itself always passes.
|
|
185
|
+
*/
|
|
186
|
+
export declare const assertVitalNoWorseThanPrevious: ({ scenario, apiContext, env }: AssertFixtures, metricName: string, toleranceMs: number) => Promise<void>;
|
|
187
|
+
/**
|
|
188
|
+
* PROVES the budgets a perf suite depends on actually exist for the environment being run.
|
|
189
|
+
*
|
|
190
|
+
* This step guards the other steps. A missing or partial `perf.budgets` block is invisible
|
|
191
|
+
* everywhere else in a run: the release gate reads `perfBudgets: true`, the suite goes green, and
|
|
192
|
+
* nobody learns the ceiling was never set. Put this in the first scenario of a perf module and the
|
|
193
|
+
* gap fails loudly, once, naming the key.
|
|
194
|
+
*/
|
|
195
|
+
export declare const assertBudgetsConfigured: ({ config, apiContext, env }: AssertFixtures, names: string) => Promise<void>;
|
|
196
|
+
interface SampleFixtures {
|
|
197
|
+
api: ApiClient;
|
|
198
|
+
apiContext: ApiContext;
|
|
199
|
+
env: EnvConfig;
|
|
200
|
+
scenario: ScenarioMeta;
|
|
201
|
+
$bddContext: {
|
|
202
|
+
stepIndex: number;
|
|
203
|
+
};
|
|
204
|
+
$testInfo: TestInfo;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* PROVES an endpoint's latency DISTRIBUTION rather than one lucky request. `the response time
|
|
208
|
+
* should be under {int} ms` (api.steps.ts) times a single call — the measurement most likely to be
|
|
209
|
+
* a cold start or a cache hit. A p95 needs a sample, and SDODS had no way to take one.
|
|
210
|
+
*/
|
|
211
|
+
export declare const sampleLatency: ({ api, apiContext, env, scenario, $bddContext, $testInfo }: SampleFixtures, method: HttpMethod, path: string, times: number) => Promise<void>;
|
|
212
|
+
/** PROVES the same for a write path, where the latency that matters belongs to a real payload. */
|
|
213
|
+
export declare const sampleLatencyWithBody: ({ api, apiContext, env, scenario, $bddContext, $testInfo }: SampleFixtures, method: HttpMethod, path: string, times: number, body: string) => Promise<void>;
|
|
214
|
+
/**
|
|
215
|
+
* PROVES the sampled p95 sits inside the environment's `apiP95Ms` ceiling. The budget key is fixed
|
|
216
|
+
* and read from config — nothing to mistype in the feature file, nothing to drift.
|
|
217
|
+
*
|
|
218
|
+
* Says nothing about whether the responses were correct: a p95 over twenty fast 500s passes this,
|
|
219
|
+
* and should. Pair it with `every latency sample should have returned status 200`, which is the
|
|
220
|
+
* step that proves the server was doing the work being timed.
|
|
221
|
+
*/
|
|
222
|
+
export declare const assertP95WithinBudget: ({ scenario, config, }: {
|
|
223
|
+
scenario: ScenarioMeta;
|
|
224
|
+
config: BudgetConfig;
|
|
225
|
+
}) => Promise<void>;
|
|
226
|
+
/** PROVES the sampled p95 sits under a limit this scenario owns. Strictly `<`, as with the vitals. */
|
|
227
|
+
export declare const assertP95UnderThreshold: ({ scenario }: {
|
|
228
|
+
scenario: ScenarioMeta;
|
|
229
|
+
}, maxMs: number) => Promise<void>;
|
|
230
|
+
/**
|
|
231
|
+
* PROVES that whatever changed between two samples did not make the endpoint slower — a warmed
|
|
232
|
+
* cache, a new index, a refusal being cheaper to serve than a response. Compares the last two
|
|
233
|
+
* sample sets in this scenario; fails outright with only one, because comparing a sample with
|
|
234
|
+
* itself always passes.
|
|
235
|
+
*/
|
|
236
|
+
export declare const assertP95NoWorseThanPrevious: ({ scenario }: {
|
|
237
|
+
scenario: ScenarioMeta;
|
|
238
|
+
}, toleranceMs: number) => Promise<void>;
|
|
239
|
+
/**
|
|
240
|
+
* PROVES the timed responses were the ones the scenario meant to time. Without it a latency budget
|
|
241
|
+
* is happily met by an endpoint that 500s in 3 ms, or that 401s because the sample lost the
|
|
242
|
+
* scenario's auth. Lists the offending statuses, because "not all 200" sends nobody anywhere.
|
|
243
|
+
*/
|
|
244
|
+
export declare const assertEverySampleStatus: ({ scenario }: {
|
|
245
|
+
scenario: ScenarioMeta;
|
|
246
|
+
}, status: number) => Promise<void>;
|
|
247
|
+
export {};
|
|
248
|
+
//# sourceMappingURL=perf.steps.d.ts.map
|