@truedat/dd 8.13.5 → 8.13.7
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/package.json +3 -3
- package/src/components/GraphGenerationDialog.js +125 -0
- package/src/components/GraphGenerationNotices.js +67 -0
- package/src/components/GraphLoader.js +12 -17
- package/src/components/GraphRedirector.js +4 -4
- package/src/components/LineageGraph.js +38 -49
- package/src/components/LineageRoutes.js +20 -13
- package/src/components/StructureNotesEdit.js +8 -2
- package/src/components/SystemForm.js +10 -1
- package/src/components/__tests__/GraphGenerationDialog.spec.js +117 -0
- package/src/components/__tests__/GraphGenerationNotices.spec.js +60 -0
- package/src/components/__tests__/GraphGenerationTranslations.spec.js +74 -0
- package/src/components/__tests__/GraphLoader.spec.js +83 -0
- package/src/components/__tests__/LineageGraphInteraction.spec.js +78 -7
- package/src/components/__tests__/StructureNotesEdit.spec.js +105 -1
- package/src/components/__tests__/SystemForm.dateRestrictions.spec.js +102 -0
- package/src/components/__tests__/__snapshots__/LineageRoutes.spec.js.snap +12 -1
- package/src/components/__tests__/fixtures/graphGenerationMessages.json +28 -0
- package/src/messages/en.js +0 -29
- package/src/messages/es.js +0 -30
- package/src/reducers/__tests__/graphGenerationState.spec.js +113 -0
- package/src/reducers/__tests__/structureNoteErrorMessage.spec.js +78 -0
- package/src/reducers/ddMessage.js +16 -1
- package/src/reducers/graphGenerationState.js +113 -0
- package/src/reducers/graphLoading.js +2 -0
- package/src/reducers/index.js +2 -0
- package/src/routines.js +3 -0
- package/src/sagas/__tests__/createGraph.spec.js +57 -122
- package/src/sagas/__tests__/fetchGraph.spec.js +19 -0
- package/src/sagas/createGraph.js +13 -65
- package/src/sagas/fetchGraph.js +6 -38
- package/src/sagas/graph/__tests__/backgroundGraphs.spec.js +22 -0
- package/src/sagas/graph/__tests__/generation.spec.js +247 -0
- package/src/sagas/graph/__tests__/polling.spec.js +88 -0
- package/src/sagas/graph/generation.js +99 -0
- package/src/sagas/graph/polling.js +26 -35
- package/src/sagas/index.js +2 -0
- package/src/styles/LineageGraph.less +117 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { act, fireEvent, screen } from "@testing-library/react";
|
|
2
|
+
import { render } from "@truedat/test/render";
|
|
3
|
+
import { GraphGenerationDialog } from "../GraphGenerationDialog";
|
|
4
|
+
|
|
5
|
+
beforeEach(() => jest.useFakeTimers());
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
jest.runOnlyPendingTimers();
|
|
8
|
+
jest.useRealTimers();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("reveals choices after three seconds and keeps waiting without closing", () => {
|
|
12
|
+
const onBackground = jest.fn();
|
|
13
|
+
render(
|
|
14
|
+
<GraphGenerationDialog open requestKey="a" onBackground={onBackground} />,
|
|
15
|
+
);
|
|
16
|
+
act(() => jest.advanceTimersByTime(2999));
|
|
17
|
+
expect(
|
|
18
|
+
screen.queryByRole("button", { name: "lineage.graph.wait.continue" }),
|
|
19
|
+
).not.toBeInTheDocument();
|
|
20
|
+
act(() => jest.advanceTimersByTime(1));
|
|
21
|
+
fireEvent.click(
|
|
22
|
+
screen.getByRole("button", { name: "lineage.graph.wait.continue" }),
|
|
23
|
+
);
|
|
24
|
+
expect(
|
|
25
|
+
screen.getByRole("button", { name: "lineage.graph.wait.waiting" }),
|
|
26
|
+
).toBeDisabled();
|
|
27
|
+
expect(onBackground).not.toHaveBeenCalled();
|
|
28
|
+
fireEvent.click(
|
|
29
|
+
screen.getByRole("button", { name: "lineage.graph.wait.background" }),
|
|
30
|
+
);
|
|
31
|
+
expect(onBackground).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("manual close is available before the delay without enabling notifications", () => {
|
|
35
|
+
const onClose = jest.fn();
|
|
36
|
+
const onBackground = jest.fn();
|
|
37
|
+
render(
|
|
38
|
+
<GraphGenerationDialog
|
|
39
|
+
open
|
|
40
|
+
requestKey="a"
|
|
41
|
+
onClose={onClose}
|
|
42
|
+
onBackground={onBackground}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
fireEvent.click(
|
|
46
|
+
screen.getByRole("button", { name: "lineage.graph.wait.close" }),
|
|
47
|
+
);
|
|
48
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(onBackground).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("escape uses manual close and unmount cancels its timer", () => {
|
|
53
|
+
const onClose = jest.fn();
|
|
54
|
+
const onBackground = jest.fn();
|
|
55
|
+
const { unmount } = render(
|
|
56
|
+
<GraphGenerationDialog
|
|
57
|
+
open
|
|
58
|
+
requestKey="a"
|
|
59
|
+
onClose={onClose}
|
|
60
|
+
onBackground={onBackground}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
fireEvent.keyDown(document, { key: "Escape", keyCode: 27 });
|
|
64
|
+
expect(onClose).toHaveBeenCalled();
|
|
65
|
+
expect(onBackground).not.toHaveBeenCalled();
|
|
66
|
+
unmount();
|
|
67
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("progress rerenders do not restart the timer, and a new request resets the choice", () => {
|
|
71
|
+
const onBackground = jest.fn();
|
|
72
|
+
const { rerender } = render(
|
|
73
|
+
<GraphGenerationDialog open requestKey="a" onBackground={onBackground} />,
|
|
74
|
+
);
|
|
75
|
+
act(() => jest.advanceTimersByTime(2000));
|
|
76
|
+
rerender(
|
|
77
|
+
<GraphGenerationDialog open requestKey="a" onBackground={onBackground} />,
|
|
78
|
+
);
|
|
79
|
+
act(() => jest.advanceTimersByTime(1000));
|
|
80
|
+
fireEvent.click(
|
|
81
|
+
screen.getByRole("button", { name: "lineage.graph.wait.continue" }),
|
|
82
|
+
);
|
|
83
|
+
rerender(
|
|
84
|
+
<GraphGenerationDialog open requestKey="b" onBackground={onBackground} />,
|
|
85
|
+
);
|
|
86
|
+
expect(
|
|
87
|
+
screen.queryByRole("button", { name: "lineage.graph.wait.waiting" }),
|
|
88
|
+
).not.toBeInTheDocument();
|
|
89
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
90
|
+
expect(
|
|
91
|
+
screen.getByRole("button", { name: "lineage.graph.wait.continue" }),
|
|
92
|
+
).toBeEnabled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("traps keyboard focus inside the dialog and restores it after closing", () => {
|
|
96
|
+
const trigger = document.createElement("button");
|
|
97
|
+
document.body.appendChild(trigger);
|
|
98
|
+
trigger.focus();
|
|
99
|
+
const { unmount } = render(
|
|
100
|
+
<GraphGenerationDialog open requestKey="a" onBackground={jest.fn()} />,
|
|
101
|
+
);
|
|
102
|
+
const close = screen.getByRole("button", {
|
|
103
|
+
name: "lineage.graph.wait.close",
|
|
104
|
+
});
|
|
105
|
+
expect(close).toHaveFocus();
|
|
106
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
107
|
+
fireEvent.keyDown(close, { key: "Tab", shiftKey: true });
|
|
108
|
+
const background = screen.getByRole("button", {
|
|
109
|
+
name: "lineage.graph.wait.background",
|
|
110
|
+
});
|
|
111
|
+
expect(background).toHaveFocus();
|
|
112
|
+
fireEvent.keyDown(background, { key: "Tab" });
|
|
113
|
+
expect(close).toHaveFocus();
|
|
114
|
+
unmount();
|
|
115
|
+
expect(trigger).toHaveFocus();
|
|
116
|
+
trigger.remove();
|
|
117
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { fireEvent, screen } from "@testing-library/react";
|
|
2
|
+
import { render } from "@truedat/test/render";
|
|
3
|
+
import { GraphGenerationNotices } from "../GraphGenerationNotices";
|
|
4
|
+
|
|
5
|
+
test("completion gives an explicit link without automatic navigation", () => {
|
|
6
|
+
const dismiss = jest.fn();
|
|
7
|
+
render(
|
|
8
|
+
<GraphGenerationNotices
|
|
9
|
+
jobs={{
|
|
10
|
+
a: {
|
|
11
|
+
requestId: "a",
|
|
12
|
+
background: true,
|
|
13
|
+
notify: true,
|
|
14
|
+
status: "completed",
|
|
15
|
+
graphId: 42,
|
|
16
|
+
},
|
|
17
|
+
}}
|
|
18
|
+
dismissGraphGeneration={dismiss}
|
|
19
|
+
/>,
|
|
20
|
+
);
|
|
21
|
+
expect(
|
|
22
|
+
screen.getByRole("link", { name: "lineage.graph.wait.open" }),
|
|
23
|
+
).toHaveAttribute("href", "/graphs/42");
|
|
24
|
+
expect(dismiss).not.toHaveBeenCalled();
|
|
25
|
+
fireEvent.click(
|
|
26
|
+
screen.getByRole("button", { name: "lineage.graph.wait.dismiss" }),
|
|
27
|
+
);
|
|
28
|
+
expect(dismiss).toHaveBeenCalledWith("a");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("a failure is announced and has no success link", () => {
|
|
32
|
+
render(
|
|
33
|
+
<GraphGenerationNotices
|
|
34
|
+
jobs={{ a: { requestId: "a", status: "failed" } }}
|
|
35
|
+
dismissGraphGeneration={jest.fn()}
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
expect(screen.getByRole("alert")).toHaveTextContent(
|
|
39
|
+
"lineage.graph.wait.failed",
|
|
40
|
+
);
|
|
41
|
+
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("background work without explicit notification consent stays silent", () => {
|
|
45
|
+
render(
|
|
46
|
+
<GraphGenerationNotices
|
|
47
|
+
jobs={{
|
|
48
|
+
a: {
|
|
49
|
+
requestId: "a",
|
|
50
|
+
background: true,
|
|
51
|
+
status: "completed",
|
|
52
|
+
graphId: 42,
|
|
53
|
+
},
|
|
54
|
+
}}
|
|
55
|
+
dismissGraphGeneration={jest.fn()}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
|
59
|
+
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
60
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { IntlProvider } from "react-intl";
|
|
3
|
+
import { MemoryRouter } from "react-router";
|
|
4
|
+
import { GraphGenerationDialog } from "../GraphGenerationDialog";
|
|
5
|
+
import { GraphGenerationNotices } from "../GraphGenerationNotices";
|
|
6
|
+
import en from "../../messages/en";
|
|
7
|
+
import es from "../../messages/es";
|
|
8
|
+
import migratedMessages from "./fixtures/graphGenerationMessages.json";
|
|
9
|
+
|
|
10
|
+
beforeEach(() => jest.useFakeTimers());
|
|
11
|
+
afterEach(() => jest.useRealTimers());
|
|
12
|
+
|
|
13
|
+
test.each([
|
|
14
|
+
["en", { ...en, ...migratedMessages.en }],
|
|
15
|
+
["es", { ...es, ...migratedMessages.es }],
|
|
16
|
+
])(
|
|
17
|
+
"renders translated generation controls and notices in %s",
|
|
18
|
+
(locale, messages) => {
|
|
19
|
+
const onError = jest.fn();
|
|
20
|
+
const view = render(
|
|
21
|
+
<IntlProvider locale={locale} messages={messages} onError={onError}>
|
|
22
|
+
<MemoryRouter>
|
|
23
|
+
<GraphGenerationDialog open requestKey="a" onBackground={jest.fn()} />
|
|
24
|
+
<GraphGenerationNotices
|
|
25
|
+
jobs={{
|
|
26
|
+
pending: {
|
|
27
|
+
requestId: "pending",
|
|
28
|
+
notify: true,
|
|
29
|
+
status: "pending",
|
|
30
|
+
},
|
|
31
|
+
completed: {
|
|
32
|
+
requestId: "completed",
|
|
33
|
+
notify: true,
|
|
34
|
+
status: "completed",
|
|
35
|
+
graphId: 42,
|
|
36
|
+
},
|
|
37
|
+
failed: { requestId: "failed", notify: true, status: "failed" },
|
|
38
|
+
}}
|
|
39
|
+
dismissGraphGeneration={jest.fn()}
|
|
40
|
+
/>
|
|
41
|
+
</MemoryRouter>
|
|
42
|
+
</IntlProvider>,
|
|
43
|
+
);
|
|
44
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
45
|
+
const text = (suffix) => messages[`lineage.graph.wait.${suffix}`];
|
|
46
|
+
["explanation", "background.notice", "completed", "failed"].forEach(
|
|
47
|
+
(suffix) => {
|
|
48
|
+
expect(text(suffix)).toEqual(expect.any(String));
|
|
49
|
+
expect(screen.getByText(text(suffix))).toBeInTheDocument();
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
["close", "continue", "background"].forEach((suffix) => {
|
|
53
|
+
expect(text(suffix)).toEqual(expect.any(String));
|
|
54
|
+
expect(
|
|
55
|
+
screen.getByRole("button", { name: text(suffix) }),
|
|
56
|
+
).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
expect(text("open")).toEqual(expect.any(String));
|
|
59
|
+
expect(text("dismiss")).toEqual(expect.any(String));
|
|
60
|
+
expect(
|
|
61
|
+
screen.getByRole("link", { name: text("open"), hidden: true }),
|
|
62
|
+
).toHaveAttribute("href", "/graphs/42");
|
|
63
|
+
expect(
|
|
64
|
+
screen.getAllByRole("button", { name: text("dismiss"), hidden: true }),
|
|
65
|
+
).toHaveLength(2);
|
|
66
|
+
fireEvent.click(screen.getByRole("button", { name: text("continue") }));
|
|
67
|
+
expect(text("waiting")).toEqual(expect.any(String));
|
|
68
|
+
expect(
|
|
69
|
+
screen.getByRole("button", { name: text("waiting") }),
|
|
70
|
+
).toBeDisabled();
|
|
71
|
+
expect(onError).not.toHaveBeenCalled();
|
|
72
|
+
view.unmount();
|
|
73
|
+
},
|
|
74
|
+
);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { fireEvent, screen } from "@testing-library/react";
|
|
2
|
+
import { useDispatch } from "react-redux";
|
|
3
|
+
import { Route, Routes, useLocation } from "react-router";
|
|
4
|
+
import { render } from "@truedat/test/render";
|
|
5
|
+
import ConnectedGraphLoader, { GraphLoader } from "../GraphLoader";
|
|
6
|
+
import GraphRedirector from "../GraphRedirector";
|
|
7
|
+
import { graph as graphReducer } from "../../reducers/graph";
|
|
8
|
+
import { graphRedirect as redirectReducer } from "../../reducers/graphRedirect";
|
|
9
|
+
import { createGraph, fetchGraph } from "../../routines";
|
|
10
|
+
|
|
11
|
+
const GenerateGraph = () => {
|
|
12
|
+
const dispatch = useDispatch();
|
|
13
|
+
const { pathname } = useLocation();
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<button
|
|
17
|
+
onClick={() =>
|
|
18
|
+
dispatch(
|
|
19
|
+
createGraph.success({ data: { id: 43, isRedirected: true } }),
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
>
|
|
23
|
+
Complete generation
|
|
24
|
+
</button>
|
|
25
|
+
<output>{pathname}</output>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
test("redirects to the generated graph without refetching the previous graph", () => {
|
|
31
|
+
const requests = jest.fn();
|
|
32
|
+
render(
|
|
33
|
+
<Routes>
|
|
34
|
+
<Route
|
|
35
|
+
path="/graphs/:id"
|
|
36
|
+
element={
|
|
37
|
+
<>
|
|
38
|
+
<GenerateGraph />
|
|
39
|
+
<GraphRedirector />
|
|
40
|
+
<ConnectedGraphLoader />
|
|
41
|
+
</>
|
|
42
|
+
}
|
|
43
|
+
/>
|
|
44
|
+
</Routes>,
|
|
45
|
+
{
|
|
46
|
+
routes: ["/graphs/42"],
|
|
47
|
+
state: { graph: { id: 42 }, graphRedirect: "" },
|
|
48
|
+
reducer: (state, action) => {
|
|
49
|
+
if (action.type === fetchGraph.TRIGGER) requests(action.payload);
|
|
50
|
+
return {
|
|
51
|
+
graph: graphReducer(state.graph, action),
|
|
52
|
+
graphRedirect: redirectReducer(state.graphRedirect, action),
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
fireEvent.click(screen.getByRole("button", { name: "Complete generation" }));
|
|
58
|
+
expect(screen.getByText("/graphs/43")).toBeInTheDocument();
|
|
59
|
+
expect(requests).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("does not fetch repeatedly when a response is missing its graph id", () => {
|
|
63
|
+
const fetchGraph = jest.fn();
|
|
64
|
+
const clearGraph = jest.fn();
|
|
65
|
+
const view = (graph) => (
|
|
66
|
+
<Routes>
|
|
67
|
+
<Route
|
|
68
|
+
path="/graphs/:id"
|
|
69
|
+
element={
|
|
70
|
+
<GraphLoader
|
|
71
|
+
graph={graph}
|
|
72
|
+
fetchGraph={fetchGraph}
|
|
73
|
+
clearGraph={clearGraph}
|
|
74
|
+
/>
|
|
75
|
+
}
|
|
76
|
+
/>
|
|
77
|
+
</Routes>
|
|
78
|
+
);
|
|
79
|
+
const { rerender } = render(view({}), { routes: ["/graphs/42"] });
|
|
80
|
+
rerender(view({ data: { id: 42 } }));
|
|
81
|
+
rerender(view({ data: { id: 42 } }));
|
|
82
|
+
expect(fetchGraph).toHaveBeenCalledTimes(1);
|
|
83
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { fireEvent, screen, within } from "@testing-library/react";
|
|
1
|
+
import { act, fireEvent, screen, within } from "@testing-library/react";
|
|
2
2
|
import { Route, Routes, useLocation } from "react-router";
|
|
3
3
|
import { render } from "@truedat/test/render";
|
|
4
4
|
import { LineageGraph } from "../LineageGraph";
|
|
5
5
|
import { GraphLoader } from "../GraphLoader";
|
|
6
6
|
|
|
7
7
|
let mockGraphProps;
|
|
8
|
+
let mockDeferInitialFit = false;
|
|
8
9
|
|
|
9
10
|
jest.mock("@truedat/core/components/Graph", () => {
|
|
10
11
|
const React = require("react");
|
|
@@ -12,10 +13,9 @@ jest.mock("@truedat/core/components/Graph", () => {
|
|
|
12
13
|
return function Graph(props) {
|
|
13
14
|
mockGraphProps = props;
|
|
14
15
|
const [expanded, setExpanded] = React.useState(false);
|
|
15
|
-
React.useEffect(
|
|
16
|
-
()
|
|
17
|
-
|
|
18
|
-
);
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
if (!mockDeferInitialFit) props.onInitialFitComplete();
|
|
18
|
+
}, [props.onInitialFitComplete]);
|
|
19
19
|
const canvas = (name) => (
|
|
20
20
|
<div className="td-graph-container" data-testid={name}>
|
|
21
21
|
{props.nodes
|
|
@@ -50,7 +50,6 @@ const props = {
|
|
|
50
50
|
graphId: "graph-1",
|
|
51
51
|
loading: false,
|
|
52
52
|
simplified: true,
|
|
53
|
-
cancelPoll: jest.fn(),
|
|
54
53
|
groups: [],
|
|
55
54
|
resources: [
|
|
56
55
|
{ id: "orders", name: "Orders", x: 0, y: 0, w: 288, h: 100 },
|
|
@@ -72,6 +71,55 @@ const renderGraph = (extra = {}) =>
|
|
|
72
71
|
{ state },
|
|
73
72
|
);
|
|
74
73
|
|
|
74
|
+
it("keeps the unanswered loading dialog through polling and until the graph is fitted", () => {
|
|
75
|
+
mockDeferInitialFit = true;
|
|
76
|
+
const backgroundGraph = jest.fn();
|
|
77
|
+
const onGraphReady = jest.fn();
|
|
78
|
+
const generationJob = { requestId: "slow-graph", status: "pending" };
|
|
79
|
+
const pendingProps = {
|
|
80
|
+
...props,
|
|
81
|
+
simplified: false,
|
|
82
|
+
loading: true,
|
|
83
|
+
generationJob,
|
|
84
|
+
backgroundGraph,
|
|
85
|
+
onGraphReady,
|
|
86
|
+
};
|
|
87
|
+
const view = render(<LineageGraph {...pendingProps} />, { state });
|
|
88
|
+
try {
|
|
89
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
90
|
+
const continueButton = screen.getByRole("button", {
|
|
91
|
+
name: "lineage.graph.wait.continue",
|
|
92
|
+
});
|
|
93
|
+
for (let retry = 1; retry <= 3; retry += 1) {
|
|
94
|
+
view.rerender(
|
|
95
|
+
<LineageGraph
|
|
96
|
+
{...pendingProps}
|
|
97
|
+
generationJob={{ ...generationJob, hash: "slow-hash", retry }}
|
|
98
|
+
/>,
|
|
99
|
+
);
|
|
100
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
101
|
+
expect(continueButton).toBeVisible();
|
|
102
|
+
expect(continueButton).toBeEnabled();
|
|
103
|
+
}
|
|
104
|
+
view.rerender(
|
|
105
|
+
<LineageGraph
|
|
106
|
+
{...pendingProps}
|
|
107
|
+
loading={false}
|
|
108
|
+
generationJob={{ ...generationJob, status: "completed" }}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
expect(continueButton).toBeVisible();
|
|
112
|
+
expect(onGraphReady).not.toHaveBeenCalled();
|
|
113
|
+
expect(backgroundGraph).not.toHaveBeenCalled();
|
|
114
|
+
act(() => mockGraphProps.onInitialFitComplete());
|
|
115
|
+
expect(continueButton).not.toBeInTheDocument();
|
|
116
|
+
expect(onGraphReady).toHaveBeenCalledTimes(1);
|
|
117
|
+
} finally {
|
|
118
|
+
mockDeferInitialFit = false;
|
|
119
|
+
view.unmount();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
75
123
|
it("selects successive nodes through a real portal and keeps dependencies active on return", () => {
|
|
76
124
|
renderGraph();
|
|
77
125
|
fireEvent.click(screen.getByText("Toggle expanded"));
|
|
@@ -225,7 +273,6 @@ it.each([
|
|
|
225
273
|
graph={graph}
|
|
226
274
|
fetchGraph={fetchGraph}
|
|
227
275
|
clearGraph={jest.fn()}
|
|
228
|
-
cancelPoll={jest.fn()}
|
|
229
276
|
/>
|
|
230
277
|
}
|
|
231
278
|
/>
|
|
@@ -236,3 +283,27 @@ it.each([
|
|
|
236
283
|
else expect(fetchGraph).not.toHaveBeenCalled();
|
|
237
284
|
},
|
|
238
285
|
);
|
|
286
|
+
|
|
287
|
+
test.each([
|
|
288
|
+
{ simplified: true },
|
|
289
|
+
{ simplified: false },
|
|
290
|
+
{
|
|
291
|
+
simplified: true,
|
|
292
|
+
generationJob: { requestId: "other", status: "pending" },
|
|
293
|
+
},
|
|
294
|
+
])("keeps loading inline without a background action: %j", (extra) => {
|
|
295
|
+
const backgroundGraph = jest.fn();
|
|
296
|
+
const view = renderGraph({ ...extra, loading: true, backgroundGraph });
|
|
297
|
+
act(() => jest.advanceTimersByTime(3000));
|
|
298
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
299
|
+
expect(
|
|
300
|
+
screen.getByText("lineage.graph.loading").closest("[role=status]"),
|
|
301
|
+
).toBeInTheDocument();
|
|
302
|
+
expect(
|
|
303
|
+
screen.queryByRole("button", { name: "lineage.graph.wait.background" }),
|
|
304
|
+
).not.toBeInTheDocument();
|
|
305
|
+
fireEvent.keyDown(document, { key: "Escape", keyCode: 27 });
|
|
306
|
+
expect(backgroundGraph).not.toHaveBeenCalled();
|
|
307
|
+
expect(screen.getByText("lineage.graph.loading")).toBeInTheDocument();
|
|
308
|
+
view.unmount();
|
|
309
|
+
});
|
|
@@ -12,12 +12,14 @@ jest.mock("@truedat/ai/components/suggestions/SuggestionsWidget", () => ({
|
|
|
12
12
|
|
|
13
13
|
let dynamicFormOnChange;
|
|
14
14
|
let dynamicFormContent;
|
|
15
|
+
let dynamicFormOriginalContent;
|
|
15
16
|
|
|
16
17
|
jest.mock("@truedat/df/components/DynamicForm", () => ({
|
|
17
18
|
__esModule: true,
|
|
18
|
-
default: ({ onChange, content }) => {
|
|
19
|
+
default: ({ onChange, content, originalContent }) => {
|
|
19
20
|
dynamicFormOnChange = onChange;
|
|
20
21
|
dynamicFormContent = content;
|
|
22
|
+
dynamicFormOriginalContent = originalContent;
|
|
21
23
|
return <div data-testid="dynamic-form" />;
|
|
22
24
|
},
|
|
23
25
|
}));
|
|
@@ -27,6 +29,108 @@ describe("<StructureNotesEdit />", () => {
|
|
|
27
29
|
jest.clearAllMocks();
|
|
28
30
|
dynamicFormOnChange = undefined;
|
|
29
31
|
dynamicFormContent = undefined;
|
|
32
|
+
dynamicFormOriginalContent = undefined;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("date restrictions when editing", () => {
|
|
36
|
+
const template = {
|
|
37
|
+
id: 2,
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
name: "dates",
|
|
41
|
+
fields: [
|
|
42
|
+
{
|
|
43
|
+
name: "fecha",
|
|
44
|
+
type: "date",
|
|
45
|
+
restrictions: { resolved: { min: null, max: "2026-10-15" } },
|
|
46
|
+
},
|
|
47
|
+
{ name: "description", type: "string" },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
const latestDfContent = {
|
|
53
|
+
fecha: { value: "2026-10-16", origin: "user" },
|
|
54
|
+
description: { value: "Original", origin: "user" },
|
|
55
|
+
};
|
|
56
|
+
const props = {
|
|
57
|
+
structure: { id: 1, domain_ids: [1] },
|
|
58
|
+
template,
|
|
59
|
+
applyTemplate: applyTemplate(template),
|
|
60
|
+
latestDfContent,
|
|
61
|
+
selectTemplate: jest.fn(),
|
|
62
|
+
selectDomains: jest.fn(),
|
|
63
|
+
doStructureNoteAction: jest.fn(),
|
|
64
|
+
structureNotes: { structureNotes: [] },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
it("keeps Save enabled for an unchanged out-of-range date", async () => {
|
|
68
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
69
|
+
await waitForLoad(rendered);
|
|
70
|
+
|
|
71
|
+
expect(await rendered.findByText(/save/i)).toBeEnabled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("passes persisted originals to the widgets", async () => {
|
|
75
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
76
|
+
await waitForLoad(rendered);
|
|
77
|
+
|
|
78
|
+
expect(dynamicFormOriginalContent).toEqual(latestDfContent);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("keeps Save enabled when only another field is edited", async () => {
|
|
82
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
83
|
+
await waitForLoad(rendered);
|
|
84
|
+
|
|
85
|
+
await act(async () => {
|
|
86
|
+
dynamicFormOnChange({
|
|
87
|
+
...latestDfContent,
|
|
88
|
+
description: { value: "Updated", origin: "user" },
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(await rendered.findByText(/save/i)).toBeEnabled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("disables Save when the date changes to another invalid value", async () => {
|
|
96
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
97
|
+
await waitForLoad(rendered);
|
|
98
|
+
|
|
99
|
+
await act(async () => {
|
|
100
|
+
dynamicFormOnChange({
|
|
101
|
+
...latestDfContent,
|
|
102
|
+
fecha: { value: "2026-10-17", origin: "user" },
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(await rendered.findByText(/save/i)).toBeDisabled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("restores the exemption when the original date is restored", async () => {
|
|
110
|
+
const rendered = render(<StructureNotesEdit {...props} />);
|
|
111
|
+
await waitForLoad(rendered);
|
|
112
|
+
|
|
113
|
+
await act(async () => {
|
|
114
|
+
dynamicFormOnChange({
|
|
115
|
+
...latestDfContent,
|
|
116
|
+
fecha: { value: "2026-10-17", origin: "user" },
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
await act(async () => dynamicFormOnChange({ ...latestDfContent }));
|
|
120
|
+
|
|
121
|
+
expect(await rendered.findByText(/save/i)).toBeEnabled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("still validates dates on creation", async () => {
|
|
125
|
+
const rendered = render(
|
|
126
|
+
<StructureNotesEdit {...props} latestDfContent={{}} />,
|
|
127
|
+
);
|
|
128
|
+
await waitForLoad(rendered);
|
|
129
|
+
|
|
130
|
+
await act(async () => dynamicFormOnChange(latestDfContent));
|
|
131
|
+
|
|
132
|
+
expect(await rendered.findByText(/save/i)).toBeDisabled();
|
|
133
|
+
});
|
|
30
134
|
});
|
|
31
135
|
|
|
32
136
|
it("matches the latest snapshot", async () => {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { act } from "@testing-library/react";
|
|
2
|
+
import { render, waitForLoad } from "@truedat/test/render";
|
|
3
|
+
import DynamicForm from "@truedat/df/components/DynamicForm";
|
|
4
|
+
import { SystemForm } from "../SystemForm";
|
|
5
|
+
|
|
6
|
+
jest.mock("@truedat/df/components/DynamicForm", () => jest.fn(() => null));
|
|
7
|
+
jest.mock("@truedat/df/templates/components/TemplateLoader", () => () => null);
|
|
8
|
+
|
|
9
|
+
const content = { date: { value: "2025-01-01", origin: "user" } };
|
|
10
|
+
const template = {
|
|
11
|
+
name: "system",
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
name: "dates",
|
|
15
|
+
fields: [
|
|
16
|
+
{
|
|
17
|
+
name: "date",
|
|
18
|
+
type: "date",
|
|
19
|
+
cardinality: "?",
|
|
20
|
+
restrictions: { max_date: { mode: "static", value: "2024-01-01" } },
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
const system = {
|
|
27
|
+
id: 1,
|
|
28
|
+
name: "System",
|
|
29
|
+
external_id: "system",
|
|
30
|
+
dynamic_content: content,
|
|
31
|
+
};
|
|
32
|
+
const formProps = () => DynamicForm.mock.calls.at(-1)[0];
|
|
33
|
+
|
|
34
|
+
describe("<SystemForm /> date restrictions", () => {
|
|
35
|
+
beforeEach(() => jest.clearAllMocks());
|
|
36
|
+
|
|
37
|
+
it("passes persisted content to the dynamic form", async () => {
|
|
38
|
+
const rendered = render(
|
|
39
|
+
<SystemForm
|
|
40
|
+
system={system}
|
|
41
|
+
template={template}
|
|
42
|
+
createSystem={jest.fn()}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
await waitForLoad(rendered);
|
|
46
|
+
expect(formProps().originalContent).toEqual(content);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("allows unchanged persisted dates outside the current range", async () => {
|
|
50
|
+
const rendered = render(
|
|
51
|
+
<SystemForm
|
|
52
|
+
system={system}
|
|
53
|
+
template={template}
|
|
54
|
+
createSystem={jest.fn()}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
await waitForLoad(rendered);
|
|
58
|
+
expect(
|
|
59
|
+
rendered.getByRole("button", { name: "actions.update" }),
|
|
60
|
+
).toBeEnabled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rejects changed dates outside the current range", async () => {
|
|
64
|
+
const rendered = render(
|
|
65
|
+
<SystemForm
|
|
66
|
+
system={system}
|
|
67
|
+
template={template}
|
|
68
|
+
createSystem={jest.fn()}
|
|
69
|
+
/>,
|
|
70
|
+
);
|
|
71
|
+
await waitForLoad(rendered);
|
|
72
|
+
act(() =>
|
|
73
|
+
formProps().onChange({ date: { value: "2025-02-01", origin: "user" } }),
|
|
74
|
+
);
|
|
75
|
+
expect(
|
|
76
|
+
rendered.getByRole("button", { name: "actions.update" }),
|
|
77
|
+
).toBeDisabled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("keeps the edit baseline after content changes", async () => {
|
|
81
|
+
const rendered = render(
|
|
82
|
+
<SystemForm
|
|
83
|
+
system={system}
|
|
84
|
+
template={template}
|
|
85
|
+
createSystem={jest.fn()}
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
await waitForLoad(rendered);
|
|
89
|
+
act(() =>
|
|
90
|
+
formProps().onChange({ date: { value: "2023-01-01", origin: "user" } }),
|
|
91
|
+
);
|
|
92
|
+
expect(formProps().originalContent).toEqual(content);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("does not supply an original for creation", async () => {
|
|
96
|
+
const rendered = render(
|
|
97
|
+
<SystemForm template={template} createSystem={jest.fn()} />,
|
|
98
|
+
);
|
|
99
|
+
await waitForLoad(rendered);
|
|
100
|
+
expect(formProps().originalContent).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
});
|