Skip to content

Commit 4df7817

Browse files
committed
Share workflow job fixture across tests
Workflow diagram tests in `WorkflowDiagram.test.tsx` and `workflowDiagramGraphModel.test.ts` each defined a local `workflowJob` fixture with nearly identical defaults. That duplication made workflow metadata drift likely when fields like `deps` or `workflow_id` changed in one file but not the other. Add a shared `workflowJobFactory` in `src/test/factories/workflowJob.ts`, built on top of `jobFactory`. The factory centralizes workflow-specific metadata defaults (`deps`, `task`, `workflow_id`, `workflow_staged_at`) while still allowing per-test overrides for state and dependency scenarios. Both workflow diagram test files now use the shared factory and no longer carry their own fixture builder logic. This keeps existing behavior coverage while reducing fixture boilerplate and making future test updates easier to apply consistently.
1 parent 2952fd8 commit 4df7817

3 files changed

Lines changed: 109 additions & 110 deletions

File tree

src/components/workflow-diagram/WorkflowDiagram.test.tsx

Lines changed: 11 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,12 @@
11
import type { PropsWithChildren } from "react";
22

3-
import { JobWithKnownMetadata } from "@services/jobs";
4-
import { JobState } from "@services/types";
3+
import { workflowJobFactory } from "@test/factories/workflowJob";
54
import { act, render, screen } from "@testing-library/react";
65
import { beforeEach, describe, expect, it, vi } from "vitest";
76

87
import WorkflowDiagram from "./WorkflowDiagram";
98
import * as workflowDiagramLayout from "./workflowDiagramLayout";
109

