Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4378616
make folder structure more modular
chriskari Apr 9, 2025
3ad6f63
add first component and unit tests
chriskari Apr 9, 2025
f059222
test: add tasklist component test
chriskari Apr 10, 2025
920957c
test: add codePanel and Errormessage component tests
chriskari Apr 10, 2025
d690db9
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 10, 2025
618afa7
test: add integration test
chriskari Apr 10, 2025
3b1ebb7
test: add integration test for initial suggestions
chriskari Apr 10, 2025
5faf011
test: run integration test on dev
chriskari Apr 10, 2025
4424f2d
test: enable companion feature in busola config for integration test
chriskari Apr 11, 2025
4c60f66
test: dont run companion test on dev
chriskari Apr 11, 2025
cb49b99
fix: remove redundant css
chriskari Apr 11, 2025
dea9ccd
test: add more scenarios to integration test
chriskari Apr 12, 2025
c1813d0
test: finish integration test
chriskari Apr 12, 2025
4f3d27e
test: refactor integration test code
chriskari Apr 12, 2025
80a839d
test: refactor integration test code
chriskari Apr 12, 2025
72b6f6d
test: finish unit test
chriskari Apr 12, 2025
a3be8f7
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 14, 2025
3164574
test: fix flakiness
chriskari Apr 15, 2025
60d6759
test: fix flakiness
chriskari Apr 15, 2025
caec3d1
test: fix flakiness
chriskari Apr 15, 2025
e73b728
test: fix flakiness
chriskari Apr 15, 2025
8d4a3c4
test: split test files
chriskari Apr 17, 2025
4032d63
test: add companion test to cluster test workflow
chriskari Apr 17, 2025
f156c02
test: fix flakiness of component test
chriskari Apr 17, 2025
16fa7d7
Merge branch 'main' of https://github.com/kyma-project/busola into ai…
chriskari Apr 17, 2025
dd60e45
test: fix flakiness of component test
chriskari Apr 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions cypress/cypress.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <reference types="cypress" />

declare namespace Cypress {
interface Chainable {
/**
* Custom command to mount a React component with common providers
* @param component - React component to mount
* @param options - Options for mount, may include initializeRecoil function
* @example cy.mount(<MyComponent />, { initializeRecoil: (snapshot) => {...} })
*/
mount(
component: React.ReactNode,
options?: {
initializeRecoil?: (snapshot: any) => void;
[key: string]: any;
},
): Chainable<any>;
}
}
3 changes: 1 addition & 2 deletions cypress/support/component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { MemoryRouter } from 'react-router';
import { RecoilRoot } from 'recoil';

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { I18nextProvider } from 'react-i18next';
import { I18nextProvider, initReactI18next } from 'react-i18next';

