@nzila/sdk 0.1.0 → 0.1.1
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/README.md +12 -1
- package/examples/COMPLEX-USAGE.md +113 -0
- package/examples/INDEX.md +27 -0
- package/examples/README.md +94 -0
- package/examples/RUNTIME-TRACE.md +222 -0
- package/examples/TRACING-FAILURES.md +186 -0
- package/examples/backend/error-tracing.test.ts +44 -0
- package/examples/backend/order-pricing.test.ts +54 -0
- package/examples/backend/order-pricing.ts +27 -0
- package/examples/backend/runtime-trace.example.ts +31 -0
- package/examples/backend/sample-payload.json +62 -0
- package/examples/backend/sample-tracing-payload.json +90 -0
- package/examples/complex/api-services.demo.ts +72 -0
- package/examples/complex/full-platform.demo.test.ts +46 -0
- package/examples/frontend/checkout-form.test.ts +54 -0
- package/examples/frontend/checkout-form.ts +29 -0
- package/examples/frontend/error-tracing.test.ts +59 -0
- package/examples/frontend/sample-payload.json +62 -0
- package/examples/frontend/sample-tracing-payload.json +73 -0
- package/examples/frontend-webhook-client.ts +32 -0
- package/examples/jest.config.example.cjs +18 -0
- package/examples/mocharc.nzila.example.cjs +21 -0
- package/examples/react/checkout-feature.example.tsx +41 -0
- package/examples/send-run-manual.mjs +16 -0
- package/examples/shared/feature-map.ts +24 -0
- package/examples/shared/load-env.mjs +24 -0
- package/examples/shared/send-webhook.mjs +43 -0
- package/examples/vitest.config.example.ts +20 -0
- package/package.json +5 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo suite for tracing failures in Nzila.
|
|
3
|
+
* Top-level describe() names become "feature" in the webhook and dashboard.
|
|
4
|
+
*
|
|
5
|
+
* Run: npm run example:test:tracing
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
|
|
9
|
+
describe("Checkout", () => {
|
|
10
|
+
describe("Payment", () => {
|
|
11
|
+
it("should accept valid card payloads", () => {
|
|
12
|
+
expect({ status: "authorized" }).toEqual({ status: "authorized" });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should reject expired cards", () => {
|
|
16
|
+
const cardExpired = false;
|
|
17
|
+
expect(cardExpired).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("Coupons", () => {
|
|
22
|
+
it("should apply percentage discounts", () => {
|
|
23
|
+
const total = 100;
|
|
24
|
+
const discounted = total * 0.9;
|
|
25
|
+
expect(discounted).toBe(80);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("Inventory", () => {
|
|
31
|
+
it("should block checkout when stock is zero", () => {
|
|
32
|
+
const available = 0;
|
|
33
|
+
const canCheckout = available > 0;
|
|
34
|
+
expect(canCheckout).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("Auth API", () => {
|
|
39
|
+
it("should rate-limit repeated login failures", () => {
|
|
40
|
+
const attempts = 3;
|
|
41
|
+
const locked = attempts >= 5;
|
|
42
|
+
expect(locked).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { applyCoupon, lineTotal, orderTotal, reserveInventory } from "./order-pricing";
|
|
3
|
+
|
|
4
|
+
describe("Payment API", () => {
|
|
5
|
+
describe("order pricing", () => {
|
|
6
|
+
it("should compute line totals from quantity and unit price", () => {
|
|
7
|
+
expect(lineTotal({ sku: "SKU-1", qty: 2, unitCents: 1500 })).toBe(3000);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should apply SAVE10 coupon to subtotal", () => {
|
|
11
|
+
expect(applyCoupon(10_000, "save10")).toBe(9000);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should reject negative quantities", () => {
|
|
15
|
+
expect(() => lineTotal({ sku: "SKU-2", qty: -1, unitCents: 100 })).toThrow();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("checkout total", () => {
|
|
20
|
+
it("should sum multiple line items before coupon", () => {
|
|
21
|
+
const total = orderTotal(
|
|
22
|
+
[
|
|
23
|
+
{ sku: "A", qty: 1, unitCents: 2500 },
|
|
24
|
+
{ sku: "B", qty: 2, unitCents: 500 },
|
|
25
|
+
],
|
|
26
|
+
"FREESHIP",
|
|
27
|
+
);
|
|
28
|
+
expect(total).toBe(3500);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("Inventory service", () => {
|
|
34
|
+
it("should reserve stock when quantity is available", () => {
|
|
35
|
+
expect(reserveInventory("widget", 3, 10)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should fail reservation when stock is insufficient", () => {
|
|
39
|
+
expect(reserveInventory("widget", 50, 10)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("Auth API", () => {
|
|
44
|
+
it("should reject invalid password attempts", () => {
|
|
45
|
+
const attempts = 6;
|
|
46
|
+
const maxAttempts = 5;
|
|
47
|
+
expect(attempts > maxAttempts).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should lock account after 5 failures", () => {
|
|
51
|
+
const accountLocked = false;
|
|
52
|
+
expect(accountLocked).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type LineItem = { sku: string; qty: number; unitCents: number };
|
|
2
|
+
|
|
3
|
+
export function lineTotal(item: LineItem): number {
|
|
4
|
+
if (item.qty < 0 || item.unitCents < 0) {
|
|
5
|
+
throw new Error("Invalid line item");
|
|
6
|
+
}
|
|
7
|
+
return item.qty * item.unitCents;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function applyCoupon(subtotalCents: number, code: string): number {
|
|
11
|
+
if (subtotalCents < 0) throw new Error("Invalid subtotal");
|
|
12
|
+
const normalized = code.trim().toUpperCase();
|
|
13
|
+
if (normalized === "SAVE10") return Math.round(subtotalCents * 0.9);
|
|
14
|
+
if (normalized === "FREESHIP") return subtotalCents;
|
|
15
|
+
return subtotalCents;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function orderTotal(items: LineItem[], coupon?: string): number {
|
|
19
|
+
const subtotal = items.reduce((sum, item) => sum + lineTotal(item), 0);
|
|
20
|
+
return coupon ? applyCoupon(subtotal, coupon) : subtotal;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function reserveInventory(sku: string, qty: number, onHand: number): boolean {
|
|
24
|
+
if (!sku.trim()) return false;
|
|
25
|
+
if (qty <= 0) return false;
|
|
26
|
+
return onHand >= qty;
|
|
27
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend pattern: call traceNzilaFeature at the top of the handler
|
|
3
|
+
* that owns the same feature name as your Vitest suite.
|
|
4
|
+
*
|
|
5
|
+
* Env: NZILA_SIGNALS_URL, NZILA_API_KEY
|
|
6
|
+
*/
|
|
7
|
+
import { initNzilaRuntime, traceNzilaFeature } from "@nzila/sdk/runtime";
|
|
8
|
+
|
|
9
|
+
initNzilaRuntime({
|
|
10
|
+
signalsUrl:
|
|
11
|
+
process.env.NZILA_SIGNALS_URL ??
|
|
12
|
+
"http://localhost:3000/api/signals/runtime",
|
|
13
|
+
apiKey: process.env.NZILA_API_KEY ?? "",
|
|
14
|
+
locale: "en",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export async function exampleCheckoutHandler(): Promise<{ ok: boolean }> {
|
|
18
|
+
const nzila = traceNzilaFeature("Checkout");
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await nzila.runApi("inventory-reserve", async () => {
|
|
22
|
+
const res = await fetch("https://httpbin.org/status/500");
|
|
23
|
+
if (!res.ok) throw new Error(`inventory ${res.status}`);
|
|
24
|
+
return res;
|
|
25
|
+
});
|
|
26
|
+
return { ok: true };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
nzila.captureError(error, { handler: "exampleCheckoutHandler", handled: true });
|
|
29
|
+
return { ok: false };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"runId": "backend-run-placeholder",
|
|
3
|
+
"appName": "checkout-api",
|
|
4
|
+
"framework": "vitest",
|
|
5
|
+
"startedAt": "2026-05-16T14:00:00.000Z",
|
|
6
|
+
"finishedAt": "2026-05-16T14:00:04.200Z",
|
|
7
|
+
"tests": [
|
|
8
|
+
{
|
|
9
|
+
"appName": "checkout-api",
|
|
10
|
+
"framework": "vitest",
|
|
11
|
+
"startedAt": "2026-05-16T14:00:00.000Z",
|
|
12
|
+
"finishedAt": "2026-05-16T14:00:00.800Z",
|
|
13
|
+
"describePath": ["Payment API", "order pricing"],
|
|
14
|
+
"feature": "Payment API",
|
|
15
|
+
"testName": "should compute line totals from quantity and unit price",
|
|
16
|
+
"status": "passed",
|
|
17
|
+
"duration": 12,
|
|
18
|
+
"errors": []
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"appName": "checkout-api",
|
|
22
|
+
"framework": "vitest",
|
|
23
|
+
"startedAt": "2026-05-16T14:00:00.800Z",
|
|
24
|
+
"finishedAt": "2026-05-16T14:00:01.400Z",
|
|
25
|
+
"describePath": ["Payment API", "order pricing"],
|
|
26
|
+
"feature": "Payment API",
|
|
27
|
+
"testName": "should apply SAVE10 coupon to subtotal",
|
|
28
|
+
"status": "passed",
|
|
29
|
+
"duration": 8,
|
|
30
|
+
"errors": []
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"appName": "checkout-api",
|
|
34
|
+
"framework": "vitest",
|
|
35
|
+
"startedAt": "2026-05-16T14:00:01.400Z",
|
|
36
|
+
"finishedAt": "2026-05-16T14:00:02.000Z",
|
|
37
|
+
"describePath": ["Inventory service"],
|
|
38
|
+
"feature": "Inventory",
|
|
39
|
+
"testName": "should reserve stock when quantity is available",
|
|
40
|
+
"status": "passed",
|
|
41
|
+
"duration": 15,
|
|
42
|
+
"errors": []
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"appName": "checkout-api",
|
|
46
|
+
"framework": "vitest",
|
|
47
|
+
"startedAt": "2026-05-16T14:00:02.000Z",
|
|
48
|
+
"finishedAt": "2026-05-16T14:00:03.200Z",
|
|
49
|
+
"describePath": ["Auth API"],
|
|
50
|
+
"feature": "Auth API",
|
|
51
|
+
"testName": "should lock account after 5 failures",
|
|
52
|
+
"status": "failed",
|
|
53
|
+
"duration": 95,
|
|
54
|
+
"errors": [
|
|
55
|
+
{
|
|
56
|
+
"message": "Expected accountLocked to be true, received false",
|
|
57
|
+
"stack": "AssertionError: expected false to be true\n at examples/backend/order-pricing.test.ts"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"runId": "tracing-demo-manual-001",
|
|
3
|
+
"appName": "checkout-api",
|
|
4
|
+
"framework": "vitest",
|
|
5
|
+
"startedAt": "2026-05-16T16:00:00.000Z",
|
|
6
|
+
"finishedAt": "2026-05-16T16:00:05.000Z",
|
|
7
|
+
"locale": "en",
|
|
8
|
+
"tests": [
|
|
9
|
+
{
|
|
10
|
+
"appName": "checkout-api",
|
|
11
|
+
"framework": "vitest",
|
|
12
|
+
"startedAt": "2026-05-16T16:00:00.000Z",
|
|
13
|
+
"finishedAt": "2026-05-16T16:00:01.000Z",
|
|
14
|
+
"describePath": ["Checkout", "Payment"],
|
|
15
|
+
"feature": "Checkout",
|
|
16
|
+
"testName": "should accept valid card payloads",
|
|
17
|
+
"status": "passed",
|
|
18
|
+
"duration": 10,
|
|
19
|
+
"errors": []
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"appName": "checkout-api",
|
|
23
|
+
"framework": "vitest",
|
|
24
|
+
"startedAt": "2026-05-16T16:00:01.000Z",
|
|
25
|
+
"finishedAt": "2026-05-16T16:00:02.200Z",
|
|
26
|
+
"describePath": ["Checkout", "Payment"],
|
|
27
|
+
"feature": "Checkout",
|
|
28
|
+
"testName": "should reject expired cards",
|
|
29
|
+
"status": "failed",
|
|
30
|
+
"duration": 45,
|
|
31
|
+
"errors": [
|
|
32
|
+
{
|
|
33
|
+
"message": "expected false to be true — card marked valid but expiry check failed",
|
|
34
|
+
"stack": "AssertionError: expected false to be true\n at examples/backend/error-tracing.test.ts:18:26"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"appName": "checkout-api",
|
|
40
|
+
"framework": "vitest",
|
|
41
|
+
"startedAt": "2026-05-16T16:00:02.200Z",
|
|
42
|
+
"finishedAt": "2026-05-16T16:00:03.000Z",
|
|
43
|
+
"describePath": ["Checkout", "Coupons"],
|
|
44
|
+
"feature": "Checkout",
|
|
45
|
+
"testName": "should apply percentage discounts",
|
|
46
|
+
"status": "failed",
|
|
47
|
+
"duration": 30,
|
|
48
|
+
"errors": [
|
|
49
|
+
{
|
|
50
|
+
"message": "expected 90 to be 80 — coupon math mismatch",
|
|
51
|
+
"stack": "AssertionError: expected 90 to be 80\n at examples/backend/error-tracing.test.ts:26:25"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"appName": "checkout-api",
|
|
57
|
+
"framework": "vitest",
|
|
58
|
+
"startedAt": "2026-05-16T16:00:03.000Z",
|
|
59
|
+
"finishedAt": "2026-05-16T16:00:04.000Z",
|
|
60
|
+
"describePath": ["Inventory"],
|
|
61
|
+
"feature": "Inventory",
|
|
62
|
+
"testName": "should block checkout when stock is zero",
|
|
63
|
+
"status": "failed",
|
|
64
|
+
"duration": 22,
|
|
65
|
+
"errors": [
|
|
66
|
+
{
|
|
67
|
+
"message": "expected false to be true — checkout allowed with zero stock",
|
|
68
|
+
"stack": "AssertionError: expected false to be true\n at examples/backend/error-tracing.test.ts:35:24"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"appName": "checkout-api",
|
|
74
|
+
"framework": "vitest",
|
|
75
|
+
"startedAt": "2026-05-16T16:00:04.000Z",
|
|
76
|
+
"finishedAt": "2026-05-16T16:00:05.000Z",
|
|
77
|
+
"describePath": ["Auth API"],
|
|
78
|
+
"feature": "Auth API",
|
|
79
|
+
"testName": "should rate-limit repeated login failures",
|
|
80
|
+
"status": "failed",
|
|
81
|
+
"duration": 18,
|
|
82
|
+
"errors": [
|
|
83
|
+
{
|
|
84
|
+
"message": "expected false to be true — account should lock after threshold",
|
|
85
|
+
"stack": "AssertionError: expected false to be true\n at examples/backend/error-tracing.test.ts:43:19"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complex backend example: multiple features, runApi, captureError, reportFeatureError.
|
|
3
|
+
*
|
|
4
|
+
* Run with: npx tsx examples/complex/api-services.demo.ts
|
|
5
|
+
* Env: NZILA_SIGNALS_URL, NZILA_API_KEY
|
|
6
|
+
*/
|
|
7
|
+
import { config } from "dotenv";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
initNzilaRuntime,
|
|
11
|
+
reportFeatureError,
|
|
12
|
+
traceNzilaFeature,
|
|
13
|
+
} from "@nzila/sdk/runtime";
|
|
14
|
+
|
|
15
|
+
config({ path: resolve(process.cwd(), ".env.local") });
|
|
16
|
+
|
|
17
|
+
initNzilaRuntime({
|
|
18
|
+
signalsUrl:
|
|
19
|
+
process.env.NZILA_SIGNALS_URL ?? "http://localhost:3000/api/signals/runtime",
|
|
20
|
+
apiKey: process.env.NZILA_API_KEY ?? "",
|
|
21
|
+
locale: "en",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
async function fakePaymentGateway(amount: number): Promise<{ ok: boolean }> {
|
|
25
|
+
if (amount > 10_000) throw new Error("limit_exceeded");
|
|
26
|
+
return { ok: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function checkoutHandler(body: { amount: number; sku: string }) {
|
|
30
|
+
const nzila = traceNzilaFeature("Checkout");
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const inventory = await nzila.runApi("inventory.reserve", async () => {
|
|
34
|
+
if (body.sku === "OUT_OF_STOCK") throw new Error("no_stock");
|
|
35
|
+
return { reserved: true };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!inventory.reserved) {
|
|
39
|
+
const err = new Error("reservation_failed");
|
|
40
|
+
nzila.captureError(err, { sku: body.sku, handled: true });
|
|
41
|
+
return { status: 409 as const, body: { error: "out_of_stock" } };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await nzila.runApi("payment.charge", () => fakePaymentGateway(body.amount));
|
|
45
|
+
return { status: 200 as const, body: { ok: true } };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
nzila.captureApiError(error, "checkout.pipeline", { sku: body.sku });
|
|
48
|
+
return { status: 502 as const, body: { error: "checkout_failed" } };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function inventorySyncJob() {
|
|
53
|
+
const nzila = traceNzilaFeature("Inventory");
|
|
54
|
+
try {
|
|
55
|
+
await nzila.runApi("warehouse.pull", async () => {
|
|
56
|
+
throw new Error("warehouse_timeout");
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
reportFeatureError("Inventory", error, { job: "nightly-sync", handled: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
console.log("checkout 200:", await checkoutHandler({ amount: 50, sku: "SKU-1" }));
|
|
65
|
+
console.log("checkout 409:", await checkoutHandler({ amount: 50, sku: "OUT_OF_STOCK" }));
|
|
66
|
+
console.log("inventory job:");
|
|
67
|
+
await inventorySyncJob();
|
|
68
|
+
console.log("Signals queued — wait ~1s for flush.");
|
|
69
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
void main();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complex Vitest demo: multiple features, mapFeature, passes and failures.
|
|
3
|
+
* Sends one webhook when the file finishes (NzilaVitestReporter).
|
|
4
|
+
*
|
|
5
|
+
* npm run example:test:complex
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
describe("Checkout UI", () => {
|
|
10
|
+
it("shows pay button when cart has items", () => {
|
|
11
|
+
expect([{ sku: "a" }].length).toBeGreaterThan(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("validates card expiry format", () => {
|
|
15
|
+
const expiry = "13/99";
|
|
16
|
+
expect(/^\d{2}\/\d{2}$/.test(expiry)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("surfaces gateway error when charge fails", () => {
|
|
20
|
+
const gateway = { status: 502, message: "upstream unavailable" };
|
|
21
|
+
expect(gateway.status).toBe(200);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("Inventory", () => {
|
|
26
|
+
it("computes available quantity", () => {
|
|
27
|
+
expect(10 - 3).toBe(7);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("flags oversell when reserved exceeds on-hand", () => {
|
|
31
|
+
const onHand = 2;
|
|
32
|
+
const reserved = 5;
|
|
33
|
+
expect(reserved).toBeLessThanOrEqual(onHand);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("Authentication", () => {
|
|
38
|
+
it("accepts valid session token shape", () => {
|
|
39
|
+
expect("sess_abc".startsWith("sess_")).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects expired session", () => {
|
|
43
|
+
const expired = true;
|
|
44
|
+
expect(expired).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
canSubmitCheckout,
|
|
4
|
+
formatCardDisplay,
|
|
5
|
+
isValidEmail,
|
|
6
|
+
shippingLabel,
|
|
7
|
+
} from "./checkout-form";
|
|
8
|
+
|
|
9
|
+
describe("Checkout UI", () => {
|
|
10
|
+
describe("email field", () => {
|
|
11
|
+
it("should accept a valid customer email", () => {
|
|
12
|
+
expect(isValidEmail("buyer@example.com")).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should reject malformed email addresses", () => {
|
|
16
|
+
expect(isValidEmail("not-an-email")).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("payment display", () => {
|
|
21
|
+
it("should mask card number showing last four digits", () => {
|
|
22
|
+
expect(formatCardDisplay("4242")).toBe("•••• •••• •••• 4242");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should block submit when terms are not accepted", () => {
|
|
26
|
+
expect(
|
|
27
|
+
canSubmitCheckout({
|
|
28
|
+
email: "a@b.co",
|
|
29
|
+
cardLast4: "1234",
|
|
30
|
+
country: "US",
|
|
31
|
+
acceptTerms: false,
|
|
32
|
+
}),
|
|
33
|
+
).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("shipping banner", () => {
|
|
38
|
+
it("should show free shipping over threshold for US", () => {
|
|
39
|
+
expect(shippingLabel("US", 6000)).toBe("Free shipping");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("Product card component", () => {
|
|
45
|
+
it("should render sale badge when discount is active", () => {
|
|
46
|
+
const discountPct = 20;
|
|
47
|
+
expect(discountPct >= 15).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should hide add-to-cart when out of stock", () => {
|
|
51
|
+
const inStock = 0;
|
|
52
|
+
expect(inStock > 0).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type CheckoutForm = {
|
|
2
|
+
email: string;
|
|
3
|
+
cardLast4: string;
|
|
4
|
+
country: string;
|
|
5
|
+
acceptTerms: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function isValidEmail(email: string): boolean {
|
|
9
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatCardDisplay(last4: string): string {
|
|
13
|
+
const digits = last4.replace(/\D/g, "");
|
|
14
|
+
if (digits.length !== 4) return "•••• •••• •••• ••••";
|
|
15
|
+
return `•••• •••• •••• ${digits}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function canSubmitCheckout(form: CheckoutForm): boolean {
|
|
19
|
+
if (!form.acceptTerms) return false;
|
|
20
|
+
if (!isValidEmail(form.email)) return false;
|
|
21
|
+
if (form.country.length !== 2) return false;
|
|
22
|
+
return form.cardLast4.replace(/\D/g, "").length === 4;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function shippingLabel(country: string, subtotalCents: number): string {
|
|
26
|
+
if (country === "US" && subtotalCents >= 5000) return "Free shipping";
|
|
27
|
+
if (country === "US") return "Standard (3–5 days)";
|
|
28
|
+
return "International";
|
|
29
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend tracing demo — failures attached to features via mapFeature in
|
|
3
|
+
* examples/vitest.tracing.frontend.config.ts (Checkout UI → feature "Checkout").
|
|
4
|
+
*
|
|
5
|
+
* Run: npm run example:test:tracing:frontend
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
canSubmitCheckout,
|
|
10
|
+
formatCardDisplay,
|
|
11
|
+
isValidEmail,
|
|
12
|
+
shippingLabel,
|
|
13
|
+
} from "./checkout-form";
|
|
14
|
+
|
|
15
|
+
describe("Checkout UI", () => {
|
|
16
|
+
describe("email field", () => {
|
|
17
|
+
it("should accept a valid customer email", () => {
|
|
18
|
+
expect(isValidEmail("buyer@example.com")).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should reject malformed email addresses", () => {
|
|
22
|
+
expect(isValidEmail("not-an-email")).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("payment display", () => {
|
|
27
|
+
it("should mask card number showing last four digits", () => {
|
|
28
|
+
expect(formatCardDisplay("4242")).toBe("•••• •••• •••• 9999");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should block submit when terms are not accepted", () => {
|
|
32
|
+
expect(
|
|
33
|
+
canSubmitCheckout({
|
|
34
|
+
email: "a@b.co",
|
|
35
|
+
cardLast4: "1234",
|
|
36
|
+
country: "US",
|
|
37
|
+
acceptTerms: false,
|
|
38
|
+
}),
|
|
39
|
+
).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("shipping banner", () => {
|
|
44
|
+
it("should show free shipping over threshold for US", () => {
|
|
45
|
+
expect(shippingLabel("US", 6000)).toBe("Paid shipping");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("Product card component", () => {
|
|
51
|
+
it("should render sale badge when discount is active", () => {
|
|
52
|
+
expect(20 >= 15).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should hide add-to-cart when out of stock", () => {
|
|
56
|
+
const inStock = 0;
|
|
57
|
+
expect(inStock > 0).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"runId": "frontend-run-placeholder",
|
|
3
|
+
"appName": "checkout-web",
|
|
4
|
+
"framework": "vitest",
|
|
5
|
+
"startedAt": "2026-05-16T14:05:00.000Z",
|
|
6
|
+
"finishedAt": "2026-05-16T14:05:03.800Z",
|
|
7
|
+
"tests": [
|
|
8
|
+
{
|
|
9
|
+
"appName": "checkout-web",
|
|
10
|
+
"framework": "vitest",
|
|
11
|
+
"startedAt": "2026-05-16T14:05:00.000Z",
|
|
12
|
+
"finishedAt": "2026-05-16T14:05:00.600Z",
|
|
13
|
+
"describePath": ["Checkout UI", "email field"],
|
|
14
|
+
"feature": "Checkout UI",
|
|
15
|
+
"testName": "should accept a valid customer email",
|
|
16
|
+
"status": "passed",
|
|
17
|
+
"duration": 22,
|
|
18
|
+
"errors": []
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"appName": "checkout-web",
|
|
22
|
+
"framework": "vitest",
|
|
23
|
+
"startedAt": "2026-05-16T14:05:00.600Z",
|
|
24
|
+
"finishedAt": "2026-05-16T14:05:01.200Z",
|
|
25
|
+
"describePath": ["Checkout UI", "payment display"],
|
|
26
|
+
"feature": "Checkout UI",
|
|
27
|
+
"testName": "should mask card number showing last four digits",
|
|
28
|
+
"status": "passed",
|
|
29
|
+
"duration": 18,
|
|
30
|
+
"errors": []
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"appName": "checkout-web",
|
|
34
|
+
"framework": "vitest",
|
|
35
|
+
"startedAt": "2026-05-16T14:05:01.200Z",
|
|
36
|
+
"finishedAt": "2026-05-16T14:05:02.400Z",
|
|
37
|
+
"describePath": ["Checkout UI", "shipping banner"],
|
|
38
|
+
"feature": "Checkout UI",
|
|
39
|
+
"testName": "should show free shipping over threshold for US",
|
|
40
|
+
"status": "passed",
|
|
41
|
+
"duration": 11,
|
|
42
|
+
"errors": []
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"appName": "checkout-web",
|
|
46
|
+
"framework": "vitest",
|
|
47
|
+
"startedAt": "2026-05-16T14:05:02.400Z",
|
|
48
|
+
"finishedAt": "2026-05-16T14:05:03.800Z",
|
|
49
|
+
"describePath": ["Product card component"],
|
|
50
|
+
"feature": "Product card",
|
|
51
|
+
"testName": "should hide add-to-cart when out of stock",
|
|
52
|
+
"status": "failed",
|
|
53
|
+
"duration": 40,
|
|
54
|
+
"errors": [
|
|
55
|
+
{
|
|
56
|
+
"message": "Expected inStock > 0 to be true, received false",
|
|
57
|
+
"stack": "AssertionError: expected false to be true\n at examples/frontend/checkout-form.test.ts"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|