Skip to content

Commit cf754ec

Browse files
Merge branch 'main' into installation-state-manager-resource-check
2 parents f8d2033 + 9614d14 commit cf754ec

36 files changed

+2625
-65
lines changed

cypress/cypress.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference types="cypress" />
2+
3+
declare namespace Cypress {
4+
interface Chainable {
5+
/**
6+
* Custom command to mount a React component with common providers
7+
* @param component - React component to mount
8+
* @param options - Options for mount, may include initializeRecoil function
9+
* @example cy.mount(<MyComponent />, { initializeRecoil: (snapshot) => {...} })
10+
*/
11+
mount(
12+
component: React.ReactNode,
13+
options?: {
14+
initializeRecoil?: (snapshot: any) => void;
15+
[key: string]: any;
16+
},
17+
): Chainable<any>;
18+
}
19+
}

cypress/support/component.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { MemoryRouter } from 'react-router';
66
import { RecoilRoot } from 'recoil';
77

88
import i18n from 'i18next';
9-
import { initReactI18next } from 'react-i18next';
10-
import { I18nextProvider } from 'react-i18next';
9+
import { I18nextProvider, initReactI18next } from 'react-i18next';
1110

1211
i18n.use(initReactI18next).init({
1312
lng: 'en',

src/components/KymaCompanion/api/getChatResponse.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getClusterConfig } from 'state/utils/getBackendInfo';
2-
import { MessageChunk } from '../components/Chat/messages/Message';
2+
import { MessageChunk } from '../components/Chat/Message/Message';
33

44
interface ClusterAuth {
55
token?: string;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/* global cy */
2+
import Bubbles from './Bubbles';
3+
4+
describe('Bubbles Component', () => {
5+
it('Displays a busy indicator when isLoading is true', () => {
6+
cy.mount(
7+
<Bubbles
8+
suggestions={undefined}
9+
isLoading={true}
10+
onClick={cy.stub().as('onClickStub')}
11+
/>,
12+
);
13+
14+
cy.get('.ai-busy-indicator').should('exist');
15+
cy.get('ui5-busy-indicator').should('have.attr', 'active');
16+
cy.get('ui5-busy-indicator').should('have.attr', 'size', 'M');
17+
cy.get('ui5-busy-indicator').should('have.attr', 'delay', '0');
18+
});
19+
20+
it('Renders nothing when isLoading is false and suggestions is undefined', () => {
21+
cy.mount(
22+
<Bubbles
23+
suggestions={undefined}
24+
isLoading={false}
25+
onClick={cy.stub().as('onClickStub')}
26+
/>,
27+
);
28+
29+
cy.get('.bubbles-container').should('not.exist');
30+
cy.get('.ai-busy-indicator').should('not.exist');
31+
});
32+
33+
it('Renders suggestion buttons when isLoading is false and suggestions are provided', () => {
34+
const suggestions = ['Suggestion 1', 'Suggestion 2', 'Suggestion 3'];
35+
cy.mount(
36+
<Bubbles
37+
suggestions={suggestions}
38+
isLoading={false}
39+
onClick={cy.stub().as('onClickStub')}
40+
/>,
41+
);
42+
43+
cy.get('.bubbles-container').should('exist');
44+
cy.get('.bubble-button').should('have.length', suggestions.length);
45+
46+
suggestions.forEach(suggestion => {
47+
cy.contains('.bubble-button', suggestion).should('exist');
48+
});
49+
});
50+
51+
it('should call onClick function with correct suggestion when a button is clicked', () => {
52+
const suggestions = ['Suggestion 1', 'Suggestion 2', 'Suggestion 3'];
53+
cy.mount(
54+
<Bubbles
55+
suggestions={suggestions}
56+
isLoading={false}
57+
onClick={cy.stub().as('onClickStub')}
58+
/>,
59+
);
60+
61+
cy.contains('.bubble-button', 'Suggestion 2').click();
62+
cy.get('@onClickStub').should('have.been.calledWith', 'Suggestion 2');
63+
});
64+
65+
it('should handle suggestions as an empty array', () => {
66+
cy.mount(
67+
<Bubbles
68+
suggestions={[]}
69+
isLoading={false}
70+
onClick={cy.stub().as('onClickStub')}
71+
/>,
72+
);
73+
74+
cy.get('.bubbles-container').should('not.exist');
75+
cy.get('.bubble-button').should('not.exist');
76+
});
77+
78+
it('should apply correct CSS classes to components', () => {
79+
const suggestions = ['Suggestion 1', 'Suggestion 2'];
80+
cy.mount(
81+
<Bubbles
82+
suggestions={suggestions}
83+
isLoading={false}
84+
onClick={cy.stub().as('onClickStub')}
85+
/>,
86+
);
87+
88+
cy.get('.bubbles-container').should('have.class', 'sap-margin-begin-tiny');
89+
cy.get('.bubbles-container').should('have.class', 'sap-margin-bottom-tiny');
90+
cy.get('.bubbles-container').should('have.css', 'flex-wrap', 'wrap');
91+
cy.get('.bubbles-container').should(
92+
'have.css',
93+
'justify-content',
94+
'flex-start',
95+
);
96+
97+
cy.get('.bubble-button').each($btn => {
98+
cy.wrap($btn).should('have.attr', 'design', 'Default');
99+
});
100+
});
101+
});

src/components/KymaCompanion/components/Chat/messages/Bubbles.scss renamed to src/components/KymaCompanion/components/Chat/Bubbles/Bubbles.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
.suggestions-loading-indicator {
1+
.ai-busy-indicator {
22
align-self: flex-start;
3+
width: fit-content;
34
}
45

56
.bubbles-container {

src/components/KymaCompanion/components/Chat/messages/Bubbles.tsx renamed to src/components/KymaCompanion/components/Chat/Bubbles/Bubbles.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ export default function Bubbles({
1515
if (isLoading) {
1616
return (
1717
<BusyIndicator
18-
className="suggestions-loading-indicator sap-margin-begin-tiny ai-busy-indicator"
18+
className="ai-busy-indicator sap-margin-begin-tiny"
1919
active
2020
size="M"
2121
delay={0}
2222
/>
2323
);
2424
}
2525

26-
return suggestions ? (
26+
return suggestions?.length ? (
2727
<FlexBox
2828
wrap="Wrap"
2929
justifyContent="Start"

src/components/KymaCompanion/components/Chat/Chat.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { useTranslation } from 'react-i18next';
22
import React, { useEffect, useRef, useState } from 'react';
33
import { useRecoilValue } from 'recoil';
44
import { FlexBox, Icon, Text, TextArea } from '@ui5/webcomponents-react';
5-
import Message, { MessageChunk } from './messages/Message';
6-
import Bubbles from './messages/Bubbles';
7-
import ErrorMessage from './messages/ErrorMessage';
5+
import Message, { MessageChunk } from './Message/Message';
6+
import Bubbles from './Bubbles/Bubbles';
7+
import ErrorMessage from './ErrorMessage/ErrorMessage';
88
import { sessionIDState } from 'state/companion/sessionIDAtom';
99
import { clusterState } from 'state/clusterAtom';
1010
import { authDataState } from 'state/authDataAtom';
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/* global cy */
2+
import { t } from 'i18next';
3+
import CodePanel from './CodePanel';
4+
import '@ui5/webcomponents-icons/dist/AllIcons.js';
5+
6+
describe('CodePanel Component', () => {
7+
it('renders text-only code response when no language is provided', () => {
8+
const code = 'const hello = "world";';
9+
10+
cy.mount(<CodePanel code={code} language="" />);
11+
12+
cy.get('.code-response').should('exist');
13+
cy.get('#copy-icon').should('exist');
14+
cy.get('#code-text').should('contain.text', code);
15+
cy.get('ui5-panel').should('not.exist');
16+
cy.get('pre').should('not.exist');
17+
});
18+
19+
it('renders a code panel with syntax highlighting when language is provided', () => {
20+
const code = 'const hello = "world";';
21+
const language = 'javascript';
22+
23+
cy.mount(<CodePanel code={code} language={language} />);
24+
25+
cy.get('.code-panel').should('exist');
26+
cy.get('ui5-panel').should('exist');
27+
cy.get('ui5-title').should('contain.text', language);
28+
cy.get('pre').should('exist');
29+
cy.get('code').should('contain.text', code);
30+
});
31+
32+
it('displays only the copy button when withAction is false or link is not provided', () => {
33+
const code = 'const hello = "world";';
34+
const language = 'javascript';
35+
36+
cy.mount(<CodePanel code={code} language={language} withAction={false} />);
37+
38+
cy.get('.action-button').should('have.length', 1);
39+
cy.get('.action-button')
40+
.eq(0)
41+
.should('have.attr', 'icon', 'copy');
42+
cy.get('.action-button')
43+
.eq(0)
44+
.should('contain.text', t('common.buttons.copy'));
45+
});
46+
47+
it('displays both copy and place buttons when withAction is true and link is provided', () => {
48+
const code = 'const hello = "world";';
49+
const language = 'javascript';
50+
const link = {
51+
name: 'my-service',
52+
address: 'namespaces/default/services/my-service',
53+
actionType: 'New',
54+
};
55+
56+
cy.mount(
57+
<CodePanel
58+
code={code}
59+
language={language}
60+
withAction={true}
61+
link={link}
62+
/>,
63+
);
64+
65+
cy.get('.action-button').should('have.length', 2);
66+
cy.get('.action-button')
67+
.eq(0)
68+
.should('have.attr', 'icon', 'copy')
69+
.should('have.attr', 'design', 'Transparent')
70+
.should('have.attr', 'accessible-name', t('common.buttons.copy'));
71+
cy.get('.action-button')
72+
.eq(1)
73+
.should('have.attr', 'icon', 'sys-add');
74+
cy.get('.action-button')
75+
.eq(1)
76+
.should('contain.text', t('common.buttons.place'));
77+
});
78+
79+
it('renders the place button with correct attributes for namespace resources', () => {
80+
const code = 'apiVersion: v1\nkind: Service\nmetadata:\n name: my-service';
81+
const language = 'yaml';
82+
const link = {
83+
name: 'my-service',
84+
address: 'namespaces/default/services/my-service',
85+
actionType: 'New',
86+
};
87+
88+
cy.mount(
89+
<CodePanel
90+
code={code}
91+
language={language}
92+
withAction={true}
93+
link={link}
94+
/>,
95+
);
96+
97+
cy.get('.action-button')
98+
.eq(1)
99+
.should('exist');
100+
cy.get('.action-button')
101+
.eq(1)
102+
.should('have.attr', 'icon', 'sys-add');
103+
cy.get('.action-button')
104+
.eq(1)
105+
.should('contain.text', t('common.buttons.place'));
106+
});
107+
108+
it('renders the place button with correct attributes for cluster-level resources', () => {
109+
const code =
110+
'apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n name: my-role';
111+
const language = 'yaml';
112+
const link = {
113+
name: 'my-role',
114+
address: 'clusterroles/my-role',
115+
actionType: 'Update',
116+
};
117+
118+
cy.mount(
119+
<CodePanel
120+
code={code}
121+
language={language}
122+
withAction={true}
123+
link={link}
124+
/>,
125+
);
126+
127+
cy.get('.action-button')
128+
.eq(1)
129+
.should('exist');
130+
cy.get('.action-button')
131+
.eq(1)
132+
.should('have.attr', 'icon', 'sys-add');
133+
cy.get('.action-button')
134+
.eq(1)
135+
.should('contain.text', t('common.buttons.place'));
136+
});
137+
138+
it('handles long code samples with proper wrapping', () => {
139+
const longCode =
140+
'const veryLongVariableName = "This is an extremely long string that should trigger the line wrapping feature of the syntax highlighter component and test if it works properly in all cases, even with very long continuous text without natural breaking points";';
141+
const language = 'javascript';
142+
143+
cy.mount(<CodePanel code={longCode} language={language} />);
144+
145+
cy.get('pre').should('exist');
146+
cy.get('code').should('be.visible');
147+
// Visual check for wrapping will be done in UI
148+
});
149+
150+
it('renders correctly with different languages', () => {
151+
const languages = ['javascript', 'python', 'yaml', 'json', 'bash'];
152+
153+
languages.forEach(lang => {
154+
const code = `// Sample ${lang} code`;
155+
cy.mount(<CodePanel code={code} language={lang} />);
156+
157+
cy.get('ui5-title').should('contain.text', lang);
158+
cy.get('code').should('contain.text', code);
159+
cy.get('ui5-panel').should('exist');
160+
});
161+
});
162+
163+
it('handles empty code string gracefully', () => {
164+
cy.mount(<CodePanel code="" language="javascript" />);
165+
166+
cy.get('.code-panel').should('exist');
167+
cy.get('pre')
168+
.find('code')
169+
.should('exist');
170+
});
171+
172+
it('renders correctly with Update action type', () => {
173+
const code = 'apiVersion: v1\nkind: Service\nmetadata:\n name: my-service';
174+
const language = 'yaml';
175+
const link = {
176+
name: 'my-service',
177+
address: 'namespaces/default/services/my-service',
178+
actionType: 'Update',
179+
};
180+
181+
cy.mount(
182+
<CodePanel
183+
code={code}
184+
language={language}
185+
withAction={true}
186+
link={link}
187+
/>,
188+
);
189+
190+
cy.get('.action-button')
191+
.eq(1)
192+
.should('exist');
193+
});
194+
195+
it('renders panel header with correct title', () => {
196+
const code = 'const hello = "world";';
197+
const language = 'javascript';
198+
199+
cy.mount(<CodePanel code={code} language={language} />);
200+
201+
cy.get('ui5-title').should('contain.text', language);
202+
cy.get('ui5-title').should('have.attr', 'level', 'H6');
203+
});
204+
});

src/components/KymaCompanion/components/Chat/messages/CodePanel.scss renamed to src/components/KymaCompanion/components/Chat/CodePanel/CodePanel.scss

File renamed without changes.

src/components/KymaCompanion/components/Chat/messages/CodePanel.tsx renamed to src/components/KymaCompanion/components/Chat/CodePanel/CodePanel.tsx

File renamed without changes.

0 commit comments

Comments
 (0)