Skip to content

Commit c8eefbf

Browse files
authored
fix(web-awesome): fix group sorting by order/duration; add defaultSortBy option (#676)
1 parent be1f5fa commit c8eefbf

9 files changed

Lines changed: 212 additions & 8 deletions

File tree

packages/plugin-awesome/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@ The plugin accepts the following options:
5858
| `groupBy` | Grouping tests by labels or combining labels. By default, tests are grouped using the `titlePath` provided by the test framework. | `string` | `[]`(Grouping by `titlepath`) |
5959
| `appendTitlePath`| Special marker for `groupBy`. Forces a final grouping by `titlePath` after all label-based groups. | `boolean` | `false` |
6060
| `stepTreeExpansion` | Default expansion policy for step trees in test details. | `"collapsed" \| "expand_failed_only" \| "expanded"` | `"expand_failed_only"` |
61+
| `defaultSortBy` | Default sort order for the test tree. Accepted values: `order,asc`, `order,desc`, `duration,asc`, `duration,desc`, `name,asc`, `name,desc`, `status,asc`, `status,desc`. User's manual selection is preserved in `localStorage` and takes priority over this value. | `string` | `order,asc` |
62+
63+
### Default sort order
64+
65+
Use `defaultSortBy` to change the initial sort order of the test tree:
66+
67+
```ts
68+
import { defineConfig } from "allure";
69+
70+
export default defineConfig({
71+
plugins: {
72+
awesome: {
73+
options: {
74+
defaultSortBy: "name,asc",
75+
},
76+
},
77+
},
78+
});
79+
```
80+
81+
The user's last selected sort order is always preserved in `localStorage`, so once a user changes the sort manually, their preference takes priority over the configured default.
6182

6283
### Step tree expansion
6384

packages/plugin-awesome/src/generators.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ export const generateStaticFiles = async (
627627
defaultSection = "",
628628
ci,
629629
stepTreeExpansion,
630+
defaultSortBy,
630631
} = payload;
631632
const manifest = await readTemplateManifest(payload.singleFile);
632633
const headTags: string[] = [];
@@ -690,6 +691,7 @@ export const generateStaticFiles = async (
690691
sections,
691692
defaultSection,
692693
stepTreeExpansion,
694+
defaultSortBy,
693695
};
694696

695697
try {

packages/plugin-awesome/src/model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type AwesomeOptions = {
2222
publish?: boolean;
2323
appendTitlePath?: boolean;
2424
stepTreeExpansion?: StepTreeExpansion;
25+
defaultSortBy?: string;
2526
};
2627

2728
export type TemplateManifest = Record<string, string>;

packages/web-awesome/src/components/TestResult/TrSteps/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import { TrDropdown } from "@/components/TestResult/TrDropdown";
88
import {
99
collectExpandableStepNodes,
1010
getStepTreeExpansionPolicy,
11-
hasFailedStepContext,
12-
isOpenByDefaultForPolicy,
1311
isSubtreeFirstLevelOnlyOpened,
1412
type SubtreeNode,
1513
} from "@/components/TestResult/TrSteps/stepTreeExpansion";
@@ -35,7 +33,7 @@ export type TrStepsProps = {
3533
export const TrSteps: FunctionalComponent<TrStepsProps> = ({ bodyItems, id }) => {
3634
const stepsId = typeof id === "string" ? `${id}-steps` : null;
3735
const policy = getStepTreeExpansionPolicy();
38-
const isRootOpenedByDefault = isOpenByDefaultForPolicy(policy, hasFailedStepContext(bodyItems));
36+
const isRootOpenedByDefault = true;
3937
const isOpened = stepsId !== null ? isTreeOpened(stepsId, isRootOpenedByDefault) : isRootOpenedByDefault;
4038
const expandableTreeNodes = collectExpandableStepNodes(bodyItems, policy);
4139
const hasChildren = stepsId !== null && bodyItems.length > 0;

packages/web-awesome/src/stores/treeSort.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { getParamValue, hasParam, setParams } from "@allurereport/web-commons";
1+
import { getParamValue, getReportOptions, hasParam, setParams } from "@allurereport/web-commons";
22
import { computed, effect, signal } from "@preact/signals";
33

4+
import type { AwesomeReportOptions } from "../../types.js";
5+
46
export type SortByDirection = "asc" | "desc";
57
export type SortByField = "order" | "duration" | "status" | "name";
68
export type SortBy = `${SortByField},${SortByDirection}`;
@@ -34,6 +36,10 @@ const getInitialSortBy = (): SortBy => {
3436
if (stored && validateSortBy(stored.toLowerCase())) {
3537
return stored.toLowerCase() as SortBy;
3638
}
39+
const { defaultSortBy } = getReportOptions<AwesomeReportOptions>() ?? {};
40+
if (defaultSortBy && validateSortBy(defaultSortBy.toLowerCase())) {
41+
return defaultSortBy.toLowerCase() as SortBy;
42+
}
3743
return DEFAULT_SORT_BY;
3844
};
3945

packages/web-awesome/src/utils/treeFilters.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Comparator, DefaultTreeGroup, Statistic, TestStatus, TreeLeaf } from "@allurereport/core-api";
1+
import type { Comparator, Statistic, TestStatus, TreeLeaf } from "@allurereport/core-api";
22
import {
33
alphabetically,
44
andThen,
@@ -38,16 +38,18 @@ const leafComparatorByTreeSortBy = (sortBy: SortBy = "status,asc"): Comparator<T
3838
}
3939
};
4040

41-
const groupComparatorByTreeSortBy = (sortBy: SortBy = "status,asc"): Comparator<DefaultTreeGroup> => {
42-
const typedCompareBy = compareBy<DefaultTreeGroup>;
41+
const groupComparatorByTreeSortBy = (sortBy: SortBy = "status,asc"): Comparator<AwesomeRecursiveTree> => {
42+
const typedCompareBy = compareBy<AwesomeRecursiveTree>;
4343
switch (sortBy) {
4444
case "name,desc":
4545
case "name,asc":
4646
return typedCompareBy("name", alphabetically());
4747
case "order,desc":
4848
case "order,asc":
49+
return typedCompareBy("groupOrder", ordinal());
4950
case "duration,desc":
5051
case "duration,asc":
52+
return typedCompareBy("duration", ordinal());
5153
case "status,desc":
5254
case "status,asc":
5355
return typedCompareBy("statistic", byStatistic());
@@ -76,7 +78,7 @@ export const leafComparator = (sortBy: SortBy = "status,asc"): Comparator<TreeLe
7678
return withDirection(cmp, sortBy);
7779
};
7880

79-
export const groupComparator = (sortBy: SortBy = "status,asc"): Comparator<DefaultTreeGroup> => {
81+
export const groupComparator = (sortBy: SortBy = "status,asc"): Comparator<AwesomeRecursiveTree> => {
8082
const cmp = groupComparatorByTreeSortBy(sortBy);
8183

8284
return withDirection(cmp, sortBy);
@@ -150,11 +152,20 @@ export const createRecursiveTree = (payload: {
150152
incrementStatistic(statistic, status);
151153
});
152154

155+
const duration =
156+
leaves.reduce((acc, leaf) => acc + (leaf.duration ?? 0), 0) + trees.reduce((acc, rt) => acc + rt.duration, 0);
157+
158+
const leafMinOrder = leaves.reduce((acc, leaf) => Math.min(acc, leaf.groupOrder ?? Infinity), Infinity);
159+
const treeMinOrder = trees.reduce((acc, rt) => Math.min(acc, rt.groupOrder), Infinity);
160+
const groupOrder = Math.min(leafMinOrder, treeMinOrder);
161+
153162
return {
154163
...group,
155164
statistic,
156165
leaves,
157166
trees: trees.sort(groupComparator(sortBy)),
167+
duration,
168+
groupOrder: isFinite(groupOrder) ? groupOrder : 0,
158169
};
159170
};
160171

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { epic, feature, label, story } from "allure-js-commons";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
4+
const STORAGE_KEY = "ALLURE_REPORT_SORT_BY";
5+
6+
beforeEach(async () => {
7+
await epic("coverage");
8+
await feature("sort");
9+
await story("treeSort");
10+
await label("coverage", "sort");
11+
});
12+
13+
describe("stores > treeSort", () => {
14+
beforeEach(() => {
15+
vi.resetModules();
16+
localStorage.clear();
17+
delete (globalThis as any).allureReportOptions;
18+
});
19+
20+
it("defaults to order,asc when no config and no localStorage value", async () => {
21+
const { sortBy } = await import("../../src/stores/treeSort.js");
22+
23+
expect(sortBy.value).toBe("order,asc");
24+
});
25+
26+
it("uses defaultSortBy from reportOptions when localStorage is empty", async () => {
27+
(globalThis as any).allureReportOptions = { defaultSortBy: "name,asc" };
28+
29+
const { sortBy } = await import("../../src/stores/treeSort.js");
30+
31+
expect(sortBy.value).toBe("name,asc");
32+
});
33+
34+
it("is case-insensitive for defaultSortBy", async () => {
35+
(globalThis as any).allureReportOptions = { defaultSortBy: "Name,ASC" };
36+
37+
const { sortBy } = await import("../../src/stores/treeSort.js");
38+
39+
expect(sortBy.value).toBe("name,asc");
40+
});
41+
42+
it("ignores invalid defaultSortBy and falls back to order,asc", async () => {
43+
(globalThis as any).allureReportOptions = { defaultSortBy: "invalid,value" };
44+
45+
const { sortBy } = await import("../../src/stores/treeSort.js");
46+
47+
expect(sortBy.value).toBe("order,asc");
48+
});
49+
50+
it("localStorage takes priority over defaultSortBy from reportOptions", async () => {
51+
localStorage.setItem(STORAGE_KEY, "duration,desc");
52+
(globalThis as any).allureReportOptions = { defaultSortBy: "name,asc" };
53+
54+
const { sortBy } = await import("../../src/stores/treeSort.js");
55+
56+
expect(sortBy.value).toBe("duration,desc");
57+
});
58+
});

packages/web-awesome/test/utils/treeFilters.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,110 @@ describe("utils > treeFilters", () => {
345345
);
346346
});
347347

348+
it("sorts groups by earliest leaf groupOrder when sorting by order ascending", () => {
349+
const group = {
350+
leaves: [],
351+
groups: ["groupA", "groupB", "groupC"],
352+
};
353+
const leavesById = {
354+
t1: { name: "t1", status: "passed", groupOrder: 5 } as AwesomeTestResult,
355+
t2: { name: "t2", status: "passed", groupOrder: 2 } as AwesomeTestResult,
356+
t3: { name: "t3", status: "passed", groupOrder: 8 } as AwesomeTestResult,
357+
};
358+
const groupsById = {
359+
groupA: { name: "groupA", leaves: ["t1"], groups: [] as string[] },
360+
groupB: { name: "groupB", leaves: ["t2"], groups: [] as string[] },
361+
groupC: { name: "groupC", leaves: ["t3"], groups: [] as string[] },
362+
};
363+
const result = createRecursiveTree({
364+
group: group as any,
365+
leavesById: leavesById as any,
366+
groupsById: groupsById as any,
367+
filterPredicate: alwaysTruePredicate,
368+
sortBy: "order,asc",
369+
});
370+
371+
expect(result.trees.map((t) => t.name)).toEqual(["groupB", "groupA", "groupC"]);
372+
});
373+
374+
it("sorts groups by earliest leaf groupOrder when sorting by order descending", () => {
375+
const group = {
376+
leaves: [],
377+
groups: ["groupA", "groupB", "groupC"],
378+
};
379+
const leavesById = {
380+
t1: { name: "t1", status: "passed", groupOrder: 5 } as AwesomeTestResult,
381+
t2: { name: "t2", status: "passed", groupOrder: 2 } as AwesomeTestResult,
382+
t3: { name: "t3", status: "passed", groupOrder: 8 } as AwesomeTestResult,
383+
};
384+
const groupsById = {
385+
groupA: { name: "groupA", leaves: ["t1"], groups: [] as string[] },
386+
groupB: { name: "groupB", leaves: ["t2"], groups: [] as string[] },
387+
groupC: { name: "groupC", leaves: ["t3"], groups: [] as string[] },
388+
};
389+
const result = createRecursiveTree({
390+
group: group as any,
391+
leavesById: leavesById as any,
392+
groupsById: groupsById as any,
393+
filterPredicate: alwaysTruePredicate,
394+
sortBy: "order,desc",
395+
});
396+
397+
expect(result.trees.map((t) => t.name)).toEqual(["groupC", "groupA", "groupB"]);
398+
});
399+
400+
it("sorts groups by aggregated duration when sorting by duration ascending", () => {
401+
const group = {
402+
leaves: [],
403+
groups: ["groupA", "groupB", "groupC"],
404+
};
405+
const leavesById = {
406+
t1: { name: "t1", status: "passed", duration: 3000 } as AwesomeTestResult,
407+
t2: { name: "t2", status: "passed", duration: 1000 } as AwesomeTestResult,
408+
t3: { name: "t3", status: "passed", duration: 5000 } as AwesomeTestResult,
409+
};
410+
const groupsById = {
411+
groupA: { name: "groupA", leaves: ["t1"], groups: [] as string[] },
412+
groupB: { name: "groupB", leaves: ["t2"], groups: [] as string[] },
413+
groupC: { name: "groupC", leaves: ["t3"], groups: [] as string[] },
414+
};
415+
const result = createRecursiveTree({
416+
group: group as any,
417+
leavesById: leavesById as any,
418+
groupsById: groupsById as any,
419+
filterPredicate: alwaysTruePredicate,
420+
sortBy: "duration,asc",
421+
});
422+
423+
expect(result.trees.map((t) => t.name)).toEqual(["groupB", "groupA", "groupC"]);
424+
});
425+
426+
it("sorts groups by aggregated duration when sorting by duration descending", () => {
427+
const group = {
428+
leaves: [],
429+
groups: ["groupA", "groupB", "groupC"],
430+
};
431+
const leavesById = {
432+
t1: { name: "t1", status: "passed", duration: 3000 } as AwesomeTestResult,
433+
t2: { name: "t2", status: "passed", duration: 1000 } as AwesomeTestResult,
434+
t3: { name: "t3", status: "passed", duration: 5000 } as AwesomeTestResult,
435+
};
436+
const groupsById = {
437+
groupA: { name: "groupA", leaves: ["t1"], groups: [] as string[] },
438+
groupB: { name: "groupB", leaves: ["t2"], groups: [] as string[] },
439+
groupC: { name: "groupC", leaves: ["t3"], groups: [] as string[] },
440+
};
441+
const result = createRecursiveTree({
442+
group: group as any,
443+
leavesById: leavesById as any,
444+
groupsById: groupsById as any,
445+
filterPredicate: alwaysTruePredicate,
446+
sortBy: "duration,desc",
447+
});
448+
449+
expect(result.trees.map((t) => t.name)).toEqual(["groupC", "groupA", "groupB"]);
450+
});
451+
348452
it("keeps problem-heavy groups first when sorting by status in descending order", () => {
349453
const group = {
350454
leaves: [],

packages/web-awesome/types.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type AwesomeReportOptions = {
2929
cacheKey: string;
3030
ci?: CiDescriptor;
3131
stepTreeExpansion?: StepTreeExpansion;
32+
defaultSortBy?: string;
3233
};
3334

3435
export type AwesomeFixtureResult = Omit<
@@ -122,6 +123,8 @@ export type AwesomeRecursiveTree = DefaultTreeGroup & {
122123
nodeId: string;
123124
leaves: AwesomeTreeLeaf[];
124125
trees: AwesomeRecursiveTree[];
126+
duration: number;
127+
groupOrder: number;
125128
};
126129

127130
// TODO: maybe it should call `TestCase` instead of Group

0 commit comments

Comments
 (0)