lhcb-ntuple-wizard-test 2.1.0 → 2.2.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.
Files changed (178) hide show
  1. package/dist/components/AddTupleToolButton.d.ts +1 -0
  2. package/dist/components/AddTupleToolButton.js +16 -0
  3. package/dist/components/BookkeepingPathDropdown.d.ts +1 -1
  4. package/dist/components/BookkeepingPathDropdown.js +1 -4
  5. package/dist/components/DatasetEventTypeBadge.js +1 -1
  6. package/dist/components/DatasetInfo.js +3 -3
  7. package/dist/components/DecayCard.d.ts +1 -1
  8. package/dist/components/DecayCard.js +12 -40
  9. package/dist/components/DecayLatex.d.ts +1 -1
  10. package/dist/components/DecayLatex.js +4 -87
  11. package/dist/components/DecayListItem.js +2 -2
  12. package/dist/components/DecayTagBadge.d.ts +1 -1
  13. package/dist/components/DecayTagBadge.js +0 -3
  14. package/dist/components/DecayTreeCard.d.ts +1 -7
  15. package/dist/components/DecayTreeCard.js +13 -18
  16. package/dist/components/DecayTreeCardHeader.d.ts +2 -3
  17. package/dist/components/DecayTreeCardHeader.js +2 -2
  18. package/dist/components/DecayTreeGraph.d.ts +1 -4
  19. package/dist/components/DecayTreeGraph.js +66 -41
  20. package/dist/components/DeleteButton.d.ts +2 -1
  21. package/dist/components/DeleteButton.js +2 -2
  22. package/dist/components/DttNameInput.d.ts +9 -0
  23. package/dist/components/DttNameInput.js +43 -0
  24. package/dist/components/GuideLinkButton.d.ts +15 -0
  25. package/dist/components/GuideLinkButton.js +16 -0
  26. package/dist/components/NtupleWizard.js +1 -7
  27. package/dist/components/ParticleDropdown.d.ts +11 -1
  28. package/dist/components/ParticleDropdown.js +3 -6
  29. package/dist/components/ParticleLatex.d.ts +6 -0
  30. package/dist/components/ParticleLatex.js +13 -0
  31. package/dist/components/ParticleTagBadge.d.ts +2 -1
  32. package/dist/components/ParticleTagBadge.js +5 -7
  33. package/dist/components/ParticleTagFilters.d.ts +1 -6
  34. package/dist/components/ParticleTagFilters.js +16 -17
  35. package/dist/components/RequestButtonGroup.d.ts +1 -1
  36. package/dist/components/RequestButtonGroup.js +0 -3
  37. package/dist/components/RequestRow.d.ts +1 -1
  38. package/dist/components/RequestRow.js +4 -9
  39. package/dist/components/StrippingLineDropdown.js +14 -16
  40. package/dist/components/StrippingLineInfo.d.ts +5 -5
  41. package/dist/components/StrippingLineInfo.js +14 -4
  42. package/dist/components/StrippingLineInfoButton.d.ts +6 -0
  43. package/dist/components/StrippingLineInfoButton.js +7 -0
  44. package/dist/components/StrippingLineVersionBadge.d.ts +7 -0
  45. package/dist/components/{StrippingLineBadge.js → StrippingLineVersionBadge.js} +5 -5
  46. package/dist/components/StrippingLinesCountBadge.d.ts +8 -0
  47. package/dist/components/StrippingLinesCountBadge.js +11 -0
  48. package/dist/components/TagDropdown.d.ts +3 -3
  49. package/dist/components/TagDropdown.js +2 -2
  50. package/dist/components/TupleToolClassDropdown.js +11 -14
  51. package/dist/components/TupleToolDocsAccordion.d.ts +1 -1
  52. package/dist/components/TupleToolDocsAccordion.js +4 -8
  53. package/dist/components/TupleToolGroupsAccordion.d.ts +5 -0
  54. package/dist/components/TupleToolGroupsAccordion.js +31 -0
  55. package/dist/components/TupleToolLabel.d.ts +1 -1
  56. package/dist/components/TupleToolLabel.js +2 -5
  57. package/dist/components/TupleToolsAccordion.d.ts +1 -0
  58. package/dist/components/TupleToolsAccordion.js +22 -0
  59. package/dist/components/modals/AddTupleToolModal.js +3 -2
  60. package/dist/components/modals/ConfigureTupleToolModal.js +1 -1
  61. package/dist/components/modals/UploadDttConfigModal.d.ts +1 -1
  62. package/dist/components/modals/UploadDttConfigModal.js +1 -4
  63. package/dist/config.d.ts +1 -0
  64. package/dist/config.js +3 -2
  65. package/dist/models/bkPath.js +1 -1
  66. package/dist/models/dtt.d.ts +5 -2
  67. package/dist/models/dtt.js +37 -18
  68. package/dist/models/rowData.d.ts +1 -1
  69. package/dist/models/yamlFile.js +1 -1
  70. package/dist/pages/DecaySearchPage.js +4 -9
  71. package/dist/pages/DecayTreeConfigPage.js +4 -38
  72. package/dist/pages/RequestPage.js +2 -4
  73. package/dist/providers/DttProvider.d.ts +3 -3
  74. package/dist/providers/DttProvider.js +30 -55
  75. package/dist/providers/MetadataProvider.d.ts +1 -1
  76. package/dist/providers/MetadataProvider.js +11 -5
  77. package/dist/providers/RequestProvider.js +2 -2
  78. package/dist/providers/RowProvider.d.ts +2 -2
  79. package/dist/providers/RowProvider.js +10 -9
  80. package/dist/providers/RowsProvider.js +0 -3
  81. package/dist/tests/components/BookkeepingPathDropdown.test.d.ts +1 -0
  82. package/dist/tests/components/BookkeepingPathDropdown.test.js +118 -0
  83. package/dist/tests/components/DatasetInfo.test.d.ts +1 -0
  84. package/dist/tests/components/DatasetInfo.test.js +38 -0
  85. package/dist/tests/components/DecayCard.test.d.ts +1 -0
  86. package/dist/tests/components/DecayCard.test.js +115 -0
  87. package/dist/tests/components/DecayLatex.test.d.ts +1 -0
  88. package/dist/tests/components/DecayLatex.test.js +31 -0
  89. package/dist/tests/components/DecayList.test.d.ts +1 -0
  90. package/dist/tests/components/DecayList.test.js +76 -0
  91. package/dist/tests/components/DecayListItem.test.d.ts +1 -0
  92. package/dist/tests/components/DecayListItem.test.js +51 -0
  93. package/dist/tests/components/DecayTreeCard.test.d.ts +1 -0
  94. package/dist/tests/components/DecayTreeCard.test.js +119 -0
  95. package/dist/tests/components/DecayTreeGraph.test.d.ts +1 -0
  96. package/dist/tests/components/DecayTreeGraph.test.js +125 -0
  97. package/dist/tests/components/DeleteButton.test.d.ts +1 -0
  98. package/dist/tests/components/DeleteButton.test.js +45 -0
  99. package/dist/tests/components/DttNameInput.test.d.ts +1 -0
  100. package/dist/tests/components/DttNameInput.test.js +75 -0
  101. package/dist/tests/components/NtupleWizard.test.d.ts +1 -0
  102. package/dist/tests/components/NtupleWizard.test.js +57 -0
  103. package/dist/tests/components/ParticleDropdown.test.d.ts +1 -0
  104. package/dist/tests/components/ParticleDropdown.test.js +23 -0
  105. package/dist/tests/components/ParticleTagFilters.test.d.ts +1 -0
  106. package/dist/tests/components/ParticleTagFilters.test.js +87 -0
  107. package/dist/tests/components/RequestButtonGroup.test.d.ts +1 -0
  108. package/dist/tests/components/RequestButtonGroup.test.js +132 -0
  109. package/dist/tests/components/RequestRow.test.d.ts +1 -0
  110. package/dist/tests/components/RequestRow.test.js +58 -0
  111. package/dist/tests/components/StrippingLineDropdown.test.d.ts +1 -0
  112. package/dist/tests/components/StrippingLineDropdown.test.js +88 -0
  113. package/dist/tests/components/badges.test.d.ts +1 -0
  114. package/dist/tests/components/badges.test.js +120 -0
  115. package/dist/tests/components/dropdowns.test.d.ts +1 -0
  116. package/dist/tests/components/dropdowns.test.js +110 -0
  117. package/dist/tests/components/dttComponents.test.d.ts +1 -0
  118. package/dist/tests/components/dttComponents.test.js +287 -0
  119. package/dist/tests/components/formInputs.test.d.ts +1 -0
  120. package/dist/tests/components/formInputs.test.js +96 -0
  121. package/dist/tests/components/metadataComponents.test.d.ts +1 -0
  122. package/dist/tests/components/metadataComponents.test.js +137 -0
  123. package/dist/tests/components/miscComponents.test.d.ts +1 -0
  124. package/dist/tests/components/miscComponents.test.js +134 -0
  125. package/dist/tests/components/modals.test.d.ts +1 -0
  126. package/dist/tests/components/modals.test.js +554 -0
  127. package/dist/tests/components/tupleToolParams.test.d.ts +1 -0
  128. package/dist/tests/components/tupleToolParams.test.js +213 -0
  129. package/dist/tests/config.test.d.ts +1 -0
  130. package/dist/tests/config.test.js +31 -0
  131. package/dist/tests/mockSetup.d.ts +1 -0
  132. package/dist/tests/mockSetup.js +30 -0
  133. package/dist/tests/models/BkPath.test.d.ts +1 -0
  134. package/dist/tests/models/BkPath.test.js +87 -0
  135. package/dist/tests/models/Dtt.test.d.ts +1 -0
  136. package/dist/tests/models/Dtt.test.js +376 -0
  137. package/dist/tests/models/TupleTool.test.d.ts +1 -0
  138. package/dist/tests/models/TupleTool.test.js +80 -0
  139. package/dist/tests/models/YamlFile.test.d.ts +1 -0
  140. package/dist/tests/models/YamlFile.test.js +123 -0
  141. package/dist/tests/pages/DecaySearchPage.test.d.ts +1 -0
  142. package/dist/tests/pages/DecaySearchPage.test.js +228 -0
  143. package/dist/tests/pages/DecayTreeConfigPage.test.d.ts +1 -0
  144. package/dist/tests/pages/DecayTreeConfigPage.test.js +105 -0
  145. package/dist/tests/pages/RequestPage.test.d.ts +1 -0
  146. package/dist/tests/pages/RequestPage.test.js +439 -0
  147. package/dist/tests/providers/DttProvider.test.d.ts +1 -0
  148. package/dist/tests/providers/DttProvider.test.js +105 -0
  149. package/dist/tests/providers/MetadataProvider.test.d.ts +1 -0
  150. package/dist/tests/providers/MetadataProvider.test.js +129 -0
  151. package/dist/tests/providers/RequestProvider.test.d.ts +1 -0
  152. package/dist/tests/providers/RequestProvider.test.js +306 -0
  153. package/dist/tests/providers/RowProvider.test.d.ts +1 -0
  154. package/dist/tests/providers/RowProvider.test.js +110 -0
  155. package/dist/tests/providers/RowsProvider.test.d.ts +1 -0
  156. package/dist/tests/providers/RowsProvider.test.js +84 -0
  157. package/dist/tests/providers/WizardConfigProvider.test.d.ts +1 -0
  158. package/dist/tests/providers/WizardConfigProvider.test.js +36 -0
  159. package/dist/tests/setupTests.d.ts +1 -0
  160. package/dist/tests/setupTests.js +15 -0
  161. package/dist/tests/testUtils.d.ts +33 -0
  162. package/dist/tests/testUtils.js +196 -0
  163. package/dist/tests/utils/latexUtils.test.d.ts +1 -0
  164. package/dist/tests/utils/latexUtils.test.js +62 -0
  165. package/dist/tests/utils/utils.test.d.ts +1 -0
  166. package/dist/tests/utils/utils.test.js +394 -0
  167. package/dist/utils/latexUtils.d.ts +13 -0
  168. package/dist/utils/latexUtils.js +86 -0
  169. package/dist/utils/utils.d.ts +1 -0
  170. package/dist/utils/utils.js +4 -1
  171. package/package.json +16 -7
  172. package/dist/components/NumStrippingLinesBadge.d.ts +0 -8
  173. package/dist/components/NumStrippingLinesBadge.js +0 -10
  174. package/dist/components/StrippingLineBadge.d.ts +0 -7
  175. package/dist/components/TupleToolGroup.d.ts +0 -5
  176. package/dist/components/TupleToolGroup.js +0 -22
  177. package/dist/components/TupleToolList.d.ts +0 -6
  178. package/dist/components/TupleToolList.js +0 -42
