datajunction-ui 0.0.1-rc.19 → 0.0.1-rc.20

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 (66) hide show
  1. package/package.json +6 -5
  2. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  3. package/src/app/components/DeleteNode.jsx +79 -0
  4. package/src/app/components/ListGroupItem.jsx +8 -1
  5. package/src/app/components/QueryInfo.jsx +4 -4
  6. package/src/app/components/Tab.jsx +9 -1
  7. package/src/app/components/ToggleSwitch.jsx +3 -0
  8. package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
  9. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  10. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  11. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +3 -0
  12. package/src/app/components/djgraph/DJNodeColumns.jsx +4 -1
  13. package/src/app/components/djgraph/DJNodeDimensions.jsx +9 -2
  14. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  15. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  16. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  17. package/src/app/index.tsx +6 -0
  18. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +15 -2
  19. package/src/app/pages/AddEditNodePage/FullNameField.jsx +2 -1
  20. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +77 -0
  21. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +93 -0
  22. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +34 -3
  23. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +4 -2
  24. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +53 -0
  25. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +53 -0
  26. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +0 -81
  27. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +82 -257
  28. package/src/app/pages/AddEditNodePage/index.jsx +13 -5
  29. package/src/app/pages/LoginPage/__tests__/index.test.jsx +70 -0
  30. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +95 -0
  31. package/src/app/pages/NamespacePage/index.jsx +8 -5
  32. package/src/app/pages/NodePage/ClientCodePopover.jsx +3 -1
  33. package/src/app/pages/NodePage/EditColumnPopover.jsx +102 -0
  34. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +135 -0
  35. package/src/app/pages/NodePage/NodeColumnTab.jsx +80 -17
  36. package/src/app/pages/NodePage/NodeHistory.jsx +63 -30
  37. package/src/app/pages/NodePage/NodeInfoTab.jsx +52 -7
  38. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +50 -27
  39. package/src/app/pages/NodePage/NodeSQLTab.jsx +0 -10
  40. package/src/app/pages/NodePage/NodesWithDimension.jsx +4 -2
  41. package/src/app/pages/NodePage/__tests__/ClientCodePopover.test.jsx +49 -0
  42. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
  43. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +165 -0
  44. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +591 -0
  45. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +57 -0
  46. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +725 -0
  47. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  48. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +402 -0
  49. package/src/app/pages/NodePage/index.jsx +22 -6
  50. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  51. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  52. package/src/app/pages/RegisterTablePage/index.jsx +163 -0
  53. package/src/app/pages/Root/__tests__/index.test.jsx +77 -0
  54. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  55. package/src/app/pages/SQLBuilderPage/index.jsx +61 -43
  56. package/src/app/services/DJService.js +125 -54
  57. package/src/app/services/__tests__/DJService.test.jsx +609 -0
  58. package/src/mocks/mockNodes.jsx +1397 -0
  59. package/src/setupTests.ts +30 -0
  60. package/src/styles/index.css +43 -0
  61. package/src/styles/node-creation.scss +7 -0
  62. package/src/utils/form.jsx +23 -0
  63. package/.github/pull_request_template.md +0 -11
  64. package/.github/workflows/ci.yml +0 -33
  65. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -118
  66. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