11-
const baseDate = new Date("2025-01-01T00:00:00.000Z");
12-
13-
const workflowJob = ({
14-
deps = [],
15-
id,
16-
state = JobState.Available,
17-
task,
18-
workflowID = "wf-1",
19-
}: {
20-
deps?: string[];
21-
id: number;
22-
state?: JobState;
23-
task: string;
24-
workflowID?: string;
25-
}): JobWithKnownMetadata => ({
26-
args: {},
27-
attempt: 0,
28-
attemptedAt: undefined,
29-
attemptedBy: [],
30-
createdAt: baseDate,
31-
errors: [],
32-
finalizedAt: undefined,
33-
id: BigInt(id),
34-
kind: `job-${task}`,
35-
logs: {},
36-
maxAttempts: 1,
37-
metadata: {
38-
deps,
39-
task,
40-
workflow_id: workflowID,
41-
workflow_staged_at: baseDate.toISOString(),
42-
},
43-
priority: 1,
44-
queue: "default",
45-
scheduledAt: baseDate,
46-
state,
47-
tags: [],
48-
});
49-
5010
type MockReactFlowProps = PropsWithChildren<{
5111
edges: unknown[];
5212
nodes: unknown[];
@@ -95,9 +55,9 @@ describe("WorkflowDiagram", () => {
9555

9656
it("renders nodes and edges for a workflow", () => {
9757
const tasks = [
98-
workflowJob({ id: 1, task: "a" }),
99-
workflowJob({ deps: ["a"], id: 2, task: "b" }),
100-
workflowJob({ deps: ["a", "b"], id: 3, task: "c" }),
58+
workflowJobFactory.build({ id: 1, task: "a" }),
59+
workflowJobFactory.build({ deps: ["a"], id: 2, task: "b" }),
60+
workflowJobFactory.build({ deps: ["a", "b"], id: 3, task: "c" }),
10161
];
10262

10363
render(
@@ -115,8 +75,8 @@ describe("WorkflowDiagram", () => {
11575

11676
it("calls setSelectedJobId when a node is selected", () => {
11777
const tasks = [
118-
workflowJob({ id: 1, task: "a" }),
119-
workflowJob({ deps: ["a"], id: 2, task: "b" }),
78+
workflowJobFactory.build({ id: 1, task: "a" }),
79+
workflowJobFactory.build({ deps: ["a"], id: 2, task: "b" }),
12080
];
12181

12282
const setSelectedJobID = vi.fn();
@@ -142,8 +102,8 @@ describe("WorkflowDiagram", () => {
142102

143103
it("does not rerun layout when only selectedJobId changes", () => {
144104
const tasks = [
145-
workflowJob({ id: 1, task: "a" }),
146-
workflowJob({ deps: ["a"], id: 2, task: "b" }),
105+
workflowJobFactory.build({ id: 1, task: "a" }),
106+
workflowJobFactory.build({ deps: ["a"], id: 2, task: "b" }),
147107
];
148108
const layoutSpy = vi.spyOn(workflowDiagramLayout, "getLayoutedElements");
149109

@@ -170,8 +130,8 @@ describe("WorkflowDiagram", () => {
170130

171131
it("does not rerun layout when only theme changes", () => {
172132
const tasks = [
173-
workflowJob({ id: 1, task: "a" }),
174-
workflowJob({ deps: ["a"], id: 2, task: "b" }),
133+
workflowJobFactory.build({ id: 1, task: "a" }),
134+
workflowJobFactory.build({ deps: ["a"], id: 2, task: "b" }),
175135
];
176136
const layoutSpy = vi.spyOn(workflowDiagramLayout, "getLayoutedElements");
177137

@@ -199,7 +159,7 @@ describe("WorkflowDiagram", () => {
199159
});
200160

201161
it("renders when metadata.deps is missing on a task", () => {
202-
const malformedJob = workflowJob({ id: 1, task: "a" });
162+
const malformedJob = workflowJobFactory.build({ id: 1, task: "a" });
203163
(
204164
malformedJob.metadata as unknown as {
205165
deps?: string[];

src/components/workflow-diagram/workflowDiagramGraphModel.test.ts

Lines changed: 50 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Edge } from "@xyflow/react";
22

3-
import { JobWithKnownMetadata } from "@services/jobs";
43
import { JobState } from "@services/types";
4+
import { workflowJobFactory } from "@test/factories/workflowJob";
55
import { describe, expect, it } from "vitest";
66

77
import {
@@ -10,48 +10,11 @@ import {
1010
depStatusFromJob,
1111
} from "./workflowDiagramGraphModel";
1212

13-
const baseDate = new Date("2025-01-01T00:00:00.000Z");
14-
15-
const workflowJob = ({
16-
deps = [],
17-
id,
18-
state = JobState.Available,
19-
task,
20-
workflowID = "wf-1",
21-
}: {
22-
deps?: string[];
23-
id: number;
24-
state?: JobState;
25-
task: string;
26-
workflowID?: string;
27-
}): JobWithKnownMetadata => ({
28-
args: {},
29-
attempt: 0,
30-
attemptedAt: undefined,
31-
attemptedBy: [],
32-
createdAt: baseDate,
33-
errors: [],
34-
finalizedAt: undefined,
35-
id: BigInt(id),
36-
kind: `job-${task}`,
37-
logs: {},
38-
maxAttempts: 1,
39-
metadata: {
40-
deps,
41-
task,
42-
workflow_id: workflowID,
43-
workflow_staged_at: baseDate.toISOString(),
44-
},
45-
priority: 1,
46-
queue: "default",
47-
scheduledAt: baseDate,
48-
state,
49-
tags: [],
50-
});
51-
5213
describe("buildWorkflowGraphModel", () => {
5314
it("returns one node and zero edges for a single task", () => {
54-
const model = buildWorkflowGraphModel([workflowJob({ id: 1, task: "a" })]);
15+
const model = buildWorkflowGraphModel([
16+
workflowJobFactory.build({ id: 1, task: "a" }),
17+
]);
5518

5619
expect(model.nodes).toHaveLength(1);
5720
expect(model.edges).toHaveLength(0);
@@ -60,9 +23,13 @@ describe("buildWorkflowGraphModel", () => {
6023

6124
it("builds dependency edges in deterministic task/dep order", () => {
6225
const tasks = [
63-
workflowJob({ id: 1, task: "task-a" }),
64-
workflowJob({ deps: ["task-a"], id: 2, task: "task-b" }),
65-
workflowJob({ deps: ["task-a", "task-b"], id: 3, task: "task-c" }),
26+
workflowJobFactory.build({ id: 1, task: "task-a" }),
27+
workflowJobFactory.build({ deps: ["task-a"], id: 2, task: "task-b" }),
28+
workflowJobFactory.build({
29+
deps: ["task-a", "task-b"],
30+
id: 3,
31+
task: "task-c",
32+
}),
6633
];
6734

6835
const model = buildWorkflowGraphModel(tasks);
@@ -80,30 +47,50 @@ describe("buildWorkflowGraphModel", () => {
8047
it("maps job states to dependency statuses", () => {
8148
expect(
8249
depStatusFromJob(
83-
workflowJob({ id: 1, state: JobState.Completed, task: "a" }),
50+
workflowJobFactory.build({
51+
id: 1,
52+
state: JobState.Completed,
53+
task: "a",
54+
}),
8455
),
8556
).toBe("unblocked");
8657
expect(
8758
depStatusFromJob(
88-
workflowJob({ id: 2, state: JobState.Cancelled, task: "b" }),
59+
workflowJobFactory.build({
60+
id: 2,
61+
state: JobState.Cancelled,
62+
task: "b",
63+
}),
8964
),
9065
).toBe("failed");
9166
expect(
9267
depStatusFromJob(
93-
workflowJob({ id: 3, state: JobState.Discarded, task: "c" }),
68+
workflowJobFactory.build({
69+
id: 3,
70+
state: JobState.Discarded,
71+
task: "c",
72+
}),
9473
),
9574
).toBe("failed");
9675
expect(
9776
depStatusFromJob(
98-
workflowJob({ id: 4, state: JobState.Running, task: "d" }),
77+
workflowJobFactory.build({
78+
id: 4,
79+
state: JobState.Running,
80+
task: "d",
81+
}),
9982
),
10083
).toBe("blocked");
10184
});
10285

10386
it("drops missing dependency targets", () => {
10487
const tasks = [
105-
workflowJob({ id: 1, task: "existing" }),
106-
workflowJob({ deps: ["missing", "existing"], id: 2, task: "consumer" }),
88+
workflowJobFactory.build({ id: 1, task: "existing" }),
89+
workflowJobFactory.build({
90+
deps: ["missing", "existing"],
91+
id: 2,
92+
task: "consumer",
93+
}),
10794
];
10895

10996
const model = buildWorkflowGraphModel(tasks);
@@ -114,9 +101,9 @@ describe("buildWorkflowGraphModel", () => {
114101

115102
it("sets upstream and downstream flags on node data", () => {
116103
const tasks = [
117-
workflowJob({ id: 1, task: "a" }),
118-
workflowJob({ deps: ["a"], id: 2, task: "b" }),
119-
workflowJob({ deps: ["b"], id: 3, task: "c" }),
104+
workflowJobFactory.build({ id: 1, task: "a" }),
105+
workflowJobFactory.build({ deps: ["a"], id: 2, task: "b" }),
106+
workflowJobFactory.build({ deps: ["b"], id: 3, task: "c" }),
120107
];
121108

122109
const model = buildWorkflowGraphModel(tasks);
@@ -134,25 +121,29 @@ describe("buildWorkflowGraphModel", () => {
134121

135122
it("animates only blocked dependencies when downstream job is pending", () => {
136123
const tasks = [
137-
workflowJob({ id: 1, state: JobState.Available, task: "source-blocked" }),
138-
workflowJob({
124+
workflowJobFactory.build({
125+
id: 1,
126+
state: JobState.Available,
127+
task: "source-blocked",
128+
}),
129+
workflowJobFactory.build({
139130
id: 2,
140131
state: JobState.Completed,
141132
task: "source-unblocked",
142133
}),
143-
workflowJob({
134+
workflowJobFactory.build({
144135
deps: ["source-blocked"],
145136
id: 3,
146137
state: JobState.Pending,
147138
task: "consumer-pending",
148139
}),
149-
workflowJob({
140+
workflowJobFactory.build({
150141
deps: ["source-unblocked"],
151142
id: 4,
152143
state: JobState.Pending,
153144
task: "consumer-pending-unblocked",
154145
}),
155-
workflowJob({
146+
workflowJobFactory.build({
156147
deps: ["source-blocked"],
157148
id: 5,
158149
state: JobState.Cancelled,
@@ -176,7 +167,7 @@ describe("buildWorkflowGraphModel", () => {
176167
});
177168

178169
it("treats missing metadata.deps as an empty dependency list", () => {
179-
const malformedJob = workflowJob({ id: 1, task: "a" });
170+
const malformedJob = workflowJobFactory.build({ id: 1, task: "a" });
180171
(
181172
malformedJob.metadata as unknown as {
182173
deps?: string[];

src/test/factories/workflowJob.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { JobWithKnownMetadata } from "@services/jobs";
2+
import { JobState } from "@services/types";
3+
import { Factory } from "fishery";
4+
5+
import { jobFactory } from "./job";
6+
7+
const defaultWorkflowStagedAt = new Date("2025-01-01T00:00:00.000Z");
8+
const defaultWorkflowID = "wf-1";
9+
10+
type WorkflowJobFactoryParams = {
11+
deps?: string[];
12+
id?: bigint | number;
13+
state?: JobState;
14+
task?: string;
15+
workflowID?: string;
16+
workflowStagedAt?: Date;
17+
};
18+
19+
export const workflowJobFactory = Factory.define<
20+
JobWithKnownMetadata,
21+
object,
22+
JobWithKnownMetadata,
23+
WorkflowJobFactoryParams
24+
>(({ params, sequence }) => {
25+
const id =
26+
typeof params.id === "bigint" ? params.id : BigInt(params.id ?? sequence);
27+
28+
const task = params.task ?? `task-${id.toString()}`;
29+
const workflowStagedAt = params.workflowStagedAt ?? defaultWorkflowStagedAt;
30+
31+
const baseJob = jobFactory.build({
32+
createdAt: workflowStagedAt,
33+
id,
34+
kind: `job-${task}`,
35+
scheduledAt: workflowStagedAt,
36+
state: params.state ?? JobState.Available,
37+
});
38+
39+
return {
40+
...baseJob,
41+
metadata: {
42+
deps: params.deps ?? [],
43+
task,
44+
workflow_id: params.workflowID ?? defaultWorkflowID,
45+
workflow_staged_at: workflowStagedAt.toISOString(),
46+
},
47+
};
48+
});

0 commit comments

Comments
 (0)