@gitlab/ui 66.25.1 → 66.27.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.
- package/CHANGELOG.md +14 -0
- package/dist/components/base/alert/alert.js +2 -1
- package/dist/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.js +128 -0
- package/dist/components/experimental/duo/chat/mock_data.js +41 -3
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/tokens/css/tokens.css +1 -1
- package/dist/tokens/css/tokens.dark.css +1 -1
- package/dist/tokens/js/tokens.dark.js +1 -1
- package/dist/tokens/js/tokens.js +1 -1
- package/dist/tokens/scss/_tokens.dark.scss +1 -1
- package/dist/tokens/scss/_tokens.scss +1 -1
- package/package.json +16 -16
- package/src/components/base/alert/alert.scss +12 -10
- package/src/components/base/alert/alert.spec.js +16 -0
- package/src/components/base/alert/alert.vue +4 -6
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.md +60 -0
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.scss +18 -0
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.spec.js +381 -0
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.stories.js +45 -0
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.vue +107 -0
- package/src/components/experimental/duo/chat/mock_data.js +45 -2
- package/src/scss/components.scss +1 -0
package/dist/tokens/js/tokens.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/ui",
|
|
3
|
-
"version": "66.
|
|
3
|
+
"version": "66.27.0",
|
|
4
4
|
"description": "GitLab UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -98,18 +98,18 @@
|
|
|
98
98
|
"@rollup/plugin-commonjs": "^11.1.0",
|
|
99
99
|
"@rollup/plugin-node-resolve": "^7.1.3",
|
|
100
100
|
"@rollup/plugin-replace": "^2.3.2",
|
|
101
|
-
"@storybook/addon-a11y": "7.4.
|
|
102
|
-
"@storybook/addon-docs": "7.4.
|
|
103
|
-
"@storybook/addon-essentials": "7.4.
|
|
104
|
-
"@storybook/addon-storyshots": "7.4.
|
|
105
|
-
"@storybook/addon-storyshots-puppeteer": "7.4.
|
|
106
|
-
"@storybook/addon-viewport": "7.4.
|
|
107
|
-
"@storybook/builder-webpack5": "7.4.
|
|
108
|
-
"@storybook/theming": "7.4.
|
|
109
|
-
"@storybook/vue": "7.4.
|
|
110
|
-
"@storybook/vue-webpack5": "7.4.
|
|
111
|
-
"@storybook/vue3": "7.4.
|
|
112
|
-
"@storybook/vue3-webpack5": "7.4.
|
|
101
|
+
"@storybook/addon-a11y": "7.4.6",
|
|
102
|
+
"@storybook/addon-docs": "7.4.6",
|
|
103
|
+
"@storybook/addon-essentials": "7.4.6",
|
|
104
|
+
"@storybook/addon-storyshots": "7.4.6",
|
|
105
|
+
"@storybook/addon-storyshots-puppeteer": "7.4.6",
|
|
106
|
+
"@storybook/addon-viewport": "7.4.6",
|
|
107
|
+
"@storybook/builder-webpack5": "7.4.6",
|
|
108
|
+
"@storybook/theming": "7.4.6",
|
|
109
|
+
"@storybook/vue": "7.4.6",
|
|
110
|
+
"@storybook/vue-webpack5": "7.4.6",
|
|
111
|
+
"@storybook/vue3": "7.4.6",
|
|
112
|
+
"@storybook/vue3-webpack5": "7.4.6",
|
|
113
113
|
"@vue/compat": "^3.2.40",
|
|
114
114
|
"@vue/compiler-sfc": "^3.2.40",
|
|
115
115
|
"@vue/test-utils": "1.3.0",
|
|
@@ -126,10 +126,10 @@
|
|
|
126
126
|
"cypress-axe": "^1.4.0",
|
|
127
127
|
"dompurify": "^3.0.0",
|
|
128
128
|
"emoji-regex": "^10.0.0",
|
|
129
|
-
"eslint": "8.
|
|
129
|
+
"eslint": "8.51.0",
|
|
130
130
|
"eslint-import-resolver-jest": "3.0.2",
|
|
131
131
|
"eslint-plugin-cypress": "2.15.1",
|
|
132
|
-
"eslint-plugin-storybook": "0.6.
|
|
132
|
+
"eslint-plugin-storybook": "0.6.15",
|
|
133
133
|
"glob": "10.3.3",
|
|
134
134
|
"identity-obj-proxy": "^3.0.0",
|
|
135
135
|
"inquirer-select-directory": "^1.2.0",
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"sass-loader": "^10.2.0",
|
|
161
161
|
"sass-true": "^6.1.0",
|
|
162
162
|
"start-server-and-test": "^1.10.6",
|
|
163
|
-
"storybook": "7.4.
|
|
163
|
+
"storybook": "7.4.6",
|
|
164
164
|
"storybook-dark-mode": "3.0.1",
|
|
165
165
|
"style-dictionary": "^3.8.0",
|
|
166
166
|
"stylelint": "15.10.2",
|
|
@@ -28,24 +28,26 @@
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.gl-alert-title {
|
|
31
|
-
@include gl-
|
|
32
|
-
@include gl-font-weight-bold;
|
|
33
|
-
@include gl-line-height-normal;
|
|
34
|
-
@include gl-mt-0;
|
|
31
|
+
@include gl-heading-scale-500;
|
|
35
32
|
@include gl-mb-3;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
.gl-alert-icon {
|
|
35
|
+
.gl-alert-icon-container {
|
|
39
36
|
@include gl-absolute;
|
|
40
37
|
@include gl-top-5;
|
|
41
38
|
@include gl-left-5;
|
|
42
|
-
@include gl-
|
|
43
|
-
@include gl-
|
|
44
|
-
|
|
39
|
+
@include gl-display-flex;
|
|
40
|
+
@include gl-align-items-center;
|
|
41
|
+
height: $gl-line-height-20;
|
|
42
|
+
|
|
43
|
+
.gl-alert-has-title & {
|
|
44
|
+
@include gl-heading-scale-500; // get dynamic font-size
|
|
45
|
+
height: $gl-line-height-heading * 1em; // give unit to unitless relative line-height (1.25)
|
|
46
|
+
}
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
.gl-alert-icon
|
|
48
|
-
@include gl-
|
|
49
|
+
.gl-alert-icon {
|
|
50
|
+
@include gl-fill-current-color;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
.gl-alert-body {
|
|
@@ -190,6 +190,22 @@ describe('Alert component', () => {
|
|
|
190
190
|
expect(wrapper.classes()).not.toContain(cssClass);
|
|
191
191
|
});
|
|
192
192
|
});
|
|
193
|
+
|
|
194
|
+
it('adds the `gl-alert-has-title` class if there is a title', () => {
|
|
195
|
+
createComponent({
|
|
196
|
+
propsData: {
|
|
197
|
+
title: 'title',
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(wrapper.classes()).toContain('gl-alert-has-title');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('does not add the `gl-alert-has-title` class if there is no title', () => {
|
|
205
|
+
createComponent();
|
|
206
|
+
|
|
207
|
+
expect(wrapper.classes()).not.toContain('gl-alert-has-title');
|
|
208
|
+
});
|
|
193
209
|
});
|
|
194
210
|
|
|
195
211
|
describe('role and aria-live', () => {
|
|
@@ -197,15 +197,13 @@ export default {
|
|
|
197
197
|
{ 'gl-alert-sticky': sticky },
|
|
198
198
|
{ 'gl-alert-not-dismissible': !dismissible },
|
|
199
199
|
{ 'gl-alert-no-icon': !showIcon },
|
|
200
|
+
{ 'gl-alert-has-title': !!title },
|
|
200
201
|
variantClass,
|
|
201
202
|
]"
|
|
202
203
|
>
|
|
203
|
-
<gl-icon
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
:class="{ 'gl-alert-icon': true, 'gl-alert-icon-no-title': !title }"
|
|
207
|
-
/>
|
|
208
|
-
|
|
204
|
+
<div v-if="showIcon" class="gl-alert-icon-container">
|
|
205
|
+
<gl-icon :name="iconName" class="gl-alert-icon" />
|
|
206
|
+
</div>
|
|
209
207
|
<div class="gl-alert-content" :role="role" :aria-live="ariaLive">
|
|
210
208
|
<h2 v-if="title" class="gl-alert-title">{{ title }}</h2>
|
|
211
209
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
The component represents a Duo Chat message.
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<gl-duo-chat-message :message="message" />
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Pretty rendering message content
|
|
10
|
+
|
|
11
|
+
There are two ways of pretty-rendering a message's content in the component:
|
|
12
|
+
|
|
13
|
+
- dependency injection, providing functions to convert raw markdown into HTML,
|
|
14
|
+
- sending `contentHtml` prop as part of the `message` property,
|
|
15
|
+
|
|
16
|
+
### Injecting functions
|
|
17
|
+
|
|
18
|
+
To inject the `renderMarkdown` function, which converts raw markdown into proper HTML,
|
|
19
|
+
the component relies on [dependency injection, using `provide`/`inject` options](https://docs.gitlab.com/ee/development/fe_guide/vue.html#provide-and-inject).
|
|
20
|
+
The component expects a reference to a function, converting raw markdown into HTML
|
|
21
|
+
to be _provided_ by a consumer.
|
|
22
|
+
[The example implementation](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/notes/utils.js#L22-24)
|
|
23
|
+
|
|
24
|
+
### `contentHtml`
|
|
25
|
+
|
|
26
|
+
This approach is self-explanatory and is used when raw markdown can be converted to HTML on the server
|
|
27
|
+
before the message is returned to the client. Here's an example of a message's structure where markdown
|
|
28
|
+
has been generated on the server and sent down in the `contentHtml` property:
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
{
|
|
32
|
+
content: '_Duo Chat message_ coming from AI',
|
|
33
|
+
contentHtml: '<p><em>Duo Chat message</em> coming from AI</p>',
|
|
34
|
+
role: 'assistant',
|
|
35
|
+
...
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## GitLab Flavored Markdown (GLFM)
|
|
40
|
+
|
|
41
|
+
In most cases, it's not enough to just convert raw markdown into HTML. Messages also require the
|
|
42
|
+
markup to support [GitLab Flavored Markdown (GLFM)](https://docs.gitlab.com/ee/user/markdown.html).
|
|
43
|
+
For this, the component relies on another dependency injection (in addition to `renderMarkdown`)
|
|
44
|
+
expecting a reference to the `renderGFM` function, decorating an HTML element with GLFM to be
|
|
45
|
+
_provided_ by a consumer.
|
|
46
|
+
[The example implementation](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/behaviors/markdown/render_gfm.js#L19-52)
|
|
47
|
+
|
|
48
|
+
## The underlying use of the `GlDuoUserFeedback` component
|
|
49
|
+
|
|
50
|
+
The component integrates the [`GlDuoUserFeedback`](/story/experimental-duo-user-feedback--default)
|
|
51
|
+
component to track user feedback on the AI-generated responses. Note that the `GlDuoChatMessage`
|
|
52
|
+
component renders the default state of `GlDuoUserFeedback` component, not allowing to override
|
|
53
|
+
the slots in that underlying component.
|
|
54
|
+
|
|
55
|
+
### Tracking User Feedback for a response
|
|
56
|
+
|
|
57
|
+
The component emits the `track-feedback` event, a proxy of the `feedback` event emitted by
|
|
58
|
+
the `GlDuoUserFeedback` component. Please refer to
|
|
59
|
+
[the documentation on that component](/story/experimental-duo-user-feedback--docs#listening-to-the-feedback-form-submission)
|
|
60
|
+
when processing feedback from users.
|
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.scss
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.duo-chat-message {
|
|
2
|
+
max-width: 90%;
|
|
3
|
+
|
|
4
|
+
code {
|
|
5
|
+
@include gl-bg-gray-100;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
pre code {
|
|
9
|
+
@include gl-font-sm;
|
|
10
|
+
@include gl-line-height-1;
|
|
11
|
+
@include gl-bg-transparent;
|
|
12
|
+
white-space: inherit;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
p:last-of-type {
|
|
16
|
+
@include gl-mb-0;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.spec.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { nextTick } from 'vue';
|
|
2
|
+
import { shallowMount } from '@vue/test-utils';
|
|
3
|
+
import { GlDuoUserFeedback } from '../../../../../../index';
|
|
4
|
+
import {
|
|
5
|
+
MOCK_USER_PROMPT_MESSAGE,
|
|
6
|
+
MOCK_RESPONSE_MESSAGE,
|
|
7
|
+
MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
8
|
+
} from '../../mock_data';
|
|
9
|
+
import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources.vue';
|
|
10
|
+
import GlDuoChatMessage from './duo_chat_message.vue';
|
|
11
|
+
|
|
12
|
+
describe('DuoChatMessage', () => {
|
|
13
|
+
let wrapper;
|
|
14
|
+
|
|
15
|
+
const findContent = () => wrapper.findComponent({ ref: 'content' });
|
|
16
|
+
const findDocumentSources = () => wrapper.findComponent(DocumentationSources);
|
|
17
|
+
const findUserFeedback = () => wrapper.findComponent(GlDuoUserFeedback);
|
|
18
|
+
const mockMarkdownContent = 'foo **bar**';
|
|
19
|
+
|
|
20
|
+
let renderMarkdown;
|
|
21
|
+
let renderGFM;
|
|
22
|
+
|
|
23
|
+
const componentFactory = ({ message = MOCK_USER_PROMPT_MESSAGE, options = {} } = {}) => {
|
|
24
|
+
return shallowMount(GlDuoChatMessage, {
|
|
25
|
+
...options,
|
|
26
|
+
propsData: {
|
|
27
|
+
message,
|
|
28
|
+
},
|
|
29
|
+
provide: {
|
|
30
|
+
renderMarkdown,
|
|
31
|
+
renderGFM,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const createComponent = (args) => {
|
|
37
|
+
wrapper = componentFactory(args);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
renderMarkdown = jest.fn().mockImplementation((val) => val);
|
|
42
|
+
renderGFM = jest.fn();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
jest.clearAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('rendering', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
renderMarkdown.mockImplementation(() => mockMarkdownContent);
|
|
52
|
+
createComponent();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('converts the message `content` to Markdown', () => {
|
|
56
|
+
expect(renderMarkdown).toHaveBeenCalledWith(MOCK_USER_PROMPT_MESSAGE.content);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('renders message content', () => {
|
|
60
|
+
expect(wrapper.text()).toBe(mockMarkdownContent);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('user message', () => {
|
|
64
|
+
it('does not render the documentation sources component', () => {
|
|
65
|
+
expect(findDocumentSources().exists()).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('does not render the user feedback component', () => {
|
|
69
|
+
expect(findUserFeedback().exists()).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('rendering - with assistant message', () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
createComponent({
|
|
77
|
+
message: MOCK_RESPONSE_MESSAGE,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('renders the documentation sources component by default', () => {
|
|
82
|
+
expect(findDocumentSources().exists()).toBe(true);
|
|
83
|
+
expect(findDocumentSources().props('sources')).toEqual(MOCK_RESPONSE_MESSAGE.extras.sources);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it.each([null, undefined, ''])(
|
|
87
|
+
'does not render sources component when `sources` is %s',
|
|
88
|
+
(sources) => {
|
|
89
|
+
createComponent({
|
|
90
|
+
message: {
|
|
91
|
+
...MOCK_RESPONSE_MESSAGE,
|
|
92
|
+
extras: {
|
|
93
|
+
sources,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
expect(findDocumentSources().exists()).toBe(false);
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
it('renders the user feedback component', () => {
|
|
102
|
+
expect(findUserFeedback().exists()).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('proxies the emitted event from the User Feedback component', () => {
|
|
106
|
+
findUserFeedback().vm.$emit('feedback', 'foo');
|
|
107
|
+
expect(wrapper.emitted('track-feedback')).toEqual([['foo']]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('message output', () => {
|
|
112
|
+
it('hydrates the message with GLFM when mounting the component', () => {
|
|
113
|
+
createComponent();
|
|
114
|
+
expect(renderGFM).toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('outputs errors if message has no content', async () => {
|
|
118
|
+
const errors = ['foo', 'bar', 'baz'];
|
|
119
|
+
|
|
120
|
+
createComponent({
|
|
121
|
+
message: {
|
|
122
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
123
|
+
contentHtml: '',
|
|
124
|
+
content: '',
|
|
125
|
+
errors,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await nextTick();
|
|
130
|
+
|
|
131
|
+
expect(findContent().text()).toContain(errors[0]);
|
|
132
|
+
expect(findContent().text()).toContain(errors[1]);
|
|
133
|
+
expect(findContent().text()).toContain(errors[2]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('message updates watcher', () => {
|
|
137
|
+
const newContent = 'new foo content';
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
createComponent();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('listens to the message changes', async () => {
|
|
143
|
+
expect(findContent().text()).toContain(MOCK_USER_PROMPT_MESSAGE.content);
|
|
144
|
+
// setProps is justified here because we are testing the component's
|
|
145
|
+
// reactive behavior which consistutes an exception
|
|
146
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
147
|
+
wrapper.setProps({
|
|
148
|
+
message: {
|
|
149
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
150
|
+
contentHtml: `<p>${newContent}</p>`,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
await nextTick();
|
|
154
|
+
expect(findContent().text()).not.toContain(MOCK_USER_PROMPT_MESSAGE.content);
|
|
155
|
+
expect(findContent().text()).toContain(newContent);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('prioritises the output of contentHtml over content', async () => {
|
|
159
|
+
// setProps is justified here because we are testing the component's
|
|
160
|
+
// reactive behavior which consistutes an exception
|
|
161
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
162
|
+
wrapper.setProps({
|
|
163
|
+
message: {
|
|
164
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
165
|
+
contentHtml: `<p>${MOCK_USER_PROMPT_MESSAGE.content}</p>`,
|
|
166
|
+
content: newContent,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
await nextTick();
|
|
170
|
+
expect(findContent().text()).not.toContain(newContent);
|
|
171
|
+
expect(findContent().text()).toContain(MOCK_USER_PROMPT_MESSAGE.content);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('outputs errors if message has no content', async () => {
|
|
175
|
+
// setProps is justified here because we are testing the component's
|
|
176
|
+
// reactive behavior which consistutes an exception
|
|
177
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
178
|
+
wrapper.setProps({
|
|
179
|
+
message: {
|
|
180
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
181
|
+
contentHtml: '',
|
|
182
|
+
content: '',
|
|
183
|
+
errors: ['error'],
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
await nextTick();
|
|
187
|
+
expect(findContent().text()).not.toContain(newContent);
|
|
188
|
+
expect(findContent().text()).not.toContain(MOCK_USER_PROMPT_MESSAGE.content);
|
|
189
|
+
expect(findContent().text()).toContain('error');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('merges all the errors for output', async () => {
|
|
193
|
+
const errors = ['foo', 'bar', 'baz'];
|
|
194
|
+
// setProps is justified here because we are testing the component's
|
|
195
|
+
// reactive behavior which consistutes an exception
|
|
196
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
197
|
+
wrapper.setProps({
|
|
198
|
+
message: {
|
|
199
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
200
|
+
contentHtml: '',
|
|
201
|
+
content: '',
|
|
202
|
+
errors,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
await nextTick();
|
|
206
|
+
expect(findContent().text()).toContain(errors[0]);
|
|
207
|
+
expect(findContent().text()).toContain(errors[1]);
|
|
208
|
+
expect(findContent().text()).toContain(errors[2]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('hydrates the output message with GLFM if its not a chunk', async () => {
|
|
212
|
+
// setProps is justified here because we are testing the component's
|
|
213
|
+
// reactive behavior which consistutes an exception
|
|
214
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
215
|
+
wrapper.setProps({
|
|
216
|
+
message: {
|
|
217
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
218
|
+
contentHtml: `<p>${newContent}</p>`,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
await nextTick();
|
|
222
|
+
expect(renderGFM).toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('updates to the message', () => {
|
|
228
|
+
const content1 = 'chunk #1';
|
|
229
|
+
const content2 = ' chunk #2';
|
|
230
|
+
const content3 = ' chunk #3';
|
|
231
|
+
const chunk1 = {
|
|
232
|
+
...MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
233
|
+
content: content1,
|
|
234
|
+
chunkId: 1,
|
|
235
|
+
};
|
|
236
|
+
const chunk2 = {
|
|
237
|
+
...MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
238
|
+
content: content2,
|
|
239
|
+
chunkId: 2,
|
|
240
|
+
};
|
|
241
|
+
const chunk3 = {
|
|
242
|
+
...MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
243
|
+
content: content3,
|
|
244
|
+
chunkId: 3,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
beforeEach(() => {
|
|
248
|
+
createComponent();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('does not fail if the message has no chunkId', async () => {
|
|
252
|
+
// setProps is justified here because we are testing the component's
|
|
253
|
+
// reactive behavior which consistutes an exception
|
|
254
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
255
|
+
wrapper.setProps({
|
|
256
|
+
message: {
|
|
257
|
+
...MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
258
|
+
content: content1,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
await nextTick();
|
|
262
|
+
expect(findContent().text()).toBe(content1);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('renders chunks correctly when the chunks arrive out of order', async () => {
|
|
266
|
+
// setProps is justified here because we are testing the component's
|
|
267
|
+
// reactive behavior which consistutes an exception
|
|
268
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
269
|
+
wrapper.setProps({
|
|
270
|
+
message: chunk2,
|
|
271
|
+
});
|
|
272
|
+
await nextTick();
|
|
273
|
+
expect(findContent().text()).toBe('');
|
|
274
|
+
|
|
275
|
+
wrapper.setProps({
|
|
276
|
+
message: chunk1,
|
|
277
|
+
});
|
|
278
|
+
await nextTick();
|
|
279
|
+
expect(findContent().text()).toBe(content1 + content2);
|
|
280
|
+
|
|
281
|
+
wrapper.setProps({
|
|
282
|
+
message: chunk3,
|
|
283
|
+
});
|
|
284
|
+
await nextTick();
|
|
285
|
+
expect(findContent().text()).toBe(content1 + content2 + content3);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('renders the chunks as they arrive', async () => {
|
|
289
|
+
const consolidatedContent = content1 + content2;
|
|
290
|
+
|
|
291
|
+
// setProps is justified here because we are testing the component's
|
|
292
|
+
// reactive behavior which consistutes an exception
|
|
293
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
294
|
+
wrapper.setProps({
|
|
295
|
+
message: chunk1,
|
|
296
|
+
});
|
|
297
|
+
await nextTick();
|
|
298
|
+
expect(findContent().text()).toBe(content1);
|
|
299
|
+
|
|
300
|
+
wrapper.setProps({
|
|
301
|
+
message: chunk2,
|
|
302
|
+
});
|
|
303
|
+
await nextTick();
|
|
304
|
+
expect(findContent().text()).toBe(consolidatedContent);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('treats the initial message content as chunk if message has chunkId', async () => {
|
|
308
|
+
createComponent({
|
|
309
|
+
message: chunk1,
|
|
310
|
+
});
|
|
311
|
+
await nextTick();
|
|
312
|
+
expect(findContent().text()).toBe(content1);
|
|
313
|
+
|
|
314
|
+
// setProps is justified here because we are testing the component's
|
|
315
|
+
// reactive behavior which consistutes an exception
|
|
316
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
317
|
+
wrapper.setProps({
|
|
318
|
+
message: chunk2,
|
|
319
|
+
});
|
|
320
|
+
await nextTick();
|
|
321
|
+
expect(findContent().text()).toBe(content1 + content2);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('does not hydrate the chunk messages with GLFM', async () => {
|
|
325
|
+
createComponent({
|
|
326
|
+
propsData: {
|
|
327
|
+
message: chunk1,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
renderGFM.mockClear();
|
|
331
|
+
expect(renderGFM).not.toHaveBeenCalled();
|
|
332
|
+
|
|
333
|
+
// setProps is justified here because we are testing the component's
|
|
334
|
+
// reactive behavior which consistutes an exception
|
|
335
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
336
|
+
wrapper.setProps({
|
|
337
|
+
message: chunk2,
|
|
338
|
+
});
|
|
339
|
+
await nextTick();
|
|
340
|
+
expect(renderGFM).not.toHaveBeenCalled();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it.each`
|
|
344
|
+
content | contentHtml | errors | expectedContent
|
|
345
|
+
${'alpha'} | ${'beta'} | ${['foo', 'bar']} | ${'beta'}
|
|
346
|
+
${'alpha'} | ${'beta'} | ${[]} | ${'beta'}
|
|
347
|
+
${'alpha'} | ${''} | ${['foo', 'bar']} | ${'alpha'}
|
|
348
|
+
${'alpha'} | ${''} | ${[]} | ${'alpha'}
|
|
349
|
+
${''} | ${'beta'} | ${['foo', 'bar']} | ${'beta'}
|
|
350
|
+
${''} | ${'beta'} | ${[]} | ${'beta'}
|
|
351
|
+
${''} | ${''} | ${['foo', 'bar']} | ${'foo; bar'}
|
|
352
|
+
`(
|
|
353
|
+
'outputs "$expectedContent" and hydrates this content when content is "$content", contentHtml is "$contentHtml" and errors is "$errors" with "chunkId: null"',
|
|
354
|
+
async ({ content, contentHtml, errors, expectedContent } = {}) => {
|
|
355
|
+
createComponent({
|
|
356
|
+
propsData: {
|
|
357
|
+
message: chunk1,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
renderGFM.mockClear();
|
|
361
|
+
expect(renderGFM).not.toHaveBeenCalled();
|
|
362
|
+
|
|
363
|
+
// setProps is justified here because we are testing the component's
|
|
364
|
+
// reactive behavior which consistutes an exception
|
|
365
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
366
|
+
wrapper.setProps({
|
|
367
|
+
message: {
|
|
368
|
+
...MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
369
|
+
chunkId: null,
|
|
370
|
+
content,
|
|
371
|
+
contentHtml,
|
|
372
|
+
errors,
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
await nextTick();
|
|
376
|
+
expect(renderGFM).toHaveBeenCalled();
|
|
377
|
+
expect(findContent().text()).toBe(expectedContent);
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
});
|