@@ -8,8 +8,10 @@ export default function NodeMaterializationTab({ node, djClient }) {
8
8
  const [materializations, setMaterializations] = useState([]);
9
9
  useEffect(() => {
10
10
  const fetchData = async () => {
11
- const data = await djClient.materializations(node.name);
12
- setMaterializations(data);
11
+ if (node) {
12
+ const data = await djClient.materializations(node.name);
13
+ setMaterializations(data);
14
+ }
13
15
  };
14
16
  fetchData().catch(console.error);
15
17
  }, [djClient, node]);
@@ -35,7 +37,7 @@ export default function NodeMaterializationTab({ node, djClient }) {
35
37
 
36
38
  const materializationRows = materializations => {
37
39
  return materializations.map(materialization => (
38
- <tr>
40
+ <tr key={materialization.name}>
39
41
  <td className="text-start node_name">
40
42
  <a href={materialization.urls[0]}>{materialization.name}</a>
41
43
  <ClientCodePopover code={materialization.clientCode} />
@@ -53,20 +55,25 @@ export default function NodeMaterializationTab({ node, djClient }) {
53
55
  {materialization.config.partitions ? (
54
56
  materialization.config.partitions.map(partition =>
55
57
  partition.type_ === 'categorical' ? (
56
- <div className="partition__full">
58
+ <div className="partition__full" key={partition.name}>
57
59
  <div className="partition__header">{partition.name}</div>
58
60
  <div className="partition__body">
59
61
  {partition.values !== null && partition.values.length > 0
60
62
  ? partition.values.map(val => (
61
- <span className="badge partition_value">{val}</span>
63
+ <span
64
+ className="badge partition_value"
65
+ key={`partition-value-${val}`}
66
+ >
67
+ {val}
68
+ </span>
62
69
  ))
63
70
  : null}
64
71
  {partition.range !== null && partition.range.length > 0
65
72
  ? rangePartition(partition)
66
73
  : null}
67
74
  {(partition.range === null && partition.values === null) ||
68
- (partition.range.length === 0 &&
69
- partition.values.length === 0) ? (
75
+ (partition.range?.length === 0 &&
76
+ partition.values?.length === 0) ? (
70
77
  <span className={`badge partition_value_highlight`}>
71
78
  ALL
72
79
  </span>
@@ -81,7 +88,7 @@ export default function NodeMaterializationTab({ node, djClient }) {
81
88
  </td>
82
89
  <td>
83
90
  {materialization.output_tables.map(table => (
84
- <div className={`table__full`}>
91
+ <div className={`table__full`} key={table}>
85
92
  <div className="table__header">
86
93
  <TableIcon />{' '}
87
94
  <span className={`entity-info`}>
@@ -100,7 +107,7 @@ export default function NodeMaterializationTab({ node, djClient }) {
100
107
  {materialization.config.partitions ? (
101
108
  materialization.config.partitions.map(partition =>
102
109
  partition.type_ === 'temporal' ? (
103
- <div className="partition__full">
110
+ <div className="partition__full" key={partition.name}>
104
111
  <div className="partition__header">{partition.name}</div>
105
112
  <div className="partition__body">
106
113
  {partition.values !== null && partition.values.length > 0
@@ -121,7 +128,9 @@ export default function NodeMaterializationTab({ node, djClient }) {
121
128
  </td>
122
129
  <td>
123
130
  {materialization.urls.map((url, idx) => (
124
- <a href={url}>[{idx + 1}]</a>
131
+ <a href={url} key={`url-${idx}`}>
132
+ [{idx + 1}]
133
+ </a>
125
134
  ))}
126
135
  </td>
127
136
  </tr>
@@ -129,23 +138,37 @@ export default function NodeMaterializationTab({ node, djClient }) {
129
138
  };
130
139
  return (
131
140
  <div className="table-responsive">
132
- <table className="card-inner-table table">
133
- <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
134
- <th className="text-start">Name</th>
135
- <th>Schedule</th>
136
- <th>Engine</th>
137
- <th>Partitions</th>
138
- <th>Output Tables</th>
139
- <th>Backfills</th>
140
- <th>URLs</th>
141
- </thead>
142
- {materializationRows(
143
- materializations.filter(
144
- materialization =>
145
- !(materialization.name === 'default' && node.type === 'cube'),
146
- ),
147
- )}
148
- </table>
141
+ {materializations.length > 0 ? (
142
+ <table
143
+ className="card-inner-table table"
144
+ aria-label="Materializations"
145
+ aria-hidden="false"
146
+ >
147
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
148
+ <tr>
149
+ <th className="text-start">Name</th>
150
+ <th>Schedule</th>
151
+ <th>Engine</th>
152
+ <th>Partitions</th>
153
+ <th>Output Tables</th>
154
+ <th>Backfills</th>
155
+ <th>URLs</th>
156
+ </tr>
157
+ </thead>
158
+ <tbody>
159
+ {materializationRows(
160
+ materializations.filter(
161
+ materialization =>
162
+ !(materialization.name === 'default' && node.type === 'cube'),
163
+ ),
164
+ )}
165
+ </tbody>
166
+ </table>
167
+ ) : (
168
+ <div className="message alert" style={{ marginTop: '10px' }}>
169
+ No materializations available for this node
170
+ </div>
171
+ )}
149
172
  </div>
150
173
  );
151
174
  }
@@ -27,16 +27,6 @@ const NodeSQLTab = djNode => {
27
27
  }))
28
28
  : [''];
29
29
 
30
- // const options = [
31
- // { value: '>=', label: '>=' },
32
- // { value: '<=', label: '<=' },
33
- // { value: '>', label: '>' },
34
- // { value: '<', label: '<' },
35
- // { value: '=', label: '=' },
36
- // { value: '!=', label: '!=' },
37
- // { value: 'IN', label: 'IN' },
38
- // ];
39
-
40
30
  const handleSubmit = event => {
41
31
  event.preventDefault();
42
32
  };
@@ -15,8 +15,10 @@ export default function NodesWithDimension({ node, djClient }) {
15
15
  <div className="table-responsive">
16
16
  <table className="card-inner-table table">
17
17
  <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
18
- <th className="text-start">Name</th>
19
- <th>Type</th>
18
+ <tr>
19
+ <th className="text-start">Name</th>
20
+ <th>Type</th>
21
+ </tr>
20
22
  </thead>
21
23
  <tbody>
22
24
  {availableNodes.map(node => (
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen, waitFor } from '@testing-library/react';
3
+ import ClientCodePopover from '../ClientCodePopover';
4
+ import userEvent from '@testing-library/user-event';
5
+
6
+ describe('<ClientCodePopover />', () => {
7
+ const defaultProps = {
8
+ code: "print('Hello, World!')",
9
+ };
10
+
11
+ it('toggles the code popover visibility when the button is clicked', async () => {
12
+ render(<ClientCodePopover {...defaultProps} />);
13
+
14
+ const button = screen.getByRole('button', 'code-button');
15
+
16
+ // Initially, the popover should be hidden
17
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
18
+ 'display: none',
19
+ );
20
+
21
+ // Clicking the button should display the popover
22
+ fireEvent.click(button);
23
+ expect(screen.getByRole('dialog', { hidden: true })).not.toHaveStyle(
24
+ 'display: none',
25
+ );
26
+
27
+ // Clicking the button again should hide the popover
28
+ fireEvent.click(button);
29
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
30
+ 'display: none',
31
+ );
32
+
33
+ // Trigger onClose by pressing <escape>
34
+ userEvent.keyboard('{Escape}');
35
+ // fireEvent.click(screen.getByTestId('body').firstChild());
36
+ await waitFor(() => {
37
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveStyle(
38
+ 'display: none',
39
+ );
40
+ });
41
+ });
42
+
43
+ it('renders the provided code within the SyntaxHighlighter', () => {
44
+ render(<ClientCodePopover {...defaultProps} />);
45
+ expect(screen.getByRole('dialog', { hidden: true })).toHaveTextContent(
46
+ defaultProps.code,
47
+ );
48
+ });
49
+ });
@@ -0,0 +1,148 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, waitFor, screen } from '@testing-library/react';
3
+ import EditColumnPopover from '../EditColumnPopover';
4
+ import DJClientContext from '../../../providers/djclient';
5
+
6
+ const mockDjClient = {
7
+ DataJunctionAPI: {
8
+ setAttributes: jest.fn(),
9
+ },
10
+ };
11
+
12
+ describe('<EditColumnPopover />', () => {
13
+ it('renders correctly and handles form submission', async () => {
14
+ // Mock necessary data
15
+ const column = {
16
+ name: 'column1',
17
+ dimension: { name: 'dimension1' },
18
+ attributes: [
19
+ { attribute_type: { name: 'primary_key', namespace: 'system' } },
20
+ ],
21
+ };
22
+ const node = { name: 'default.node1' };
23
+ const options = [
24
+ { value: 'dimension', label: 'Dimension' },
25
+ { value: 'primary_key', label: 'Primary Key' },
26
+ ];
27
+
28
+ // Mock onSubmit function
29
+ const onSubmitMock = jest.fn();
30
+
31
+ mockDjClient.DataJunctionAPI.setAttributes.mockReturnValue({
32
+ status: 201,
33
+ json: [
34
+ {
35
+ name: 'id',
36
+ type: 'int',
37
+ attributes: [
38
+ { attribute_type: { name: 'primary_key', namespace: 'system' } },
39
+ ],
40
+ dimension: null,
41
+ },
42
+ ],
43
+ });
44
+
45
+ // Render the component
46
+ const { getByLabelText, getByText, getByTestId } = render(
47
+ <DJClientContext.Provider value={mockDjClient}>
48
+ <EditColumnPopover
49
+ column={column}
50
+ node={node}
51
+ options={options}
52
+ onSubmit={onSubmitMock}
53
+ />
54
+ </DJClientContext.Provider>,
55
+ );
56
+
57
+ // Open the popover
58
+ fireEvent.click(getByLabelText('EditColumn'));
59
+
60
+ // Click on one attribute in the select
61
+ const editAttributes = getByTestId('edit-attributes');
62
+ fireEvent.keyDown(editAttributes.firstChild, { key: 'ArrowDown' });
63
+ fireEvent.click(screen.getByText('Dimension'));
64
+ fireEvent.click(getByText('Save'));
65
+ getByText('Save').click();
66
+
67
+ // Expect setAttributes to be called
68
+ await waitFor(() => {
69
+ expect(mockDjClient.DataJunctionAPI.setAttributes).toHaveBeenCalled();
70
+ expect(getByText('Saved!')).toBeInTheDocument();
71
+ });
72
+
73
+ // Click on two attributes in the select
74
+ fireEvent.keyDown(editAttributes.firstChild, { key: 'ArrowDown' });
75
+ fireEvent.click(screen.getByText('Dimension'));
76
+ fireEvent.click(screen.getByText('Primary Key'));
77
+ fireEvent.click(getByText('Save'));
78
+ getByText('Save').click();
79
+
80
+ // Expect setAttributes to be called
81
+ await waitFor(() => {
82
+ expect(mockDjClient.DataJunctionAPI.setAttributes).toHaveBeenCalledWith(
83
+ 'default.node1',
84
+ 'column1',
85
+ ['primary_key', 'dimension'],
86
+ );
87
+ expect(getByText('Saved!')).toBeInTheDocument();
88
+ });
89
+
90
+ // Close the popover
91
+ fireEvent.click(getByLabelText('EditColumn'));
92
+ });
93
+
94
+ it('handles failed form submission', async () => {
95
+ // Mock necessary data
96
+ const column = {
97
+ name: 'column1',
98
+ dimension: { name: 'dimension1' },
99
+ attributes: [
100
+ { attribute_type: { name: 'primary_key', namespace: 'system' } },
101
+ ],
102
+ };
103
+ const node = { name: 'default.node1' };
104
+ const options = [
105
+ { value: 'dimension', label: 'Dimension' },
106
+ { value: 'primary_key', label: 'Primary Key' },
107
+ ];
108
+
109
+ // Mock onSubmit function
110
+ const onSubmitMock = jest.fn();
111
+
112
+ mockDjClient.DataJunctionAPI.setAttributes.mockReturnValue({
113
+ status: 500,
114
+ json: { message: 'bad request' },
115
+ });
116
+
117
+ // Render the component
118
+ const { getByLabelText, getByText, getByTestId } = render(
119
+ <DJClientContext.Provider value={mockDjClient}>
120
+ <EditColumnPopover
121
+ column={column}
122
+ node={node}
123
+ options={options}
124
+ onSubmit={onSubmitMock}
125
+ />
126
+ </DJClientContext.Provider>,
127
+ );
128
+
129
+ // Open the popover
130
+ fireEvent.click(getByLabelText('EditColumn'));
131
+
132
+ // Click on one attribute in the select
133
+ const editAttributes = getByTestId('edit-attributes');
134
+ fireEvent.keyDown(editAttributes.firstChild, { key: 'ArrowDown' });
135
+ fireEvent.click(screen.getByText('Dimension'));
136
+ fireEvent.click(getByText('Save'));
137
+ getByText('Save').click();
138
+
139
+ // Expect setAttributes to be called and the failure message to show up
140
+ await waitFor(() => {
141
+ expect(mockDjClient.DataJunctionAPI.setAttributes).toHaveBeenCalled();
142
+ expect(getByText('bad request')).toBeInTheDocument();
143
+ });
144
+
145
+ // Close the popover
146
+ fireEvent.click(getByLabelText('EditColumn'));
147
+ });
148
+ });
@@ -0,0 +1,165 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, waitFor, screen } from '@testing-library/react';
3
+ import LinkDimensionPopover from '../LinkDimensionPopover';
4
+ import DJClientContext from '../../../providers/djclient';
5
+
6
+ const mockDjClient = {
7
+ DataJunctionAPI: {
8
+ linkDimension: jest.fn(),
9
+ unlinkDimension: jest.fn(),
10
+ },
11
+ };
12
+
13
+ describe('<LinkDimensionPopover />', () => {
14
+ it('renders correctly and handles form submission', async () => {
15
+ // Mock necessary data
16
+ const column = {
17
+ name: 'column1',
18
+ dimension: { name: 'default.dimension1' },
19
+ };
20
+ const node = { name: 'default.node1' };
21
+ const options = [
22
+ { value: 'Remove', label: '[Remove dimension link]' },
23
+ { value: 'default.dimension1', label: 'Dimension 1' },
24
+ { value: 'default.dimension2', label: 'Dimension 2' },
25
+ ];
26
+
27
+ // Mock onSubmit function
28
+ const onSubmitMock = jest.fn();
29
+
30
+ mockDjClient.DataJunctionAPI.linkDimension.mockReturnValue({
31
+ status: 200,
32
+ json: { message: 'Success' },
33
+ });
34
+
35
+ mockDjClient.DataJunctionAPI.unlinkDimension.mockReturnValue({
36
+ status: 200,
37
+ json: { message: 'Success' },
38
+ });
39
+
40
+ // Render the component
41
+ const { getByLabelText, getByText, getByTestId } = render(
42
+ <DJClientContext.Provider value={mockDjClient}>
43
+ <LinkDimensionPopover
44
+ column={column}
45
+ node={node}
46
+ options={options}
47
+ onSubmit={onSubmitMock}
48
+ />
49
+ </DJClientContext.Provider>,
50
+ );
51
+
52
+ // Open the popover
53
+ fireEvent.click(getByLabelText('LinkDimension'));
54
+
55
+ // Click on a dimension and save
56
+ const linkDimension = getByTestId('link-dimension');
57
+ fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
58
+ fireEvent.click(screen.getByText('Dimension 1'));
59
+ fireEvent.click(getByText('Save'));
60
+ getByText('Save').click();
61
+
62
+ // Expect linkDimension to be called
63
+ await waitFor(() => {
64
+ expect(mockDjClient.DataJunctionAPI.linkDimension).toHaveBeenCalledWith(
65
+ 'default.node1',
66
+ 'column1',
67
+ 'default.dimension1',
68
+ );
69
+ expect(getByText('Saved!')).toBeInTheDocument();
70
+ });
71
+
72
+ // Click on the 'Remove' option and save
73
+ fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
74
+ fireEvent.click(screen.getByText('[Remove dimension link]'));
75
+ fireEvent.click(getByText('Save'));
76
+ getByText('Save').click();
77
+
78
+ // Expect unlinkDimension to be called
79
+ await waitFor(() => {
80
+ expect(mockDjClient.DataJunctionAPI.unlinkDimension).toHaveBeenCalledWith(
81
+ 'default.node1',
82
+ 'column1',
83
+ 'default.dimension1',
84
+ );
85
+ expect(getByText('Removed dimension link!')).toBeInTheDocument();
86
+ });
87
+ });
88
+
89
+ it('handles failed form submission', async () => {
90
+ // Mock necessary data
91
+ const column = {
92
+ name: 'column1',
93
+ dimension: { name: 'default.dimension1' },
94
+ };
95
+ const node = { name: 'default.node1' };
96
+ const options = [
97
+ { value: 'Remove', label: '[Remove dimension link]' },
98
+ { value: 'default.dimension1', label: 'Dimension 1' },
99
+ { value: 'default.dimension2', label: 'Dimension 2' },
100
+ ];
101
+
102
+ // Mock onSubmit function
103
+ const onSubmitMock = jest.fn();
104
+
105
+ mockDjClient.DataJunctionAPI.linkDimension.mockReturnValue({
106
+ status: 500,
107
+ json: { message: 'Failed due to nonexistent dimension' },
108
+ });
109
+
110
+ mockDjClient.DataJunctionAPI.unlinkDimension.mockReturnValue({
111
+ status: 500,
112
+ json: { message: 'Failed due to no dimension link' },
113
+ });
114
+
115
+ // Render the component
116
+ const { getByLabelText, getByText, getByTestId } = render(
117
+ <DJClientContext.Provider value={mockDjClient}>
118
+ <LinkDimensionPopover
119
+ column={column}
120
+ node={node}
121
+ options={options}
122
+ onSubmit={onSubmitMock}
123
+ />
124
+ </DJClientContext.Provider>,
125
+ );
126
+
127
+ // Open the popover
128
+ fireEvent.click(getByLabelText('LinkDimension'));
129
+
130
+ // Click on a dimension and save
131
+ const linkDimension = getByTestId('link-dimension');
132
+ fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
133
+ fireEvent.click(screen.getByText('Dimension 1'));
134
+ fireEvent.click(getByText('Save'));
135
+ getByText('Save').click();
136
+
137
+ // Expect linkDimension to be called
138
+ await waitFor(() => {
139
+ expect(mockDjClient.DataJunctionAPI.linkDimension).toHaveBeenCalledWith(
140
+ 'default.node1',
141
+ 'column1',
142
+ 'default.dimension1',
143
+ );
144
+ expect(
145
+ getByText('Failed due to nonexistent dimension'),
146
+ ).toBeInTheDocument();
147
+ });
148
+
149
+ // Click on the 'Remove' option and save
150
+ fireEvent.keyDown(linkDimension.firstChild, { key: 'ArrowDown' });
151
+ fireEvent.click(screen.getByText('[Remove dimension link]'));
152
+ fireEvent.click(getByText('Save'));
153
+ getByText('Save').click();
154
+
155
+ // Expect unlinkDimension to be called
156
+ await waitFor(() => {
157
+ expect(mockDjClient.DataJunctionAPI.unlinkDimension).toHaveBeenCalledWith(
158
+ 'default.node1',
159
+ 'column1',
160
+ 'default.dimension1',
161
+ );
162
+ expect(getByText('Failed due to no dimension link')).toBeInTheDocument();
163
+ });
164
+ });
165
+ });