@featurevisor/catalog 3.0.1 → 3.1.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/assets/index-DB7fEbI6.js +25 -0
- package/dist/assets/index-DgA0Oqrg.css +2 -0
- package/dist/index.html +2 -2
- package/lib/entityTypes.d.ts +1 -0
- package/lib/entityTypes.js +7 -0
- package/lib/entityTypes.js.map +1 -1
- package/lib/node/index.js +22 -6
- package/lib/node/index.js.map +1 -1
- package/lib/types.d.ts +2 -1
- package/package.json +2 -2
- package/src/App.tsx +2 -0
- package/src/api.spec.ts +14 -0
- package/src/api.ts +2 -2
- package/src/components/tests.spec.ts +134 -0
- package/src/components/tests.tsx +301 -0
- package/src/components/ui.tsx +11 -11
- package/src/entityTypes.spec.ts +13 -0
- package/src/entityTypes.ts +7 -0
- package/src/node/index.spec.ts +147 -3
- package/src/node/index.ts +24 -6
- package/src/pages/EntityDetailPage.tsx +140 -0
- package/src/pages/ListPage.tsx +15 -21
- package/src/testModel.ts +123 -0
- package/src/types.ts +2 -1
- package/dist/assets/index-BVAfG1yl.js +0 -25
- package/dist/assets/index-BrUFr8px.css +0 -2
package/src/node/index.ts
CHANGED
|
@@ -242,8 +242,18 @@ class CatalogProgressReporter {
|
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
function
|
|
246
|
-
return
|
|
245
|
+
function encodeKeyPath(key: string) {
|
|
246
|
+
return key
|
|
247
|
+
.split("/")
|
|
248
|
+
.map((segment) => encodeURIComponent(segment))
|
|
249
|
+
.join(path.sep);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function encodeKeyUrlPath(key: string) {
|
|
253
|
+
return key
|
|
254
|
+
.split("/")
|
|
255
|
+
.map((segment) => encodeURIComponent(segment))
|
|
256
|
+
.join("/");
|
|
247
257
|
}
|
|
248
258
|
|
|
249
259
|
function toPosixPath(value: string) {
|
|
@@ -448,7 +458,7 @@ function getEntitySummary(
|
|
|
448
458
|
promotable: entity.promotable,
|
|
449
459
|
...extra,
|
|
450
460
|
lastModified: getLastModified(historyIndex, type, key, set),
|
|
451
|
-
href: `entities/${type}/${
|
|
461
|
+
href: `entities/${type}/${encodeKeyUrlPath(key)}.json`,
|
|
452
462
|
};
|
|
453
463
|
}
|
|
454
464
|
|
|
@@ -1193,6 +1203,13 @@ async function buildSetCatalog(
|
|
|
1193
1203
|
);
|
|
1194
1204
|
const lastModified = getLastModified(context.historyIndex, plan.type, key, set);
|
|
1195
1205
|
const entityRelationships = getEntityRelationships(plan.type, key, relationships);
|
|
1206
|
+
const entityTests =
|
|
1207
|
+
plan.type === "feature" || plan.type === "segment"
|
|
1208
|
+
? (entityRelationships.tests || []).map((testKey) => ({
|
|
1209
|
+
...maps.test[testKey],
|
|
1210
|
+
key: maps.test[testKey].key || testKey,
|
|
1211
|
+
}))
|
|
1212
|
+
: undefined;
|
|
1196
1213
|
const detail: EntityDetail = {
|
|
1197
1214
|
type: plan.type,
|
|
1198
1215
|
key,
|
|
@@ -1201,25 +1218,26 @@ async function buildSetCatalog(
|
|
|
1201
1218
|
editLinks: getEditorLinks(context.devEditors, sourceFileInfo),
|
|
1202
1219
|
lastModified,
|
|
1203
1220
|
relationships: entityRelationships,
|
|
1221
|
+
tests: entityTests?.length ? entityTests : undefined,
|
|
1204
1222
|
environments: projectConfig.environments,
|
|
1205
1223
|
historyPath: `${path.posix.join(
|
|
1206
1224
|
"data",
|
|
1207
1225
|
outputRelativeDirectory.split(path.sep).join(path.posix.sep),
|
|
1208
1226
|
"entities",
|
|
1209
1227
|
plan.type,
|
|
1210
|
-
|
|
1228
|
+
encodeKeyUrlPath(key),
|
|
1211
1229
|
"history",
|
|
1212
1230
|
)}`,
|
|
1213
1231
|
};
|
|
1214
1232
|
|
|
1215
1233
|
await context.writer.write(
|
|
1216
|
-
path.join(outputDirectoryPath, "entities", plan.type, `${
|
|
1234
|
+
path.join(outputDirectoryPath, "entities", plan.type, `${encodeKeyPath(key)}.json`),
|
|
1217
1235
|
detail,
|
|
1218
1236
|
);
|
|
1219
1237
|
|
|
1220
1238
|
await writeHistoryPages(
|
|
1221
1239
|
context.writer,
|
|
1222
|
-
path.join(outputDirectoryPath, "entities", plan.type,
|
|
1240
|
+
path.join(outputDirectoryPath, "entities", plan.type, encodeKeyPath(key), "history"),
|
|
1223
1241
|
context.historyIndex.byEntity[getHistoryEntityKey(plan.type, key, set)] || [],
|
|
1224
1242
|
);
|
|
1225
1243
|
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type SchemaLike,
|
|
36
36
|
} from "../components/variables";
|
|
37
37
|
import { FeatureVariationsList } from "../components/variations";
|
|
38
|
+
import { EntityTests } from "../components/tests";
|
|
38
39
|
|
|
39
40
|
function isEntityPath(value: string | undefined): value is EntityPath {
|
|
40
41
|
return (
|
|
@@ -78,6 +79,129 @@ function RulePermalink(props: { targetId: string }) {
|
|
|
78
79
|
);
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
async function copyText(value: string) {
|
|
83
|
+
if (navigator.clipboard?.writeText) {
|
|
84
|
+
try {
|
|
85
|
+
await navigator.clipboard.writeText(value);
|
|
86
|
+
return;
|
|
87
|
+
} catch {
|
|
88
|
+
// Fall through for browsers that expose the API but deny access in the current context.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const textarea = document.createElement("textarea");
|
|
93
|
+
textarea.value = value;
|
|
94
|
+
textarea.setAttribute("readonly", "");
|
|
95
|
+
textarea.style.position = "fixed";
|
|
96
|
+
textarea.style.opacity = "0";
|
|
97
|
+
document.body.appendChild(textarea);
|
|
98
|
+
textarea.select();
|
|
99
|
+
|
|
100
|
+
let copied = false;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
copied = document.execCommand("copy");
|
|
104
|
+
} finally {
|
|
105
|
+
document.body.removeChild(textarea);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!copied) {
|
|
109
|
+
throw new Error("Could not copy entity key");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function CopyEntityKeyButton(props: { entityKey: string }) {
|
|
114
|
+
const [status, setStatus] = React.useState<"idle" | "copied" | "error">("idle");
|
|
115
|
+
const resetTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
116
|
+
|
|
117
|
+
React.useEffect(() => {
|
|
118
|
+
return () => {
|
|
119
|
+
if (resetTimer.current) {
|
|
120
|
+
clearTimeout(resetTimer.current);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
async function handleCopy() {
|
|
126
|
+
if (resetTimer.current) {
|
|
127
|
+
clearTimeout(resetTimer.current);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await copyText(props.entityKey);
|
|
132
|
+
setStatus("copied");
|
|
133
|
+
} catch {
|
|
134
|
+
setStatus("error");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
resetTimer.current = setTimeout(() => {
|
|
138
|
+
setStatus("idle");
|
|
139
|
+
resetTimer.current = null;
|
|
140
|
+
}, 2000);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const label =
|
|
144
|
+
status === "copied"
|
|
145
|
+
? `Copied ${props.entityKey}`
|
|
146
|
+
: status === "error"
|
|
147
|
+
? `Could not copy ${props.entityKey}`
|
|
148
|
+
: `Copy ${props.entityKey}`;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
aria-label={label}
|
|
155
|
+
title={label}
|
|
156
|
+
onClick={handleCopy}
|
|
157
|
+
className={[
|
|
158
|
+
"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md border text-faint outline-none transition-all",
|
|
159
|
+
"opacity-100 md:opacity-0 md:group-hover:opacity-100",
|
|
160
|
+
"hover:border-border hover:bg-elevated hover:text-text",
|
|
161
|
+
"focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-primary",
|
|
162
|
+
status === "copied"
|
|
163
|
+
? "border-green-200 bg-green-50 text-green-600 !opacity-100"
|
|
164
|
+
: status === "error"
|
|
165
|
+
? "border-red-200 bg-red-50 text-red-600 !opacity-100"
|
|
166
|
+
: "border-border/70 bg-surface",
|
|
167
|
+
].join(" ")}
|
|
168
|
+
>
|
|
169
|
+
{status === "copied" ? (
|
|
170
|
+
<svg
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
viewBox="0 0 20 20"
|
|
173
|
+
fill="none"
|
|
174
|
+
stroke="currentColor"
|
|
175
|
+
strokeLinecap="round"
|
|
176
|
+
strokeLinejoin="round"
|
|
177
|
+
strokeWidth="1.8"
|
|
178
|
+
className="h-4 w-4"
|
|
179
|
+
>
|
|
180
|
+
<path d="m4 10 3.5 3.5L16 5" />
|
|
181
|
+
</svg>
|
|
182
|
+
) : (
|
|
183
|
+
<svg
|
|
184
|
+
aria-hidden="true"
|
|
185
|
+
viewBox="0 0 20 20"
|
|
186
|
+
fill="none"
|
|
187
|
+
stroke="currentColor"
|
|
188
|
+
strokeLinecap="round"
|
|
189
|
+
strokeLinejoin="round"
|
|
190
|
+
strokeWidth="1.6"
|
|
191
|
+
className="h-4 w-4"
|
|
192
|
+
>
|
|
193
|
+
<rect x="6.5" y="6.5" width="9" height="9" rx="1.5" />
|
|
194
|
+
<path d="M13.5 6.5V5A1.5 1.5 0 0 0 12 3.5H5A1.5 1.5 0 0 0 3.5 5v7A1.5 1.5 0 0 0 5 13.5h1.5" />
|
|
195
|
+
</svg>
|
|
196
|
+
)}
|
|
197
|
+
</button>
|
|
198
|
+
<span className="sr-only" aria-live="polite">
|
|
199
|
+
{status === "copied" ? "Entity key copied" : status === "error" ? "Copy failed" : ""}
|
|
200
|
+
</span>
|
|
201
|
+
</>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
81
205
|
function useScrollToHash(dependencies: React.DependencyList) {
|
|
82
206
|
React.useEffect(() => {
|
|
83
207
|
if (typeof window === "undefined" || !window.location.hash) {
|
|
@@ -362,6 +486,7 @@ export function EntityDetailPage() {
|
|
|
362
486
|
{ to: "force", label: "Force" },
|
|
363
487
|
]
|
|
364
488
|
: []),
|
|
489
|
+
...(type === "feature" || type === "segment" ? [{ to: "tests", label: "Tests" }] : []),
|
|
365
490
|
...(type !== "test" ? [{ to: "usage", label: "Usage" }] : []),
|
|
366
491
|
{ to: "history", label: "History" },
|
|
367
492
|
];
|
|
@@ -370,6 +495,7 @@ export function EntityDetailPage() {
|
|
|
370
495
|
<div>
|
|
371
496
|
<PageHeader
|
|
372
497
|
title={detail.key}
|
|
498
|
+
titleAction={<CopyEntityKeyButton entityKey={detail.key} />}
|
|
373
499
|
description={entityLabels[type].singular}
|
|
374
500
|
actions={<EditLink sourcePath={detail.sourcePath} editLinks={detail.editLinks} />}
|
|
375
501
|
/>
|
|
@@ -948,6 +1074,20 @@ export function UsageTab() {
|
|
|
948
1074
|
);
|
|
949
1075
|
}
|
|
950
1076
|
|
|
1077
|
+
export function TestsTab() {
|
|
1078
|
+
const { detail } = useEntityDetail();
|
|
1079
|
+
|
|
1080
|
+
if (detail.type !== "feature" && detail.type !== "segment") {
|
|
1081
|
+
return <Navigate to=".." replace />;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (!detail.tests?.length) {
|
|
1085
|
+
return <EmptyState title="No test specs found" />;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return <EntityTests tests={detail.tests} />;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
951
1091
|
export function HistoryTab() {
|
|
952
1092
|
const { detail } = useEntityDetail();
|
|
953
1093
|
const { manifest } = useCatalog();
|
package/src/pages/ListPage.tsx
CHANGED
|
@@ -450,7 +450,7 @@ function VariationsIcon() {
|
|
|
450
450
|
return (
|
|
451
451
|
<span
|
|
452
452
|
aria-hidden="true"
|
|
453
|
-
className="inline-flex
|
|
453
|
+
className="inline-flex items-center justify-center font-mono text-[9px] font-semibold leading-none tracking-tight"
|
|
454
454
|
>
|
|
455
455
|
a/b
|
|
456
456
|
</span>
|
|
@@ -461,31 +461,19 @@ function VariablesIcon() {
|
|
|
461
461
|
return (
|
|
462
462
|
<span
|
|
463
463
|
aria-hidden="true"
|
|
464
|
-
className="inline-flex
|
|
464
|
+
className="inline-flex items-center justify-center font-mono text-[9px] font-semibold leading-none"
|
|
465
465
|
>
|
|
466
466
|
{"{ }"}
|
|
467
467
|
</span>
|
|
468
468
|
);
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
function TargetIcon() {
|
|
472
|
-
return (
|
|
473
|
-
<svg aria-hidden="true" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4">
|
|
474
|
-
<path
|
|
475
|
-
fillRule="evenodd"
|
|
476
|
-
d="M10 2.75a7.25 7.25 0 1 0 0 14.5 7.25 7.25 0 0 0 0-14.5ZM4.25 10a5.75 5.75 0 1 1 11.5 0 5.75 5.75 0 0 1-11.5 0ZM10 6.25a3.75 3.75 0 1 0 0 7.5 3.75 3.75 0 0 0 0-7.5ZM7.75 10a2.25 2.25 0 1 1 4.5 0 2.25 2.25 0 0 1-4.5 0Z"
|
|
477
|
-
clipRule="evenodd"
|
|
478
|
-
/>
|
|
479
|
-
</svg>
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
471
|
function sortValues(values?: string[]) {
|
|
484
472
|
return Array.from(new Set(values || [])).sort((left, right) => left.localeCompare(right));
|
|
485
473
|
}
|
|
486
474
|
|
|
487
|
-
const
|
|
488
|
-
"
|
|
475
|
+
const rowMetadataItemClassName =
|
|
476
|
+
"h-full min-w-5 items-center justify-center px-1 text-faint transition-colors hover:bg-elevated hover:text-muted";
|
|
489
477
|
|
|
490
478
|
function RowMetadataIcons(props: { entity: EntitySummary; type: CatalogEntityType }) {
|
|
491
479
|
const targets = sortValues(props.entity.targets);
|
|
@@ -498,20 +486,26 @@ function RowMetadataIcons(props: { entity: EntitySummary; type: CatalogEntityTyp
|
|
|
498
486
|
}
|
|
499
487
|
|
|
500
488
|
return (
|
|
501
|
-
<div className="flex shrink-0
|
|
489
|
+
<div className="flex h-5 shrink-0 divide-x divide-border/70 overflow-hidden rounded-md border border-border/70 bg-surface">
|
|
502
490
|
{showVariations && (
|
|
503
|
-
<HoverTooltip label="Has variations" className={
|
|
491
|
+
<HoverTooltip label="Has variations" className={rowMetadataItemClassName}>
|
|
504
492
|
<VariationsIcon />
|
|
505
493
|
</HoverTooltip>
|
|
506
494
|
)}
|
|
507
495
|
{showVariables && (
|
|
508
|
-
<HoverTooltip label="Has variables" className={
|
|
496
|
+
<HoverTooltip label="Has variables" className={rowMetadataItemClassName}>
|
|
509
497
|
<VariablesIcon />
|
|
510
498
|
</HoverTooltip>
|
|
511
499
|
)}
|
|
512
500
|
{targets.length > 0 && (
|
|
513
|
-
<HoverTooltip
|
|
514
|
-
|
|
501
|
+
<HoverTooltip
|
|
502
|
+
label={`Targets: ${targets.join(", ")}`}
|
|
503
|
+
className={`${rowMetadataItemClassName} px-1.5`}
|
|
504
|
+
>
|
|
505
|
+
<span className="inline-flex items-baseline gap-1 text-[10px] leading-none">
|
|
506
|
+
<span>Targets</span>
|
|
507
|
+
<span className="font-semibold tabular-nums text-muted">{targets.length}</span>
|
|
508
|
+
</span>
|
|
515
509
|
</HoverTooltip>
|
|
516
510
|
)}
|
|
517
511
|
</div>
|
package/src/testModel.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssertionMatrix,
|
|
3
|
+
FeatureAssertion,
|
|
4
|
+
SegmentAssertion,
|
|
5
|
+
Test,
|
|
6
|
+
} from "@featurevisor/types";
|
|
7
|
+
|
|
8
|
+
export interface ExpandedTestAssertion {
|
|
9
|
+
assertion: FeatureAssertion | SegmentAssertion;
|
|
10
|
+
assertionIndex: number;
|
|
11
|
+
caseIndex?: number;
|
|
12
|
+
caseCount?: number;
|
|
13
|
+
label: string;
|
|
14
|
+
matrixValues?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getMatrixCombinations(matrix: AssertionMatrix) {
|
|
18
|
+
const keys = Object.keys(matrix);
|
|
19
|
+
|
|
20
|
+
if (keys.length === 0) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return keys.reduce<Array<Record<string, unknown>>>(
|
|
25
|
+
(combinations, key) =>
|
|
26
|
+
combinations.flatMap((combination) =>
|
|
27
|
+
matrix[key].map((value) => ({ ...combination, [key]: value })),
|
|
28
|
+
),
|
|
29
|
+
[{}],
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function applyCombinationToValue(value: unknown, combination: Record<string, unknown>) {
|
|
34
|
+
if (typeof value !== "string") {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const placeholders = value.match(/\${{(.+?)}}/g);
|
|
39
|
+
if (!placeholders) {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (placeholders.length === 1 && value.startsWith("${{") && value.endsWith("}}")) {
|
|
44
|
+
const key = value.replace("${{", "").replace("}}", "").trim();
|
|
45
|
+
return combination[key];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return value.replace(/\${{(.+?)}}/g, (_, key) => String(combination[key.trim()]));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function applyCombinationToContext(
|
|
52
|
+
context: Record<string, unknown> | undefined,
|
|
53
|
+
combination: Record<string, unknown>,
|
|
54
|
+
) {
|
|
55
|
+
return Object.fromEntries(
|
|
56
|
+
Object.entries(context || {}).map(([key, value]) => [
|
|
57
|
+
key,
|
|
58
|
+
applyCombinationToValue(value, combination),
|
|
59
|
+
]),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function applyCombinationToAssertion(
|
|
64
|
+
test: Test,
|
|
65
|
+
assertion: FeatureAssertion | SegmentAssertion,
|
|
66
|
+
combination: Record<string, unknown>,
|
|
67
|
+
) {
|
|
68
|
+
const result = {
|
|
69
|
+
...assertion,
|
|
70
|
+
context: applyCombinationToContext(assertion.context, combination),
|
|
71
|
+
};
|
|
72
|
+
delete result.matrix;
|
|
73
|
+
|
|
74
|
+
if (result.description) {
|
|
75
|
+
result.description = String(applyCombinationToValue(result.description, combination));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if ("feature" in test) {
|
|
79
|
+
const featureResult = result as FeatureAssertion;
|
|
80
|
+
featureResult.environment = applyCombinationToValue(
|
|
81
|
+
featureResult.environment,
|
|
82
|
+
combination,
|
|
83
|
+
) as FeatureAssertion["environment"];
|
|
84
|
+
featureResult.target = applyCombinationToValue(
|
|
85
|
+
featureResult.target,
|
|
86
|
+
combination,
|
|
87
|
+
) as FeatureAssertion["target"];
|
|
88
|
+
const at = applyCombinationToValue(featureResult.at, combination);
|
|
89
|
+
featureResult.at = (
|
|
90
|
+
typeof at === "string" ? (at.includes(".") ? parseFloat(at) : parseInt(at, 10)) : at
|
|
91
|
+
) as FeatureAssertion["at"];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function expandTestAssertions(test: Test): ExpandedTestAssertion[] {
|
|
98
|
+
return test.assertions.flatMap((assertion, assertionIndex) => {
|
|
99
|
+
if (!assertion.matrix) {
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
assertion: { ...assertion },
|
|
103
|
+
assertionIndex,
|
|
104
|
+
label: String(assertionIndex + 1),
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const combinations = getMatrixCombinations(assertion.matrix);
|
|
110
|
+
return combinations.map((combination, caseIndex) => ({
|
|
111
|
+
assertion: applyCombinationToAssertion(test, assertion, combination),
|
|
112
|
+
assertionIndex,
|
|
113
|
+
caseIndex,
|
|
114
|
+
caseCount: combinations.length,
|
|
115
|
+
label: `${assertionIndex + 1}.${caseIndex + 1}`,
|
|
116
|
+
matrixValues: combination,
|
|
117
|
+
}));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getTestAssertionPermalink(testKey: string, assertionLabel: string) {
|
|
122
|
+
return `${testKey}:${assertionLabel}`;
|
|
123
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityType } from "@featurevisor/types";
|
|
1
|
+
import type { EntityType, Test } from "@featurevisor/types";
|
|
2
2
|
|
|
3
3
|
export type CatalogEntityType = EntityType;
|
|
4
4
|
export type EntityPath =
|
|
@@ -106,6 +106,7 @@ export interface EntityDetail<T = Record<string, unknown>> {
|
|
|
106
106
|
editLinks?: Partial<Record<DevEditorId, string>>;
|
|
107
107
|
lastModified?: LastModified;
|
|
108
108
|
relationships?: Record<string, string[]>;
|
|
109
|
+
tests?: Test[];
|
|
109
110
|
environments?: string[];
|
|
110
111
|
historyPath?: string;
|
|
111
112
|
}
|