i18n.use(initReactI18next).init({
lng: 'en',
Expand Down
2 changes: 1 addition & 1 deletion src/components/KymaCompanion/api/getChatResponse.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getClusterConfig } from 'state/utils/getBackendInfo';
import { MessageChunk } from '../components/Chat/messages/Message';
import { MessageChunk } from '../components/Chat/Message/Message';

interface ClusterAuth {
token?: string;
Expand Down
101 changes: 101 additions & 0 deletions src/components/KymaCompanion/components/Chat/Bubbles/Bubbles.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/* global cy */
import Bubbles from './Bubbles';

describe('Bubbles Component', () => {
it('Displays a busy indicator when isLoading is true', () => {
cy.mount(
<Bubbles
suggestions={undefined}
isLoading={true}
onClick={cy.stub().as('onClickStub')}
/>,
);

cy.get('.ai-busy-indicator').should('exist');
cy.get('ui5-busy-indicator').should('have.attr', 'active');
cy.get('ui5-busy-indicator').should('have.attr', 'size', 'M');
cy.get('ui5-busy-indicator').should('have.attr', 'delay', '0');
});

it('Renders nothing when isLoading is false and suggestions is undefined', () => {
cy.mount(
<Bubbles
suggestions={undefined}
isLoading={false}
onClick={cy.stub().as('onClickStub')}
/>,
);

cy.get('.bubbles-container').should('not.exist');
cy.get('.ai-busy-indicator').should('not.exist');
});

it('Renders suggestion buttons when isLoading is false and suggestions are provided', () => {
const suggestions = ['Suggestion 1', 'Suggestion 2', 'Suggestion 3'];
cy.mount(
<Bubbles
suggestions={suggestions}
isLoading={false}
onClick={cy.stub().as('onClickStub')}
/>,
);

cy.get('.bubbles-container').should('exist');
cy.get('.bubble-button').should('have.length', suggestions.length);

suggestions.forEach(suggestion => {
cy.contains('.bubble-button', suggestion).should('exist');
});
});

it('should call onClick function with correct suggestion when a button is clicked', () => {
const suggestions = ['Suggestion 1', 'Suggestion 2', 'Suggestion 3'];
cy.mount(
<Bubbles
suggestions={suggestions}
isLoading={false}
onClick={cy.stub().as('onClickStub')}
/>,
);

cy.contains('.bubble-button', 'Suggestion 2').click();
cy.get('@onClickStub').should('have.been.calledWith', 'Suggestion 2');
});

it('should handle suggestions as an empty array', () => {
cy.mount(
<Bubbles
suggestions={[]}
isLoading={false}
onClick={cy.stub().as('onClickStub')}
/>,
);

cy.get('.bubbles-container').should('not.exist');
cy.get('.bubble-button').should('not.exist');
});

it('should apply correct CSS classes to components', () => {
const suggestions = ['Suggestion 1', 'Suggestion 2'];
cy.mount(
<Bubbles
suggestions={suggestions}
isLoading={false}
onClick={cy.stub().as('onClickStub')}
/>,
);

cy.get('.bubbles-container').should('have.class', 'sap-margin-begin-tiny');
cy.get('.bubbles-container').should('have.class', 'sap-margin-bottom-tiny');
cy.get('.bubbles-container').should('have.css', 'flex-wrap', 'wrap');
cy.get('.bubbles-container').should(
'have.css',
'justify-content',
'flex-start',
);

cy.get('.bubble-button').each($btn => {
cy.wrap($btn).should('have.attr', 'design', 'Default');
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.suggestions-loading-indicator {
.ai-busy-indicator {
align-self: flex-start;
width: fit-content;
}

.bubbles-container {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ export default function Bubbles({
if (isLoading) {
return (
<BusyIndicator
className="suggestions-loading-indicator sap-margin-begin-tiny ai-busy-indicator"
className="ai-busy-indicator sap-margin-begin-tiny"
active
size="M"
delay={0}
/>
);
}

return suggestions ? (
return suggestions?.length ? (
<FlexBox
wrap="Wrap"
justifyContent="Start"
Expand Down
6 changes: 3 additions & 3 deletions src/components/KymaCompanion/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { useTranslation } from 'react-i18next';
import React, { useEffect, useRef, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { FlexBox, Icon, Text, TextArea } from '@ui5/webcomponents-react';
import Message, { MessageChunk } from './messages/Message';
import Bubbles from './messages/Bubbles';
import ErrorMessage from './messages/ErrorMessage';
import Message, { MessageChunk } from './Message/Message';
import Bubbles from './Bubbles/Bubbles';
import ErrorMessage from './ErrorMessage/ErrorMessage';
import { sessionIDState } from 'state/companion/sessionIDAtom';
import { clusterState } from 'state/clusterAtom';
import { authDataState } from 'state/authDataAtom';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/* global cy */
import { t } from 'i18next';
import CodePanel from './CodePanel';
import '@ui5/webcomponents-icons/dist/AllIcons.js';

describe('CodePanel Component', () => {
it('renders text-only code response when no language is provided', () => {
const code = 'const hello = "world";';

cy.mount(<CodePanel code={code} language="" />);

cy.get('.code-response').should('exist');
cy.get('#copy-icon').should('exist');
cy.get('#code-text').should('contain.text', code);
cy.get('ui5-panel').should('not.exist');
cy.get('pre').should('not.exist');
});

it('renders a code panel with syntax highlighting when language is provided', () => {
const code = 'const hello = "world";';
const language = 'javascript';

cy.mount(<CodePanel code={code} language={language} />);

cy.get('.code-panel').should('exist');
cy.get('ui5-panel').should('exist');
cy.get('ui5-title').should('contain.text', language);
cy.get('pre').should('exist');
cy.get('code').should('contain.text', code);
});

it('displays only the copy button when withAction is false or link is not provided', () => {
const code = 'const hello = "world";';
const language = 'javascript';

cy.mount(<CodePanel code={code} language={language} withAction={false} />);

cy.get('.action-button').should('have.length', 1);
cy.get('.action-button')
.eq(0)
.should('have.attr', 'icon', 'copy');
cy.get('.action-button')
.eq(0)
.should('contain.text', t('common.buttons.copy'));
});

it('displays both copy and place buttons when withAction is true and link is provided', () => {
const code = 'const hello = "world";';
const language = 'javascript';
const link = {
name: 'my-service',
address: 'namespaces/default/services/my-service',
actionType: 'New',
};

cy.mount(
<CodePanel
code={code}
language={language}
withAction={true}
link={link}
/>,
);

cy.get('.action-button').should('have.length', 2);
cy.get('.action-button')
.eq(0)
.should('have.attr', 'icon', 'copy')
.should('have.attr', 'design', 'Transparent')
.should('have.attr', 'accessible-name', t('common.buttons.copy'));
cy.get('.action-button')
.eq(1)
.should('have.attr', 'icon', 'sys-add');
cy.get('.action-button')
.eq(1)
.should('contain.text', t('common.buttons.place'));
});

it('renders the place button with correct attributes for namespace resources', () => {
const code = 'apiVersion: v1\nkind: Service\nmetadata:\n name: my-service';
const language = 'yaml';
const link = {
name: 'my-service',
address: 'namespaces/default/services/my-service',
actionType: 'New',
};

cy.mount(
<CodePanel
code={code}
language={language}
withAction={true}
link={link}
/>,
);

cy.get('.action-button')
.eq(1)
.should('exist');
cy.get('.action-button')
.eq(1)
.should('have.attr', 'icon', 'sys-add');
cy.get('.action-button')
.eq(1)
.should('contain.text', t('common.buttons.place'));
});

it('renders the place button with correct attributes for cluster-level resources', () => {
const code =
'apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n name: my-role';
const language = 'yaml';
const link = {
name: 'my-role',
address: 'clusterroles/my-role',
actionType: 'Update',
};

cy.mount(
<CodePanel
code={code}
language={language}
withAction={true}
link={link}
/>,
);

cy.get('.action-button')
.eq(1)
.should('exist');
cy.get('.action-button')
.eq(1)
.should('have.attr', 'icon', 'sys-add');
cy.get('.action-button')
.eq(1)
.should('contain.text', t('common.buttons.place'));
});

it('handles long code samples with proper wrapping', () => {
const longCode =
'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";';
const language = 'javascript';

cy.mount(<CodePanel code={longCode} language={language} />);

cy.get('pre').should('exist');
cy.get('code').should('be.visible');
// Visual check for wrapping will be done in UI
});

it('renders correctly with different languages', () => {
const languages = ['javascript', 'python', 'yaml', 'json', 'bash'];

languages.forEach(lang => {
const code = `// Sample ${lang} code`;
cy.mount(<CodePanel code={code} language={lang} />);

cy.get('ui5-title').should('contain.text', lang);
cy.get('code').should('contain.text', code);
cy.get('ui5-panel').should('exist');
});
});

it('handles empty code string gracefully', () => {
cy.mount(<CodePanel code="" language="javascript" />);

cy.get('.code-panel').should('exist');
cy.get('pre')
.find('code')
.should('exist');
});

it('renders correctly with Update action type', () => {
const code = 'apiVersion: v1\nkind: Service\nmetadata:\n name: my-service';
const language = 'yaml';
const link = {
name: 'my-service',
address: 'namespaces/default/services/my-service',
actionType: 'Update',
};

cy.mount(
<CodePanel
code={code}
language={language}
withAction={true}
link={link}
/>,
);

cy.get('.action-button')
.eq(1)
.should('exist');
});

it('renders panel header with correct title', () => {
const code = 'const hello = "world";';
const language = 'javascript';

cy.mount(<CodePanel code={code} language={language} />);

cy.get('ui5-title').should('contain.text', language);
cy.get('ui5-title').should('have.attr', 'level', 'H6');
});
});
Loading
Loading