@@ -0,0 +1,129 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi, afterEach } from "vitest";
3
+ import { render, screen, waitFor } from "@testing-library/react";
4
+ import { renderHook } from "@testing-library/react";
5
+ import { MetadataProvider, useMetadata } from "../../providers/MetadataProvider";
6
+ // ---------------------------------------------------------------------------
7
+ // Mock pako — inflate returns raw bytes of an empty JSON object so the
8
+ // provider can call JSON.parse without real gzip decompression.
9
+ // ---------------------------------------------------------------------------
10
+ const mockInflatedBytes = new TextEncoder().encode(JSON.stringify({}));
11
+ // Restore the real module — the global mock in mockSetup.tsx is overridden here
12
+ vi.mock("../../providers/MetadataProvider", async (importOriginal) => await importOriginal());
13
+ vi.mock("pako", () => ({
14
+ default: { inflate: vi.fn(() => mockInflatedBytes) },
15
+ }));
16
+ // Stub config so the provider only fetches one file — keeps tests fast.
17
+ vi.mock("../../config", () => ({
18
+ config: {
19
+ metadata_baseurl: "https://example.com/",
20
+ metadata_files: { particleProperties: "particles" },
21
+ batch_size: 10,
22
+ particleTagGroupStyles: {},
23
+ dttGraphOptions: {},
24
+ disabledTupleTools: [],
25
+ tupleToolValidation: {},
26
+ },
27
+ }));
28
+ // ---------------------------------------------------------------------------
29
+ // Helpers
30
+ // ---------------------------------------------------------------------------
31
+ const successResponse = {
32
+ ok: true,
33
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
34
+ };
35
+ function wrapper({ children }) {
36
+ return _jsx(MetadataProvider, { children: children });
37
+ }
38
+ // ---------------------------------------------------------------------------
39
+ // Tests — loading state
40
+ // ---------------------------------------------------------------------------
41
+ describe("MetadataProvider — loading state", () => {
42
+ afterEach(() => {
43
+ vi.restoreAllMocks();
44
+ });
45
+ it("shows a loading indicator while the fetch is pending", () => {
46
+ // fetch never resolves — component stays in loading state
47
+ vi.stubGlobal("fetch", vi.fn(() => new Promise(() => { })));
48
+ render(_jsx(MetadataProvider, { children: _jsx("div", { "data-testid": "children" }) }));
49
+ expect(screen.getByTestId("loading-indicator")).toBeInTheDocument();
50
+ expect(screen.queryByTestId("children")).not.toBeInTheDocument();
51
+ });
52
+ });
53
+ // ---------------------------------------------------------------------------
54
+ // Tests — success state
55
+ // ---------------------------------------------------------------------------
56
+ describe("MetadataProvider — success state", () => {
57
+ afterEach(() => {
58
+ vi.restoreAllMocks();
59
+ });
60
+ it("renders children once metadata has loaded", async () => {
61
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue(successResponse));
62
+ render(_jsx(MetadataProvider, { children: _jsx("div", { "data-testid": "children" }) }));
63
+ await waitFor(() => expect(screen.getByTestId("children")).toBeInTheDocument());
64
+ expect(screen.queryByTestId("loading-indicator")).not.toBeInTheDocument();
65
+ });
66
+ it("hides the loading indicator after metadata loads", async () => {
67
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue(successResponse));
68
+ render(_jsx(MetadataProvider, { children: _jsx("div", { "data-testid": "children" }) }));
69
+ await waitFor(() => screen.getByTestId("children"));
70
+ expect(screen.queryByTestId("loading-indicator")).not.toBeInTheDocument();
71
+ });
72
+ it("provides metadata context to children via useMetadata()", async () => {
73
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue(successResponse));
74
+ const { result } = renderHook(() => useMetadata(), { wrapper });
75
+ // Wait for metadata to load (hook result changes from throwing to returning)
76
+ await waitFor(() => expect(result.current).toBeTruthy());
77
+ });
78
+ });
79
+ // ---------------------------------------------------------------------------
80
+ // Tests — error state
81
+ // ---------------------------------------------------------------------------
82
+ describe("MetadataProvider — error state", () => {
83
+ afterEach(() => {
84
+ vi.restoreAllMocks();
85
+ });
86
+ it("shows an error message when the fetch rejects", async () => {
87
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Network error")));
88
+ render(_jsx(MetadataProvider, { children: _jsx("div", { "data-testid": "children" }) }));
89
+ await waitFor(() => expect(screen.getByText(/error/i)).toBeInTheDocument());
90
+ expect(screen.queryByTestId("children")).not.toBeInTheDocument();
91
+ expect(screen.queryByTestId("loading-indicator")).not.toBeInTheDocument();
92
+ });
93
+ it("does not render children when an error occurs", async () => {
94
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Network error")));
95
+ render(_jsx(MetadataProvider, { children: _jsx("div", { "data-testid": "children" }) }));
96
+ await waitFor(() => screen.getByText(/error/i));
97
+ expect(screen.queryByTestId("children")).not.toBeInTheDocument();
98
+ });
99
+ });
100
+ // ---------------------------------------------------------------------------
101
+ // Tests — useMetadata hook
102
+ // ---------------------------------------------------------------------------
103
+ describe("useMetadata()", () => {
104
+ it("throws when used outside a MetadataProvider", () => {
105
+ expect(() => renderHook(() => useMetadata())).toThrow("useMetadata must be used inside MetadataProvider");
106
+ });
107
+ });
108
+ // ---------------------------------------------------------------------------
109
+ // Tests — non-OK response (lines 73-75)
110
+ // ---------------------------------------------------------------------------
111
+ describe("MetadataProvider — non-OK fetch response", () => {
112
+ afterEach(() => {
113
+ vi.restoreAllMocks();
114
+ });
115
+ it("handles a non-OK response (e.g. 404) gracefully", async () => {
116
+ const notFoundResponse = {
117
+ ok: false,
118
+ status: 404,
119
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
120
+ };
121
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue(notFoundResponse));
122
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
123
+ render(_jsx(MetadataProvider, { children: _jsx("div", { "data-testid": "children" }) }));
124
+ // The provider should eventually show an error (loadDict returns null, setMetadata never fires)
125
+ // or the error boundary triggers; just verify the component doesn't throw
126
+ await new Promise((resolve) => setTimeout(resolve, 50));
127
+ consoleSpy.mockRestore();
128
+ });
129
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,306 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { RequestProvider, useRequest } from "../../providers/RequestProvider";
5
+ import { createMockDtt, createMockRow, mockStrippingLine } from "../testUtils";
6
+ // ---------------------------------------------------------------------------
7
+ // Mock RowsProvider — each test mutates these to set up scenarios
8
+ // ---------------------------------------------------------------------------
9
+ let mockRows = [createMockRow()];
10
+ let mockConfiguredRows = mockRows.filter((r) => r.dtt);
11
+ const mockSetRows = vi.fn();
12
+ vi.mock("../../providers/RowsProvider", () => ({
13
+ useRows: () => ({
14
+ rows: mockRows,
15
+ configuredRows: mockConfiguredRows,
16
+ setRows: mockSetRows,
17
+ updateRow: vi.fn(),
18
+ removeRow: vi.fn(),
19
+ generateAllFiles: vi.fn(),
20
+ }),
21
+ }));
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+ function makeWrapper(emailIsKnown = false) {
26
+ return function Wrapper({ children }) {
27
+ return _jsx(RequestProvider, { emailIsKnown: emailIsKnown, children: children });
28
+ };
29
+ }
30
+ function renderProvider(emailIsKnown = false) {
31
+ return renderHook(() => useRequest(), { wrapper: makeWrapper(emailIsKnown) });
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // Validation: isEmptySession
35
+ // ---------------------------------------------------------------------------
36
+ describe("RequestProvider — isEmptySession", () => {
37
+ it("is true when no name, no emails and no rows", () => {
38
+ mockRows = [];
39
+ mockConfiguredRows = [];
40
+ const { result } = renderProvider();
41
+ expect(result.current.validation.isEmptySession).toBe(true);
42
+ });
43
+ it("is false when there is at least one row", () => {
44
+ mockRows = [createMockRow()];
45
+ mockConfiguredRows = [];
46
+ const { result } = renderProvider();
47
+ expect(result.current.validation.isEmptySession).toBe(false);
48
+ });
49
+ it("is false after a production name is entered", () => {
50
+ mockRows = [];
51
+ mockConfiguredRows = [];
52
+ const { result } = renderProvider();
53
+ act(() => result.current.setProductionName("TestProd"));
54
+ expect(result.current.validation.isEmptySession).toBe(false);
55
+ });
56
+ });
57
+ // ---------------------------------------------------------------------------
58
+ // Validation: isEmailValid
59
+ // ---------------------------------------------------------------------------
60
+ describe("RequestProvider — isEmailValid", () => {
61
+ it("is false when contact emails list is empty", () => {
62
+ mockRows = [];
63
+ mockConfiguredRows = [];
64
+ const { result } = renderProvider();
65
+ expect(result.current.validation.isEmailValid).toBe(false);
66
+ });
67
+ it("is true for a valid email address", () => {
68
+ mockRows = [];
69
+ mockConfiguredRows = [];
70
+ const { result } = renderProvider();
71
+ act(() => result.current.setContactEmails(["user@cern.ch"]));
72
+ expect(result.current.validation.isEmailValid).toBe(true);
73
+ });
74
+ it("is false when any email is invalid", () => {
75
+ mockRows = [];
76
+ mockConfiguredRows = [];
77
+ const { result } = renderProvider();
78
+ act(() => result.current.setContactEmails(["user@cern.ch", "notvalid"]));
79
+ expect(result.current.validation.isEmailValid).toBe(false);
80
+ });
81
+ });
82
+ // ---------------------------------------------------------------------------
83
+ // Validation: allRowsHaveDtt
84
+ // ---------------------------------------------------------------------------
85
+ describe("RequestProvider — allRowsHaveDtt", () => {
86
+ it("is false when no rows have a DTT", () => {
87
+ mockRows = [createMockRow({ dtt: null })];
88
+ mockConfiguredRows = [];
89
+ const { result } = renderProvider();
90
+ expect(result.current.validation.allRowsHaveDtt).toBe(false);
91
+ });
92
+ it("is true when all rows have a DTT", () => {
93
+ const row = createMockRow({ dtt: createMockDtt() });
94
+ mockRows = [row];
95
+ mockConfiguredRows = [row];
96
+ const { result } = renderProvider();
97
+ expect(result.current.validation.allRowsHaveDtt).toBe(true);
98
+ });
99
+ });
100
+ // ---------------------------------------------------------------------------
101
+ // Validation: allRowsHaveStrippingLine
102
+ // ---------------------------------------------------------------------------
103
+ describe("RequestProvider — allRowsHaveStrippingLine", () => {
104
+ it("is false when a row has no stripping line", () => {
105
+ mockRows = [createMockRow({ line: null })];
106
+ mockConfiguredRows = [];
107
+ const { result } = renderProvider();
108
+ expect(result.current.validation.allRowsHaveStrippingLine).toBe(false);
109
+ });
110
+ it("is true when all rows have a stripping line", () => {
111
+ mockRows = [createMockRow({ line: mockStrippingLine })];
112
+ mockConfiguredRows = [];
113
+ const { result } = renderProvider();
114
+ expect(result.current.validation.allRowsHaveStrippingLine).toBe(true);
115
+ });
116
+ });
117
+ // ---------------------------------------------------------------------------
118
+ // Validation: allRowsHavePaths
119
+ // ---------------------------------------------------------------------------
120
+ describe("RequestProvider — allRowsHavePaths", () => {
121
+ it("is false when a row has no paths", () => {
122
+ mockRows = [createMockRow({ paths: [] })];
123
+ mockConfiguredRows = [];
124
+ const { result } = renderProvider();
125
+ expect(result.current.validation.allRowsHavePaths).toBe(false);
126
+ });
127
+ it("is true when all rows have at least one path", () => {
128
+ mockRows = [createMockRow({ paths: ["/some/path/DIMUON.DST"] })];
129
+ mockConfiguredRows = [];
130
+ const { result } = renderProvider();
131
+ expect(result.current.validation.allRowsHavePaths).toBe(true);
132
+ });
133
+ });
134
+ // ---------------------------------------------------------------------------
135
+ // Validation: validDttNames
136
+ // ---------------------------------------------------------------------------
137
+ describe("RequestProvider — validDttNames", () => {
138
+ it("is true when there are no configured rows (vacuous truth)", () => {
139
+ mockRows = [];
140
+ mockConfiguredRows = [];
141
+ const { result } = renderProvider();
142
+ expect(result.current.validation.validDttNames).toBe(true);
143
+ });
144
+ it("is true when all DTT names are unique and non-empty", () => {
145
+ const row1 = createMockRow({ id: 0, dtt: createMockDtt("TreeA") });
146
+ const row2 = createMockRow({ id: 1, dtt: createMockDtt("TreeB") });
147
+ mockRows = [row1, row2];
148
+ mockConfiguredRows = [
149
+ row1,
150
+ row2,
151
+ ];
152
+ const { result } = renderProvider();
153
+ expect(result.current.validation.validDttNames).toBe(true);
154
+ });
155
+ it("is false when two rows have the same DTT name", () => {
156
+ const row1 = createMockRow({ id: 0, dtt: createMockDtt("SameName") });
157
+ const row2 = createMockRow({ id: 1, dtt: createMockDtt("SameName") });
158
+ mockRows = [row1, row2];
159
+ mockConfiguredRows = [
160
+ row1,
161
+ row2,
162
+ ];
163
+ const { result } = renderProvider();
164
+ expect(result.current.validation.validDttNames).toBe(false);
165
+ });
166
+ });
167
+ // ---------------------------------------------------------------------------
168
+ // trySubmit / showErrors
169
+ // ---------------------------------------------------------------------------
170
+ describe("RequestProvider — trySubmit / showErrors", () => {
171
+ it("showErrors is false before trySubmit is called", () => {
172
+ mockRows = [];
173
+ mockConfiguredRows = [];
174
+ const { result } = renderProvider();
175
+ expect(result.current.showErrors).toBe(false);
176
+ });
177
+ it("showErrors becomes true after trySubmit when form is incomplete", () => {
178
+ mockRows = [];
179
+ mockConfiguredRows = [];
180
+ const { result } = renderProvider();
181
+ act(() => void result.current.trySubmit());
182
+ expect(result.current.showErrors).toBe(true);
183
+ });
184
+ it("trySubmit returns false when form is not ready to submit", () => {
185
+ mockRows = [];
186
+ mockConfiguredRows = [];
187
+ const { result } = renderProvider();
188
+ let canSubmit = true;
189
+ act(() => {
190
+ canSubmit = result.current.trySubmit();
191
+ });
192
+ expect(canSubmit).toBe(false);
193
+ });
194
+ it("trySubmit returns true when emailIsKnown=true and form is otherwise complete", () => {
195
+ const row = createMockRow({
196
+ dtt: createMockDtt("MyTree"),
197
+ line: mockStrippingLine,
198
+ paths: ["/some/path/DIMUON.DST"],
199
+ });
200
+ mockRows = [row];
201
+ mockConfiguredRows = [row];
202
+ const { result } = renderProvider(true /* emailIsKnown */);
203
+ act(() => result.current.setProductionName("MyProd"));
204
+ let canSubmit = false;
205
+ act(() => {
206
+ canSubmit = result.current.trySubmit();
207
+ });
208
+ expect(canSubmit).toBe(true);
209
+ });
210
+ });
211
+ // ---------------------------------------------------------------------------
212
+ // clearAll
213
+ // ---------------------------------------------------------------------------
214
+ describe("RequestProvider — clearAll", () => {
215
+ it("resets productionName to empty string", () => {
216
+ mockRows = [];
217
+ mockConfiguredRows = [];
218
+ const { result } = renderProvider();
219
+ act(() => result.current.setProductionName("TestProd"));
220
+ act(() => result.current.clearAll());
221
+ expect(result.current.productionName).toBe("");
222
+ });
223
+ it("resets contactEmails to empty array", () => {
224
+ mockRows = [];
225
+ mockConfiguredRows = [];
226
+ const { result } = renderProvider();
227
+ act(() => result.current.setContactEmails(["a@b.com"]));
228
+ act(() => result.current.clearAll());
229
+ expect(result.current.contactEmails).toEqual([]);
230
+ });
231
+ it("resets showErrors to false", () => {
232
+ mockRows = [];
233
+ mockConfiguredRows = [];
234
+ const { result } = renderProvider();
235
+ act(() => void result.current.trySubmit());
236
+ act(() => result.current.clearAll());
237
+ expect(result.current.showErrors).toBe(false);
238
+ });
239
+ });
240
+ // ---------------------------------------------------------------------------
241
+ // localStorage persistence
242
+ // ---------------------------------------------------------------------------
243
+ describe("RequestProvider — localStorage", () => {
244
+ it("persists productionName to localStorage on update", () => {
245
+ mockRows = [];
246
+ mockConfiguredRows = [];
247
+ const { result } = renderProvider();
248
+ act(() => result.current.setProductionName("Persisted"));
249
+ expect(localStorage.getItem("name")).toBe("Persisted");
250
+ });
251
+ it("persists contactEmails to localStorage on update", () => {
252
+ mockRows = [];
253
+ mockConfiguredRows = [];
254
+ const { result } = renderProvider();
255
+ act(() => result.current.setContactEmails(["x@y.com"]));
256
+ expect(localStorage.getItem("email")).toBe("x@y.com");
257
+ });
258
+ it("reads initial productionName from localStorage", () => {
259
+ localStorage.setItem("name", "FromStorage");
260
+ mockRows = [];
261
+ mockConfiguredRows = [];
262
+ const { result } = renderProvider();
263
+ expect(result.current.productionName).toBe("FromStorage");
264
+ });
265
+ it("persists reasonForRequest to localStorage on update", () => {
266
+ mockRows = [];
267
+ mockConfiguredRows = [];
268
+ const { result } = renderProvider();
269
+ act(() => result.current.setReasonForRequest("my reason"));
270
+ expect(localStorage.getItem("reasonForRequest")).toBe("my reason");
271
+ });
272
+ });
273
+ // ---------------------------------------------------------------------------
274
+ // reasonForRequest
275
+ // ---------------------------------------------------------------------------
276
+ describe("RequestProvider — reasonForRequest", () => {
277
+ it("starts with empty reasonForRequest", () => {
278
+ mockRows = [];
279
+ mockConfiguredRows = [];
280
+ const { result } = renderProvider();
281
+ expect(result.current.reasonForRequest).toBe("");
282
+ });
283
+ it("updates reasonForRequest via setReasonForRequest", () => {
284
+ mockRows = [];
285
+ mockConfiguredRows = [];
286
+ const { result } = renderProvider();
287
+ act(() => result.current.setReasonForRequest("I need these for research"));
288
+ expect(result.current.reasonForRequest).toBe("I need these for research");
289
+ });
290
+ it("resets reasonForRequest on clearAll", () => {
291
+ mockRows = [];
292
+ mockConfiguredRows = [];
293
+ const { result } = renderProvider();
294
+ act(() => result.current.setReasonForRequest("some reason"));
295
+ act(() => result.current.clearAll());
296
+ expect(result.current.reasonForRequest).toBe("");
297
+ });
298
+ });
299
+ // ---------------------------------------------------------------------------
300
+ // useRequest outside provider
301
+ // ---------------------------------------------------------------------------
302
+ describe("useRequest outside provider", () => {
303
+ it("throws when used outside a RequestProvider", () => {
304
+ expect(() => renderHook(() => useRequest())).toThrow("useRequest must be used within a RequestProvider");
305
+ });
306
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { renderHook } from "@testing-library/react";
4
+ import { RowProvider, useRow } from "../../providers/RowProvider";
5
+ import { createMockRow, mockStrippingLine } from "../testUtils";
6
+ const mockUpdateRow = vi.fn();
7
+ vi.mock("../../providers/RowsProvider", () => ({
8
+ useRows: () => ({
9
+ rows: [],
10
+ configuredRows: [],
11
+ setRows: vi.fn(),
12
+ updateRow: mockUpdateRow,
13
+ removeRow: vi.fn(),
14
+ generateAllFiles: vi.fn(),
15
+ }),
16
+ }));
17
+ function makeWrapper(row) {
18
+ return function Wrapper({ children }) {
19
+ return _jsx(RowProvider, { row: row, children: children });
20
+ };
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // validateBkPath
24
+ // ---------------------------------------------------------------------------
25
+ const VALID_PATH = "/LHCb/Collision16/Beam6500GeV-VeloClosed-MagDown/Real Data/Reco16/Stripping28r2/90000000/DIMUON.DST";
26
+ describe("RowProvider — validateBkPath", () => {
27
+ it("returns true for a valid path when no stripping line is set", () => {
28
+ const row = createMockRow({ line: null });
29
+ const { result } = renderHook(() => useRow(), { wrapper: makeWrapper(row) });
30
+ expect(result.current.validateBkPath(VALID_PATH)).toBe(true);
31
+ });
32
+ it("returns true when path matches the line's stream and version", () => {
33
+ const row = createMockRow({ line: mockStrippingLine });
34
+ const { result } = renderHook(() => useRow(), { wrapper: makeWrapper(row) });
35
+ // fileName is "DIMUON.DST" → base is "dimuon", stream is "leptonic" → must match allstreams or stream name
36
+ // Actually DIMUON.DST fileName "DIMUON" doesn't match "leptonic"
37
+ // Let me check... The validation: ["allstreams", streamName].includes(fileName)
38
+ // streamName = "leptonic", fileName = "dimuon" → NOT a match
39
+ // Wait this might return false. Let me re-check the logic:
40
+ // validateBkPathsForLines checks: ["allstreams", streamName].includes(fileName)
41
+ // DIMUON.DST → fileName = "DIMUON.DST", fileName.split(".")[0].toLowerCase() = "dimuon"
42
+ // streamName = line.stream.toLowerCase() = "leptonic"
43
+ // ["allstreams", "leptonic"].includes("dimuon") → false
44
+ // So DIMUON.DST does NOT match Leptonic stream. Use ALLSTREAMS.DST instead.
45
+ const allstreamsPath = "/LHCb/Collision16/Beam6500GeV-VeloClosed-MagDown/Real Data/Reco16/Stripping28r2/90000000/ALLSTREAMS.DST";
46
+ expect(result.current.validateBkPath(allstreamsPath)).toBe(true);
47
+ });
48
+ it("returns false for a path with wrong stripping version", () => {
49
+ const row = createMockRow({ line: mockStrippingLine });
50
+ const { result } = renderHook(() => useRow(), { wrapper: makeWrapper(row) });
51
+ // mockStrippingLine has versions: ["28r2"], wrong version "21"
52
+ const wrongVersionAllstreams = "/LHCb/Collision16/Beam6500GeV-VeloClosed-MagDown/Real Data/Reco16/Stripping21/90000000/ALLSTREAMS.DST";
53
+ expect(result.current.validateBkPath(wrongVersionAllstreams)).toBe(false);
54
+ });
55
+ it("returns false for an invalid path format", () => {
56
+ const row = createMockRow({ line: mockStrippingLine });
57
+ const { result } = renderHook(() => useRow(), { wrapper: makeWrapper(row) });
58
+ expect(result.current.validateBkPath("not-a-valid-path")).toBe(false);
59
+ });
60
+ it("returns true for an ALLSTREAMS path matching the version", () => {
61
+ const row = createMockRow({ line: mockStrippingLine });
62
+ const { result } = renderHook(() => useRow(), { wrapper: makeWrapper(row) });
63
+ const allstreams = "/LHCb/Collision16/Beam6500GeV-VeloClosed-MagDown/Real Data/Reco16/Stripping28r2/90000000/ALLSTREAMS.DST";
64
+ expect(result.current.validateBkPath(allstreams)).toBe(true);
65
+ });
66
+ it("exposes the row from context", () => {
67
+ const row = createMockRow({ id: 42 });
68
+ const { result } = renderHook(() => useRow(), { wrapper: makeWrapper(row) });
69
+ expect(result.current.row.id).toBe(42);
70
+ });
71
+ });
72
+ // ---------------------------------------------------------------------------
73
+ // updateBkPaths
74
+ // ---------------------------------------------------------------------------
75
+ describe("RowProvider — updateBkPaths", () => {
76
+ it("calls updateRow when updateBkPaths is invoked", () => {
77
+ const row = createMockRow({ line: mockStrippingLine });
78
+ const { result } = renderHook(() => useRow(), { wrapper: makeWrapper(row) });
79
+ const allstreams = "/LHCb/Collision16/Beam6500GeV-VeloClosed-MagDown/Real Data/Reco16/Stripping28r2/90000000/ALLSTREAMS.DST";
80
+ result.current.updateBkPaths([allstreams]);
81
+ expect(mockUpdateRow).toHaveBeenCalledOnce();
82
+ });
83
+ it("updateRow callback filters paths using validateBkPath", () => {
84
+ // Make mockUpdateRow actually invoke the callback so the inner lines are covered
85
+ mockUpdateRow.mockImplementation((_id, fn) => {
86
+ const fakeRow = {
87
+ id: 5,
88
+ decay: {},
89
+ dtt: null,
90
+ line: mockStrippingLine,
91
+ paths: [],
92
+ pathOptions: [],
93
+ };
94
+ fn(fakeRow);
95
+ });
96
+ const row = createMockRow({ line: mockStrippingLine });
97
+ const { result } = renderHook(() => useRow(), { wrapper: makeWrapper(row) });
98
+ const allstreams = "/LHCb/Collision16/Beam6500GeV-VeloClosed-MagDown/Real Data/Reco16/Stripping28r2/90000000/ALLSTREAMS.DST";
99
+ result.current.updateBkPaths([allstreams]);
100
+ expect(mockUpdateRow).toHaveBeenCalled();
101
+ });
102
+ });
103
+ // ---------------------------------------------------------------------------
104
+ // useRow outside provider
105
+ // ---------------------------------------------------------------------------
106
+ describe("useRow outside provider", () => {
107
+ it("throws when used outside a RowProvider", () => {
108
+ expect(() => renderHook(() => useRow())).toThrow("useRow must be used within a RowProvider");
109
+ });
110
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { RowsProvider, useRows } from "../../providers/RowsProvider";
5
+ import { createMockDtt, createMockRow } from "../testUtils";
6
+ function makeWrapper() {
7
+ return function Wrapper({ children }) {
8
+ return _jsx(RowsProvider, { children: children });
9
+ };
10
+ }
11
+ function renderProvider() {
12
+ return renderHook(() => useRows(), { wrapper: makeWrapper() });
13
+ }
14
+ describe("RowsProvider", () => {
15
+ it("starts with an empty rows list when localStorage has no rows", () => {
16
+ localStorage.removeItem("rows");
17
+ const { result } = renderProvider();
18
+ expect(result.current.rows).toEqual([]);
19
+ });
20
+ it("hydrates rows from localStorage on mount", () => {
21
+ const row = createMockRow({ id: 99 });
22
+ localStorage.setItem("rows", JSON.stringify([{ ...row, dtt: null }]));
23
+ const { result } = renderProvider();
24
+ expect(result.current.rows[0].id).toBe(99);
25
+ localStorage.removeItem("rows");
26
+ });
27
+ it("reconstructs Dtt from config when stored row has a dtt", () => {
28
+ const row = createMockRow({ id: 200, dtt: createMockDtt("StoredTree") });
29
+ localStorage.setItem("rows", JSON.stringify([row]));
30
+ const { result } = renderProvider();
31
+ expect(result.current.rows[0].dtt).not.toBeNull();
32
+ localStorage.removeItem("rows");
33
+ });
34
+ it("updateRow modifies the matching row", () => {
35
+ localStorage.removeItem("rows");
36
+ const { result } = renderProvider();
37
+ act(() => result.current.setRows([createMockRow({ id: 1 })]));
38
+ act(() => result.current.updateRow(1, (r) => ({ ...r, id: 99 })));
39
+ expect(result.current.rows[0].id).toBe(99);
40
+ });
41
+ it("updateRow does not modify rows with a different id", () => {
42
+ localStorage.removeItem("rows");
43
+ const { result } = renderProvider();
44
+ act(() => result.current.setRows([createMockRow({ id: 1 }), createMockRow({ id: 2 })]));
45
+ act(() => result.current.updateRow(1, (r) => ({ ...r, id: 99 })));
46
+ expect(result.current.rows[1].id).toBe(2);
47
+ });
48
+ it("removeRow filters out the row with the given id", () => {
49
+ localStorage.removeItem("rows");
50
+ const { result } = renderProvider();
51
+ act(() => result.current.setRows([createMockRow({ id: 1 }), createMockRow({ id: 2 })]));
52
+ act(() => result.current.removeRow(1));
53
+ expect(result.current.rows).toHaveLength(1);
54
+ expect(result.current.rows[0].id).toBe(2);
55
+ });
56
+ it("configuredRows contains only rows with a dtt", () => {
57
+ localStorage.removeItem("rows");
58
+ const { result } = renderProvider();
59
+ act(() => result.current.setRows([createMockRow({ id: 1, dtt: null }), createMockRow({ id: 2, dtt: createMockDtt() })]));
60
+ expect(result.current.configuredRows).toHaveLength(1);
61
+ expect(result.current.configuredRows[0].id).toBe(2);
62
+ });
63
+ it("generateAllFiles returns one file per configured row plus info.yaml", () => {
64
+ localStorage.removeItem("rows");
65
+ const { result } = renderProvider();
66
+ act(() => result.current.setRows([
67
+ createMockRow({ id: 1, dtt: createMockDtt("TreeA") }),
68
+ createMockRow({ id: 2, dtt: createMockDtt("TreeB") }),
69
+ ]));
70
+ const files = result.current.generateAllFiles();
71
+ // 2 dtt files + 1 info.yaml
72
+ expect(files).toHaveLength(3);
73
+ });
74
+ it("persists rows to localStorage when rows change", () => {
75
+ localStorage.removeItem("rows");
76
+ const { result } = renderProvider();
77
+ act(() => result.current.setRows([createMockRow({ id: 7 })]));
78
+ const stored = JSON.parse(localStorage.getItem("rows") ?? "[]");
79
+ expect(stored[0].id).toBe(7);
80
+ });
81
+ it("throws when useRows is used outside the provider", () => {
82
+ expect(() => renderHook(() => useRows())).toThrow("useRows must be used within a RowsProvider");
83
+ });
84
+ });