@loadstrike/loadstrike-sdk 1.0.26701 → 1.0.27101

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.
@@ -69,6 +69,7 @@ export const CrossPlatformTrackingConfiguration = {
69
69
  return this.forDuration(configuration, durationSeconds, cancellationSignal);
70
70
  }
71
71
  };
72
+ const TRAFFIC_MIX_FEATURE = "workload.traffic_mix";
72
73
  export class LoadStrikePluginDataTable {
73
74
  /**
74
75
  * Exposes the public constructor operation.
@@ -1603,6 +1604,112 @@ export class LoadStrikeContext {
1603
1604
  return this.assignState(this.values, scenarios, this.runArgs);
1604
1605
  }
1605
1606
  }
1607
+ const ACCESSIBILITY_TESTING_FEATURE = "testing.accessibility";
1608
+ const BROWSER_WEB_VITALS_FEATURE = "testing.browser_web_vitals";
1609
+ export class LoadStrikeAccessibility {
1610
+ static createScenario(name, options, check) {
1611
+ if (typeof name !== "string" || !name.trim()) {
1612
+ throw new Error("Scenario name must be provided.");
1613
+ }
1614
+ validateAbsoluteUrl(options?.url, "Accessibility check URL");
1615
+ if (typeof check !== "function") {
1616
+ throw new TypeError("Accessibility check callback must be provided.");
1617
+ }
1618
+ return LoadStrikeScenario
1619
+ .create(name, async (scenarioContext) => {
1620
+ const result = await check({ options, scenarioContext });
1621
+ const failure = evaluateAccessibilityResult(options, result);
1622
+ return failure
1623
+ ? LoadStrikeResponse.fail("accessibility", failure)
1624
+ : LoadStrikeResponse.ok("200", 0, `Accessibility check passed with ${(result.violations ?? []).length} violation(s).`);
1625
+ })
1626
+ .withoutWarmUp()
1627
+ .__loadStrikeWithInternalLicenseFeatures(ACCESSIBILITY_TESTING_FEATURE);
1628
+ }
1629
+ static CreateScenario(name, options, check) {
1630
+ return LoadStrikeAccessibility.createScenario(name, options, check);
1631
+ }
1632
+ }
1633
+ export class LoadStrikeBrowserWebVitals {
1634
+ static createScenario(name, options, measure) {
1635
+ if (typeof name !== "string" || !name.trim()) {
1636
+ throw new Error("Scenario name must be provided.");
1637
+ }
1638
+ validateAbsoluteUrl(options?.url, "Browser Web Vitals URL");
1639
+ if (typeof measure !== "function") {
1640
+ throw new TypeError("Browser Web Vitals measurement callback must be provided.");
1641
+ }
1642
+ return LoadStrikeScenario
1643
+ .create(name, async (scenarioContext) => {
1644
+ const result = await measure({ options, scenarioContext });
1645
+ const failure = evaluateWebVitalsResult(options, result);
1646
+ return failure
1647
+ ? LoadStrikeResponse.fail("web_vitals", failure)
1648
+ : LoadStrikeResponse.ok("200", 0, "Browser Web Vitals check passed.");
1649
+ })
1650
+ .withoutWarmUp()
1651
+ .__loadStrikeWithInternalLicenseFeatures(BROWSER_WEB_VITALS_FEATURE);
1652
+ }
1653
+ static CreateScenario(name, options, measure) {
1654
+ return LoadStrikeBrowserWebVitals.createScenario(name, options, measure);
1655
+ }
1656
+ }
1657
+ function validateAbsoluteUrl(value, label) {
1658
+ if (typeof value !== "string" || !value.trim()) {
1659
+ throw new Error(`${label} must be provided.`);
1660
+ }
1661
+ try {
1662
+ const parsed = new URL(value);
1663
+ if (!parsed.protocol || !parsed.host) {
1664
+ throw new Error("invalid");
1665
+ }
1666
+ }
1667
+ catch {
1668
+ throw new Error(`${label} must be an absolute URL.`);
1669
+ }
1670
+ }
1671
+ function evaluateAccessibilityResult(options, result) {
1672
+ if (!result) {
1673
+ return "Accessibility check did not return a result.";
1674
+ }
1675
+ const violations = Array.isArray(result.violations) ? result.violations : [];
1676
+ const maxViolations = Math.max(0, Number(options.maxViolations ?? 0));
1677
+ const critical = countAccessibilityImpact(violations, "critical");
1678
+ const serious = countAccessibilityImpact(violations, "serious");
1679
+ const maxCritical = Math.max(0, Number(options.maxCriticalViolations ?? 0));
1680
+ const maxSerious = Math.max(0, Number(options.maxSeriousViolations ?? 0));
1681
+ if (violations.length > maxViolations) {
1682
+ return `Accessibility violations ${violations.length} exceeded limit ${maxViolations}.`;
1683
+ }
1684
+ if (critical > maxCritical) {
1685
+ return `Critical accessibility violations ${critical} exceeded limit ${maxCritical}.`;
1686
+ }
1687
+ if (serious > maxSerious) {
1688
+ return `Serious accessibility violations ${serious} exceeded limit ${maxSerious}.`;
1689
+ }
1690
+ return "";
1691
+ }
1692
+ function countAccessibilityImpact(violations, impact) {
1693
+ return violations.filter((violation) => String(violation.impact ?? "").toLowerCase() === impact).length;
1694
+ }
1695
+ function evaluateWebVitalsResult(options, result) {
1696
+ if (!result) {
1697
+ return "Browser Web Vitals check did not return a result.";
1698
+ }
1699
+ return firstWebVitalViolation(["LCP", result.largestContentfulPaintMs, options.maxLargestContentfulPaintMs, "ms"], ["INP", result.interactionToNextPaintMs, options.maxInteractionToNextPaintMs, "ms"], ["CLS", result.cumulativeLayoutShift, options.maxCumulativeLayoutShift, ""], ["FCP", result.firstContentfulPaintMs, options.maxFirstContentfulPaintMs, "ms"], ["TTFB", result.timeToFirstByteMs, options.maxTimeToFirstByteMs, "ms"]);
1700
+ }
1701
+ function firstWebVitalViolation(...values) {
1702
+ for (const [name, actualRaw, limitRaw, unit] of values) {
1703
+ const actual = Number(actualRaw);
1704
+ const limit = Number(limitRaw);
1705
+ if (!Number.isFinite(actual) || !Number.isFinite(limit) || actual <= limit) {
1706
+ continue;
1707
+ }
1708
+ const suffix = unit ? ` ${unit}` : "";
1709
+ return `${name} ${actual}${suffix} exceeded limit ${limit}${suffix}.`;
1710
+ }
1711
+ return "";
1712
+ }
1606
1713
  export class LoadStrikeScenario {
1607
1714
  constructor(name, runHandler, initHandler, cleanHandler, loadSimulations, thresholds, trackingConfiguration, maxFailCount, withoutWarmUpValue, warmUpDurationSeconds, weight, restartIterationOnFail, internalLicenseFeatures = []) {
1608
1715
  this.name = name;
@@ -1948,6 +2055,103 @@ export class LoadStrikeScenario {
1948
2055
  return this.withWeight(weight);
1949
2056
  }
1950
2057
  }
2058
+ export class LoadStrikeScenarioShare {
2059
+ constructor(scenario, weight) {
2060
+ this.scenario = scenario;
2061
+ this.weight = weight;
2062
+ }
2063
+ /**
2064
+ * Creates a weighted scenario lane for a traffic mix.
2065
+ * Use this when a total workload should be distributed across several scenarios.
2066
+ */
2067
+ static create(scenario, weight) {
2068
+ if (!(scenario instanceof LoadStrikeScenario)) {
2069
+ throw new TypeError("Scenario must be provided.");
2070
+ }
2071
+ const normalizedWeight = Math.trunc(weight);
2072
+ if (!Number.isFinite(weight) || normalizedWeight <= 0) {
2073
+ throw new RangeError("Scenario share weight should be greater than zero.");
2074
+ }
2075
+ return new LoadStrikeScenarioShare(scenario, normalizedWeight);
2076
+ }
2077
+ /**
2078
+ * Creates a weighted scenario lane for a traffic mix.
2079
+ * Use this when a total workload should be distributed across several scenarios.
2080
+ */
2081
+ static Create(scenario, weight) {
2082
+ return LoadStrikeScenarioShare.create(scenario, weight);
2083
+ }
2084
+ }
2085
+ export class LoadStrikeTrafficMix {
2086
+ constructor(name, totalLoad = [], scenarioMix = []) {
2087
+ this.name = name;
2088
+ this.totalLoad = totalLoad.map((simulation) => attachLoadSimulationProjection({ ...simulation }));
2089
+ this.scenarioMix = [...scenarioMix];
2090
+ }
2091
+ /**
2092
+ * Creates a traffic mix definition.
2093
+ * Use this when one total load profile should be split across multiple scenario lanes.
2094
+ */
2095
+ static create(name) {
2096
+ return new LoadStrikeTrafficMix(requireNonEmpty(name, "Traffic mix name must be provided."));
2097
+ }
2098
+ /**
2099
+ * Creates a traffic mix definition.
2100
+ * Use this when one total load profile should be split across multiple scenario lanes.
2101
+ */
2102
+ static Create(name) {
2103
+ return LoadStrikeTrafficMix.create(name);
2104
+ }
2105
+ /**
2106
+ * Sets the total workload shape for the mix.
2107
+ * Use this when the same high-level load profile should be distributed by scenario weights.
2108
+ */
2109
+ withTotalLoad(...totalLoad) {
2110
+ if (!totalLoad.length) {
2111
+ throw new Error("At least one total load simulation should be provided.");
2112
+ }
2113
+ return new LoadStrikeTrafficMix(this.name, totalLoad, this.scenarioMix);
2114
+ }
2115
+ /**
2116
+ * Sets the total workload shape for the mix.
2117
+ * Use this when the same high-level load profile should be distributed by scenario weights.
2118
+ */
2119
+ WithTotalLoad(...totalLoad) {
2120
+ return this.withTotalLoad(...totalLoad);
2121
+ }
2122
+ /**
2123
+ * Sets the weighted scenario lanes for the mix.
2124
+ * Use this when each operation should receive a fixed proportion of the total workload.
2125
+ */
2126
+ withScenarioMix(...scenarioMix) {
2127
+ if (!scenarioMix.length) {
2128
+ throw new Error("At least one scenario share should be provided.");
2129
+ }
2130
+ if (scenarioMix.some((share) => !(share instanceof LoadStrikeScenarioShare))) {
2131
+ throw new TypeError("Scenario share collection cannot contain null values.");
2132
+ }
2133
+ return new LoadStrikeTrafficMix(this.name, this.totalLoad, scenarioMix);
2134
+ }
2135
+ /**
2136
+ * Sets the weighted scenario lanes for the mix.
2137
+ * Use this when each operation should receive a fixed proportion of the total workload.
2138
+ */
2139
+ WithScenarioMix(...scenarioMix) {
2140
+ return this.withScenarioMix(...scenarioMix);
2141
+ }
2142
+ expandScenarios() {
2143
+ return expandTrafficMixScenarios(this);
2144
+ }
2145
+ ExpandScenarios() {
2146
+ return this.expandScenarios();
2147
+ }
2148
+ __loadStrikeTotalLoad() {
2149
+ return this.totalLoad.map((simulation) => attachLoadSimulationProjection({ ...simulation }));
2150
+ }
2151
+ __loadStrikeScenarioMix() {
2152
+ return [...this.scenarioMix];
2153
+ }
2154
+ }
1951
2155
  export class LoadStrikeRunner {
1952
2156
  constructor(scenarios, options, contextConfigurators = []) {
1953
2157
  this.scenarios = scenarios;
@@ -1983,6 +2187,20 @@ export class LoadStrikeRunner {
1983
2187
  static RegisterScenarios(...scenarios) {
1984
2188
  return LoadStrikeRunner.registerScenarios(...scenarios);
1985
2189
  }
2190
+ /**
2191
+ * Registers a traffic mix on a fresh runnable context.
2192
+ * Use this when one total load profile should be split across weighted scenario lanes.
2193
+ */
2194
+ static registerTrafficMix(trafficMix) {
2195
+ return LoadStrikeRunner.registerScenarios(...expandTrafficMixScenarios(trafficMix));
2196
+ }
2197
+ /**
2198
+ * Registers a traffic mix on a fresh runnable context.
2199
+ * Use this when one total load profile should be split across weighted scenario lanes.
2200
+ */
2201
+ static RegisterTrafficMix(trafficMix) {
2202
+ return LoadStrikeRunner.registerTrafficMix(trafficMix);
2203
+ }
1986
2204
  /**
1987
2205
  * Toggles realtime console metric output.
1988
2206
  * Use this when a local run or CI log should stream live throughput and latency updates.
@@ -2237,6 +2455,21 @@ export class LoadStrikeRunner {
2237
2455
  AddScenarios(...scenarios) {
2238
2456
  return this.addScenarios(...scenarios);
2239
2457
  }
2458
+ /**
2459
+ * Adds a traffic mix to the current runner.
2460
+ * Use this when one total load profile should be split across weighted scenario lanes.
2461
+ */
2462
+ addTrafficMix(trafficMix) {
2463
+ this.scenarios = [...this.scenarios, ...expandTrafficMixScenarios(trafficMix)];
2464
+ return this;
2465
+ }
2466
+ /**
2467
+ * Adds a traffic mix to the current runner.
2468
+ * Use this when one total load profile should be split across weighted scenario lanes.
2469
+ */
2470
+ AddTrafficMix(trafficMix) {
2471
+ return this.addTrafficMix(trafficMix);
2472
+ }
2240
2473
  /**
2241
2474
  * Applies a grouped configuration change to the current builder or context.
2242
2475
  * Use this when several related settings should be supplied in one step.
@@ -4388,6 +4621,111 @@ function attachLoadSimulationProjection(simulation) {
4388
4621
  });
4389
4622
  return simulation;
4390
4623
  }
4624
+ function expandTrafficMixScenarios(trafficMix) {
4625
+ if (!(trafficMix instanceof LoadStrikeTrafficMix)) {
4626
+ throw new TypeError("Traffic mix must be provided.");
4627
+ }
4628
+ const totalLoad = trafficMix.__loadStrikeTotalLoad();
4629
+ if (!totalLoad.length) {
4630
+ throw new Error("Traffic mix total load must be configured before registration.");
4631
+ }
4632
+ const scenarioMix = trafficMix.__loadStrikeScenarioMix();
4633
+ if (!scenarioMix.length) {
4634
+ throw new Error("Traffic mix scenario shares must be configured before registration.");
4635
+ }
4636
+ const weights = scenarioMix.map((share) => share.weight);
4637
+ return scenarioMix.map((share, index) => {
4638
+ const splitSimulations = totalLoad
4639
+ .map((simulation) => splitTrafficSimulation(simulation, weights, index))
4640
+ .filter((simulation) => simulation != null);
4641
+ const scenario = !splitSimulations.length
4642
+ ? share.scenario.withLoadSimulations(LoadStrikeSimulation.pause(0))
4643
+ : share.scenario.withLoadSimulations(...splitSimulations);
4644
+ return scenario.__loadStrikeWithInternalLicenseFeatures(TRAFFIC_MIX_FEATURE);
4645
+ });
4646
+ }
4647
+ function splitTrafficSimulation(simulation, weights, index) {
4648
+ const kind = String(simulation.Kind ?? "");
4649
+ const duringSeconds = readFiniteSimulationNumber(simulation, "DuringSeconds");
4650
+ const intervalSeconds = readFiniteSimulationNumber(simulation, "IntervalSeconds");
4651
+ if (kind === "Inject") {
4652
+ const rate = splitTrafficValue(readFiniteSimulationNumber(simulation, "Rate"), weights)[index];
4653
+ return rate > 0 ? LoadStrikeSimulation.inject(rate, intervalSeconds, duringSeconds) : null;
4654
+ }
4655
+ if (kind === "RampingInject") {
4656
+ const rate = splitTrafficValue(readFiniteSimulationNumber(simulation, "Rate"), weights)[index];
4657
+ return rate > 0 ? LoadStrikeSimulation.rampingInject(rate, intervalSeconds, duringSeconds) : null;
4658
+ }
4659
+ if (kind === "InjectRandom") {
4660
+ const minRate = splitTrafficValue(readFiniteSimulationNumber(simulation, "MinRate"), weights)[index];
4661
+ const maxRate = splitTrafficValue(readFiniteSimulationNumber(simulation, "MaxRate"), weights)[index];
4662
+ if (maxRate <= 0) {
4663
+ return null;
4664
+ }
4665
+ return LoadStrikeSimulation.injectRandom(Math.min(minRate, maxRate), maxRate, intervalSeconds, duringSeconds);
4666
+ }
4667
+ if (kind === "IterationsForInject") {
4668
+ const rate = splitTrafficValue(readFiniteSimulationNumber(simulation, "Rate"), weights)[index];
4669
+ const iterations = splitTrafficValue(readFiniteSimulationNumber(simulation, "Iterations"), weights)[index];
4670
+ return rate > 0 && iterations > 0
4671
+ ? LoadStrikeSimulation.iterationsForInject(rate, intervalSeconds, iterations)
4672
+ : null;
4673
+ }
4674
+ if (kind === "IterationsForConstant") {
4675
+ const copies = splitTrafficValue(readFiniteSimulationNumber(simulation, "Copies"), weights)[index];
4676
+ const iterations = splitTrafficValue(readFiniteSimulationNumber(simulation, "Iterations"), weights)[index];
4677
+ return copies > 0 && iterations > 0
4678
+ ? LoadStrikeSimulation.iterationsForConstant(copies, iterations)
4679
+ : null;
4680
+ }
4681
+ if (kind === "KeepConstant") {
4682
+ const copies = splitTrafficValue(readFiniteSimulationNumber(simulation, "Copies"), weights)[index];
4683
+ return copies > 0 ? LoadStrikeSimulation.keepConstant(copies, duringSeconds) : null;
4684
+ }
4685
+ if (kind === "RampingConstant") {
4686
+ const copies = splitTrafficValue(readFiniteSimulationNumber(simulation, "Copies"), weights)[index];
4687
+ return copies > 0 ? LoadStrikeSimulation.rampingConstant(copies, duringSeconds) : null;
4688
+ }
4689
+ if (kind === "Pause") {
4690
+ return LoadStrikeSimulation.pause(duringSeconds);
4691
+ }
4692
+ return attachLoadSimulationProjection({ ...simulation });
4693
+ }
4694
+ function splitTrafficValue(totalValue, weights) {
4695
+ const normalizedTotal = Math.trunc(Number.isFinite(totalValue) ? totalValue : 0);
4696
+ if (normalizedTotal <= 0) {
4697
+ return weights.map(() => 0);
4698
+ }
4699
+ const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
4700
+ if (totalWeight <= 0) {
4701
+ throw new Error("Traffic mix scenario weights must add up to more than zero.");
4702
+ }
4703
+ const exactShares = weights.map((weight, index) => {
4704
+ const exact = (normalizedTotal * weight) / totalWeight;
4705
+ return {
4706
+ index,
4707
+ value: Math.floor(exact),
4708
+ remainder: exact - Math.floor(exact)
4709
+ };
4710
+ });
4711
+ const split = exactShares.map((share) => share.value);
4712
+ let remaining = normalizedTotal - split.reduce((sum, value) => sum + value, 0);
4713
+ for (const share of [...exactShares].sort((left, right) => {
4714
+ const remainderDelta = right.remainder - left.remainder;
4715
+ return remainderDelta !== 0 ? remainderDelta : left.index - right.index;
4716
+ })) {
4717
+ if (remaining <= 0) {
4718
+ break;
4719
+ }
4720
+ split[share.index] += 1;
4721
+ remaining -= 1;
4722
+ }
4723
+ return split;
4724
+ }
4725
+ function readFiniteSimulationNumber(simulation, key) {
4726
+ const value = Number(simulation[key]);
4727
+ return Number.isFinite(value) ? value : 0;
4728
+ }
4391
4729
  function attachScenarioStatsAliases(scenario) {
4392
4730
  const normalized = normalizeScenarioStatsValue(scenario);
4393
4731
  normalized.ok = attachMeasurementStatsAliases(normalized.ok);
@@ -8346,6 +8684,7 @@ export const __loadstrikeTestExports = {
8346
8684
  detailedToNodeStats,
8347
8685
  evaluateThresholdsForScenarios,
8348
8686
  executeTrackedScenarioInvocation,
8687
+ expandTrafficMixScenarios,
8349
8688
  extractContextOverridesFromArgs,
8350
8689
  extractContextOverridesFromConfig,
8351
8690
  findCaseInsensitiveKey,