@asgard-js/react 0.0.40 → 0.0.41-canary.2

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 (137) hide show
  1. package/dist/components/chatbot/chatbot-body/conversation-message-renderer.d.ts.map +1 -1
  2. package/dist/components/chatbot/chatbot-footer/chatbot-footer.d.ts.map +1 -1
  3. package/dist/components/templates/index.d.ts +1 -0
  4. package/dist/components/templates/index.d.ts.map +1 -1
  5. package/dist/components/templates/user-image-template/index.d.ts +2 -0
  6. package/dist/components/templates/user-image-template/index.d.ts.map +1 -0
  7. package/dist/components/templates/user-image-template/user-image-template.d.ts +12 -0
  8. package/dist/components/templates/user-image-template/user-image-template.d.ts.map +1 -0
  9. package/dist/context/asgard-service-context.d.ts +1 -0
  10. package/dist/context/asgard-service-context.d.ts.map +1 -1
  11. package/dist/hooks/use-channel.d.ts +1 -1
  12. package/dist/hooks/use-channel.d.ts.map +1 -1
  13. package/dist/index.js +17672 -18151
  14. package/dist/style.css +1 -1
  15. package/dist/utils/file-validation.d.ts +12 -0
  16. package/dist/utils/file-validation.d.ts.map +1 -0
  17. package/package.json +2 -2
  18. package/.babelrc +0 -12
  19. package/eslint.config.cjs +0 -12
  20. package/src/components/chatbot/chatbot-body/chatbot-body.module.scss +0 -13
  21. package/src/components/chatbot/chatbot-body/chatbot-body.tsx +0 -45
  22. package/src/components/chatbot/chatbot-body/conversation-message-renderer.tsx +0 -55
  23. package/src/components/chatbot/chatbot-body/index.ts +0 -1
  24. package/src/components/chatbot/chatbot-container/chatbot-container.module.scss +0 -41
  25. package/src/components/chatbot/chatbot-container/chatbot-container.tsx +0 -49
  26. package/src/components/chatbot/chatbot-container/chatbot-full-screen-container.tsx +0 -54
  27. package/src/components/chatbot/chatbot-footer/chatbot-footer.module.scss +0 -67
  28. package/src/components/chatbot/chatbot-footer/chatbot-footer.tsx +0 -140
  29. package/src/components/chatbot/chatbot-footer/index.ts +0 -1
  30. package/src/components/chatbot/chatbot-footer/speech-input-button.tsx +0 -132
  31. package/src/components/chatbot/chatbot-header/chatbot-header.module.scss +0 -48
  32. package/src/components/chatbot/chatbot-header/chatbot-header.tsx +0 -98
  33. package/src/components/chatbot/chatbot-header/index.ts +0 -1
  34. package/src/components/chatbot/chatbot.spec.tsx +0 -8
  35. package/src/components/chatbot/chatbot.tsx +0 -121
  36. package/src/components/chatbot/profile-icon.tsx +0 -26
  37. package/src/components/index.ts +0 -2
  38. package/src/components/templates/avatar/avatar.module.scss +0 -6
  39. package/src/components/templates/avatar/avatar.tsx +0 -28
  40. package/src/components/templates/avatar/index.ts +0 -1
  41. package/src/components/templates/button-template/button-template.module.scss +0 -0
  42. package/src/components/templates/button-template/button-template.tsx +0 -45
  43. package/src/components/templates/button-template/card.module.scss +0 -58
  44. package/src/components/templates/button-template/card.spec.tsx +0 -213
  45. package/src/components/templates/button-template/card.tsx +0 -123
  46. package/src/components/templates/button-template/index.ts +0 -1
  47. package/src/components/templates/carousel-template/carousel-template.module.scss +0 -15
  48. package/src/components/templates/carousel-template/carousel-template.tsx +0 -49
  49. package/src/components/templates/carousel-template/index.ts +0 -1
  50. package/src/components/templates/chart-template/chart-template.module.scss +0 -52
  51. package/src/components/templates/chart-template/chart-template.tsx +0 -75
  52. package/src/components/templates/chart-template/index.ts +0 -1
  53. package/src/components/templates/hint-template/hint-template.module.scss +0 -43
  54. package/src/components/templates/hint-template/hint-template.tsx +0 -76
  55. package/src/components/templates/hint-template/index.ts +0 -1
  56. package/src/components/templates/image-template/image-template.module.scss +0 -67
  57. package/src/components/templates/image-template/image-template.tsx +0 -58
  58. package/src/components/templates/image-template/index.ts +0 -1
  59. package/src/components/templates/index.ts +0 -10
  60. package/src/components/templates/quick-replies/index.ts +0 -1
  61. package/src/components/templates/quick-replies/quick-replies.module.scss +0 -16
  62. package/src/components/templates/quick-replies/quick-replies.tsx +0 -47
  63. package/src/components/templates/template-box/index.ts +0 -2
  64. package/src/components/templates/template-box/template-box-content.module.scss +0 -13
  65. package/src/components/templates/template-box/template-box-content.tsx +0 -30
  66. package/src/components/templates/template-box/template-box.module.scss +0 -19
  67. package/src/components/templates/template-box/template-box.tsx +0 -48
  68. package/src/components/templates/text-template/bot-typing-box.tsx +0 -81
  69. package/src/components/templates/text-template/bot-typing-placeholder.tsx +0 -28
  70. package/src/components/templates/text-template/index.ts +0 -3
  71. package/src/components/templates/text-template/text-template.module.scss +0 -131
  72. package/src/components/templates/text-template/text-template.tsx +0 -94
  73. package/src/components/templates/text-template/use-react-markdown-renderer.spec.tsx +0 -758
  74. package/src/components/templates/time/index.ts +0 -1
  75. package/src/components/templates/time/time.module.scss +0 -6
  76. package/src/components/templates/time/time.tsx +0 -34
  77. package/src/context/asgard-app-initialization-context.tsx +0 -154
  78. package/src/context/asgard-service-context.tsx +0 -145
  79. package/src/context/asgard-template-context.tsx +0 -83
  80. package/src/context/asgard-theme-context.tsx +0 -546
  81. package/src/context/index.ts +0 -4
  82. package/src/hooks/index.ts +0 -11
  83. package/src/hooks/use-asgard-service-client.ts +0 -68
  84. package/src/hooks/use-channel.ts +0 -154
  85. package/src/hooks/use-debounce.ts +0 -18
  86. package/src/hooks/use-deep-compare-memo.ts +0 -19
  87. package/src/hooks/use-is-on-screen-keyboard-open.ts +0 -43
  88. package/src/hooks/use-on-screen-keyboard-scroll-fix.ts +0 -15
  89. package/src/hooks/use-prevent-over-scrolling.ts +0 -77
  90. package/src/hooks/use-react-markdown-renderer.tsx +0 -278
  91. package/src/hooks/use-resize-observer.tsx +0 -27
  92. package/src/hooks/use-update-vh.ts +0 -30
  93. package/src/hooks/use-viewport-size.ts +0 -51
  94. package/src/icons/add_a_photo.svg +0 -3
  95. package/src/icons/bot.svg +0 -14
  96. package/src/icons/close.svg +0 -3
  97. package/src/icons/distance.svg +0 -3
  98. package/src/icons/mic.svg +0 -3
  99. package/src/icons/photo_library.svg +0 -3
  100. package/src/icons/profile.svg +0 -28
  101. package/src/icons/refresh.svg +0 -3
  102. package/src/icons/send.svg +0 -3
  103. package/src/icons/stop.svg +0 -22
  104. package/src/icons/volume_up.svg +0 -3
  105. package/src/index.ts +0 -4
  106. package/src/models/bot-provider.ts +0 -108
  107. package/src/styles/_index.scss +0 -1
  108. package/src/styles/_styles.scss +0 -11
  109. package/src/styles/colors/_colors.scss +0 -10
  110. package/src/styles/colors/_index.scss +0 -1
  111. package/src/styles/colors/_variables.scss +0 -72
  112. package/src/styles/palette/_index.scss +0 -1
  113. package/src/styles/palette/_palette.scss +0 -42
  114. package/src/styles/palette/_variables.scss +0 -40
  115. package/src/styles/radius/_index.scss +0 -1
  116. package/src/styles/radius/_radius.scss +0 -8
  117. package/src/styles/radius/_variables.scss +0 -12
  118. package/src/styles/spacing/_index.scss +0 -1
  119. package/src/styles/spacing/_spacing.scss +0 -8
  120. package/src/styles/spacing/_variables.scss +0 -13
  121. package/src/styles/utils/_index.scss +0 -1
  122. package/src/styles/utils/_map.scss +0 -22
  123. package/src/test-setup.ts +0 -1
  124. package/src/utils/color-utils.ts +0 -52
  125. package/src/utils/deep-merge.ts +0 -26
  126. package/src/utils/extractors.ts +0 -20
  127. package/src/utils/format-time.ts +0 -8
  128. package/src/utils/index.ts +0 -1
  129. package/src/utils/is.ts +0 -72
  130. package/src/utils/selectors.ts +0 -7
  131. package/src/utils/uri-validation.spec.ts +0 -208
  132. package/src/utils/uri-validation.ts +0 -103
  133. package/tsconfig.json +0 -16
  134. package/tsconfig.lib.json +0 -63
  135. package/tsconfig.spec.json +0 -36
  136. package/tsconfig.tsbuildinfo +0 -1
  137. package/vite.config.ts +0 -63
@@ -1,48 +0,0 @@
1
- .chatbot_header {
2
- border-bottom: 1px solid #434343;
3
-
4
- .chatbot_header__content {
5
- margin: 0 auto;
6
- max-width: 1200px;
7
- padding: var(--asg-spacing-4);
8
- display: flex;
9
- justify-content: space-between;
10
- }
11
- }
12
-
13
- .chatbot_header__title {
14
- display: flex;
15
- flex-direction: row;
16
- align-items: center;
17
- gap: var(--asg-spacing-2);
18
-
19
- > h4 {
20
- margin: 0;
21
- color: white;
22
- }
23
- }
24
-
25
- .chatbot_header__extra {
26
- display: flex;
27
- flex-direction: row;
28
- align-items: center;
29
- gap: var(--asg-spacing-3);
30
-
31
- > div {
32
- width: 24px;
33
- height: 24px;
34
- display: flex;
35
- justify-content: center;
36
- align-items: center;
37
-
38
- > svg {
39
- width: 15px;
40
- height: 15px;
41
- cursor: pointer;
42
-
43
- &:hover {
44
- opacity: 0.5;
45
- }
46
- }
47
- }
48
- }
@@ -1,98 +0,0 @@
1
- import { MouseEventHandler, ReactNode, useCallback, useMemo } from 'react';
2
- import styles from './chatbot-header.module.scss';
3
- import { ProfileIcon } from '../profile-icon';
4
- import RefreshSvg from '../../../icons/refresh.svg?react';
5
- import CloseSvg from '../../../icons/close.svg?react';
6
- import {
7
- useAsgardAppInitializationContext,
8
- useAsgardThemeContext,
9
- useAsgardContext,
10
- } from '../../../context/';
11
- import clsx from 'clsx';
12
-
13
- interface ChatbotHeaderProps {
14
- title?: string;
15
- customActions?: ReactNode[];
16
- maintainConnectionWhenClosed?: boolean;
17
- onClose?: () => void;
18
- onReset?: () => void;
19
- }
20
-
21
- export function ChatbotHeader(props: ChatbotHeaderProps): ReactNode {
22
- const {
23
- title,
24
- onReset,
25
- onClose,
26
- customActions,
27
- maintainConnectionWhenClosed,
28
- } = props;
29
-
30
- const { chatbot } = useAsgardThemeContext();
31
- const {
32
- data: { annotations },
33
- } = useAsgardAppInitializationContext();
34
-
35
- const { avatar, isResetting, resetChannel, closeChannel } =
36
- useAsgardContext();
37
-
38
- const contentStyles = useMemo(
39
- () => ({
40
- maxWidth: chatbot?.contentMaxWidth ?? '1200px',
41
- borderBottomColor: chatbot?.borderColor,
42
- }),
43
- [chatbot]
44
- );
45
-
46
- const _onReset = useCallback<MouseEventHandler<HTMLDivElement>>(
47
- (e) => {
48
- if (!isResetting) {
49
- e.stopPropagation();
50
- onReset?.();
51
- resetChannel?.();
52
- }
53
- },
54
- [isResetting, onReset, resetChannel]
55
- );
56
-
57
- const _onClose = useCallback<MouseEventHandler<HTMLDivElement>>(
58
- (e) => {
59
- if (!isResetting) {
60
- e.stopPropagation();
61
- onClose?.();
62
-
63
- if (!maintainConnectionWhenClosed) {
64
- closeChannel?.();
65
- }
66
- }
67
- },
68
- [isResetting, onClose, closeChannel, maintainConnectionWhenClosed]
69
- );
70
-
71
- return (
72
- <div
73
- className={clsx('asgard-chatbot-header', styles.chatbot_header)}
74
- style={chatbot?.header?.style}
75
- >
76
- <div className={styles.chatbot_header__content} style={contentStyles}>
77
- <div className={styles.chatbot_header__title}>
78
- <ProfileIcon avatar={avatar} />
79
- <h4 style={chatbot?.header?.title?.style}>
80
- {annotations?.embedConfig?.title || title || 'Bot'}
81
- </h4>
82
- </div>
83
- <div
84
- className={styles.chatbot_header__extra}
85
- style={chatbot?.header?.actionButton?.style}
86
- >
87
- {customActions}
88
- <div onClick={_onReset}>
89
- <RefreshSvg />
90
- </div>
91
- <div onClick={_onClose}>
92
- <CloseSvg />
93
- </div>
94
- </div>
95
- </div>
96
- </div>
97
- );
98
- }
@@ -1 +0,0 @@
1
- export * from './chatbot-header';
@@ -1,8 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
-
3
- describe('Chatbot Component', () => {
4
- it('should have a basic test placeholder', () => {
5
- // Basic test to verify test environment works
6
- expect(true).toBe(true);
7
- });
8
- });
@@ -1,121 +0,0 @@
1
- import { forwardRef, ForwardedRef, ReactNode, CSSProperties } from 'react';
2
- import { ClientConfig, ConversationMessage } from '@asgard-js/core';
3
- import {
4
- AsgardThemeContextProvider,
5
- AsgardThemeContextValue,
6
- } from '../../context/asgard-theme-context';
7
- import {
8
- AsgardServiceContextProvider,
9
- AsgardServiceContextValue,
10
- AsgardTemplateContextProvider,
11
- AsgardTemplateContextValue,
12
- AsgardAppInitializationContextProvider,
13
- AsgardServiceContextProviderProps,
14
- } from '../../context';
15
- import { ChatbotHeader } from './chatbot-header';
16
- import { ChatbotBody } from './chatbot-body';
17
- import { ChatbotFooter } from './chatbot-footer';
18
- import { ChatbotContainer } from './chatbot-container/chatbot-container';
19
-
20
- interface ChatbotProps extends AsgardTemplateContextValue {
21
- className?: string;
22
- style?: CSSProperties;
23
- title?: string;
24
- customActions?: ReactNode[];
25
- theme?: Partial<AsgardThemeContextValue>;
26
- config: ClientConfig;
27
- customChannelId: string;
28
- initMessages?: ConversationMessage[];
29
- onSseMessage?: AsgardServiceContextProviderProps['onSseMessage'];
30
- fullScreen?: boolean;
31
- avatar?: string;
32
- botTypingPlaceholder?: string;
33
- inputPlaceholder?: string;
34
- enableLoadConfigFromService?: boolean;
35
- maintainConnectionWhenClosed?: boolean;
36
- asyncInitializers?: Record<string, () => Promise<unknown>>;
37
- onReset?: () => void;
38
- onClose?: () => void;
39
- loadingComponent?: ReactNode;
40
- defaultLinkTarget?: '_blank' | '_self' | '_parent' | '_top';
41
- }
42
-
43
- export interface ChatbotRef {
44
- serviceContext?: AsgardServiceContextValue;
45
- }
46
-
47
- export const Chatbot = forwardRef(function Chatbot(
48
- props: ChatbotProps,
49
- ref: ForwardedRef<ChatbotRef>
50
- ): ReactNode {
51
- const {
52
- title,
53
- customActions,
54
- theme,
55
- config,
56
- customChannelId,
57
- initMessages,
58
- onSseMessage,
59
- fullScreen = false,
60
- avatar,
61
- botTypingPlaceholder,
62
- inputPlaceholder,
63
- enableLoadConfigFromService = false,
64
- maintainConnectionWhenClosed = false,
65
- asyncInitializers = {},
66
- loadingComponent,
67
- onReset,
68
- onClose,
69
- onTemplateBtnClick,
70
- onErrorClick,
71
- errorMessageRenderer,
72
- className,
73
- style,
74
- defaultLinkTarget,
75
- } = props;
76
-
77
- return (
78
- <AsgardAppInitializationContextProvider
79
- enabled={enableLoadConfigFromService}
80
- config={config}
81
- asyncInitializers={asyncInitializers}
82
- loadingComponent={loadingComponent}
83
- >
84
- <AsgardThemeContextProvider theme={theme}>
85
- <AsgardServiceContextProvider
86
- parentRef={ref}
87
- avatar={avatar}
88
- config={config}
89
- customChannelId={customChannelId}
90
- initMessages={initMessages}
91
- onSseMessage={onSseMessage}
92
- botTypingPlaceholder={botTypingPlaceholder}
93
- inputPlaceholder={inputPlaceholder}
94
- >
95
- <ChatbotContainer
96
- fullScreen={fullScreen}
97
- className={className}
98
- style={style}
99
- >
100
- <ChatbotHeader
101
- title={title}
102
- onReset={onReset}
103
- onClose={onClose}
104
- customActions={customActions}
105
- maintainConnectionWhenClosed={maintainConnectionWhenClosed}
106
- />
107
- <AsgardTemplateContextProvider
108
- onErrorClick={onErrorClick}
109
- errorMessageRenderer={errorMessageRenderer}
110
- onTemplateBtnClick={onTemplateBtnClick}
111
- defaultLinkTarget={defaultLinkTarget}
112
- >
113
- <ChatbotBody />
114
- </AsgardTemplateContextProvider>
115
- <ChatbotFooter />
116
- </ChatbotContainer>
117
- </AsgardServiceContextProvider>
118
- </AsgardThemeContextProvider>
119
- </AsgardAppInitializationContextProvider>
120
- );
121
- });
@@ -1,26 +0,0 @@
1
- import { ReactNode } from 'react';
2
- import ProfileSvg from '../../icons/profile.svg?react';
3
-
4
- interface ProfileIconProps {
5
- avatar?: string;
6
- }
7
-
8
- export function ProfileIcon(props: ProfileIconProps): ReactNode {
9
- const { avatar } = props;
10
-
11
- if (avatar) {
12
- return (
13
- <img
14
- src={avatar}
15
- alt="avatar"
16
- style={{
17
- width: 33,
18
- height: 32,
19
- borderRadius: '50%',
20
- }}
21
- />
22
- );
23
- }
24
-
25
- return <ProfileSvg />;
26
- }
@@ -1,2 +0,0 @@
1
- export * from './chatbot/chatbot';
2
- export * from './templates';
@@ -1,6 +0,0 @@
1
- .bot_avatar {
2
- flex: 0 0 auto;
3
- width: 24px;
4
- height: 24px;
5
- border-radius: 50%;
6
- }
@@ -1,28 +0,0 @@
1
- import { memo, ReactNode } from 'react';
2
- import styles from './avatar.module.scss';
3
- import BotSvg from '../../../icons/bot.svg?react';
4
- import clsx from 'clsx';
5
-
6
- interface AvatarProps {
7
- avatar?: string;
8
- }
9
-
10
- export const Avatar = memo((props: AvatarProps): ReactNode => {
11
- const { avatar } = props;
12
-
13
- if (avatar) {
14
- return (
15
- <img
16
- src={avatar}
17
- alt="Bot Avatar"
18
- className={clsx('asgard-avatar', styles.bot_avatar)}
19
- />
20
- );
21
- }
22
-
23
- return (
24
- <div className={clsx('asgard-avatar', styles.bot_avatar)}>
25
- <BotSvg />
26
- </div>
27
- );
28
- });
@@ -1 +0,0 @@
1
- export * from './avatar';
@@ -1,45 +0,0 @@
1
- import { ButtonMessageTemplate, ConversationBotMessage } from '@asgard-js/core';
2
- import { ReactNode } from 'react';
3
- import { TemplateBox, TemplateBoxContent } from '../template-box';
4
- import { Avatar } from '../avatar';
5
- import { Card } from './card';
6
- import { useAsgardContext } from '../../../context/asgard-service-context';
7
- import { useAsgardThemeContext } from '../../../context/asgard-theme-context';
8
-
9
- interface ButtonTemplateProps {
10
- message: ConversationBotMessage;
11
- }
12
-
13
- export function ButtonTemplate(props: ButtonTemplateProps): ReactNode {
14
- const { message } = props;
15
-
16
- const { template: themeTemplate } = useAsgardThemeContext();
17
-
18
- const { avatar } = useAsgardContext();
19
-
20
- const template = message.message.template as ButtonMessageTemplate;
21
-
22
- return (
23
- <TemplateBox
24
- className="asgard-button-template"
25
- type="bot"
26
- direction="horizontal"
27
- style={themeTemplate?.ButtonMessageTemplate?.style}
28
- >
29
- <Avatar avatar={avatar} />
30
- <TemplateBoxContent
31
- time={message.time}
32
- quickReplies={template?.quickReplies}
33
- >
34
- <Card
35
- template={template}
36
- customStyle={{
37
- button: {
38
- style: themeTemplate?.ButtonMessageTemplate?.button?.style ?? {},
39
- },
40
- }}
41
- />
42
- </TemplateBoxContent>
43
- </TemplateBox>
44
- );
45
- }
@@ -1,58 +0,0 @@
1
- .card_root {
2
- width: 255px;
3
- height: 368px;
4
- max-height: 380px;
5
- border-radius: 8px;
6
- background: rgba(51, 51, 51, 1);
7
- overflow: hidden;
8
- display: grid;
9
- grid-template-rows: max-content auto;
10
- }
11
-
12
- .card_content {
13
- display: grid;
14
- grid-template-rows: 1.5em auto max-content;
15
- gap: var(--asg-spacing-2);
16
- padding: 12px;
17
- line-height: 1.5em;
18
- }
19
-
20
- .card_title {
21
- color: white;
22
- margin: 0;
23
- font-size: 15px;
24
- }
25
-
26
- .card_description {
27
- color: rgba(140, 140, 140, 1);
28
- font-size: 13px;
29
- font-weight: 400;
30
- max-height: 4.5em;
31
- overflow: hidden;
32
- text-overflow: ellipsis;
33
- display: -webkit-box;
34
- line-clamp: 3;
35
- -webkit-line-clamp: 3;
36
- -webkit-box-orient: vertical;
37
- text-underline-position: from-font;
38
- text-decoration-skip-ink: none;
39
- }
40
-
41
- .card_actions {
42
- display: flex;
43
- flex-direction: column;
44
- gap: 8px;
45
-
46
- > button {
47
- width: 100%;
48
- height: 32px;
49
- line-height: 32px;
50
- text-align: center;
51
- border: none;
52
- border-radius: 4px;
53
- background: rgba(71, 103, 235, 1);
54
- color: white;
55
- font-size: 15px;
56
- cursor: pointer;
57
- }
58
- }
@@ -1,213 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { render, screen, fireEvent } from '@testing-library/react';
3
- import { Card } from './card';
4
- import { MessageTemplateType } from '@asgard-js/core';
5
- import { useAsgardContext } from '../../../context/asgard-service-context';
6
- import { useAsgardTemplateContext } from '../../../context/asgard-template-context';
7
- import * as uriValidation from '../../../utils/uri-validation';
8
-
9
- // Mock the contexts
10
- vi.mock('../../../context/asgard-service-context');
11
- vi.mock('../../../context/asgard-template-context');
12
-
13
- // Mock the URI validation utility
14
- vi.mock('../../../utils/uri-validation');
15
-
16
- const mockUseAsgardContext = vi.mocked(useAsgardContext);
17
- const mockUseAsgardTemplateContext = vi.mocked(useAsgardTemplateContext);
18
- const mockSafeWindowOpen = vi.mocked(uriValidation.safeWindowOpen);
19
-
20
- describe('Card Component - Security Tests', () => {
21
- const mockSendMessage = vi.fn();
22
- const mockOnTemplateBtnClick = vi.fn();
23
- const defaultLinkTarget = '_blank';
24
-
25
- // Define malicious URI as variable to avoid ESLint script URL warning
26
- // eslint-disable-next-line no-script-url
27
- const maliciousJsUri = 'javascript:alert("xss")';
28
-
29
- const baseTemplate = {
30
- type: MessageTemplateType.BUTTON,
31
- title: 'Test Card',
32
- text: 'Test description',
33
- thumbnailImageUrl: 'https://example.com/image.jpg',
34
- imageAspectRatio: 'rectangle' as const,
35
- imageSize: 'cover' as const,
36
- imageBackgroundColor: '#ffffff',
37
- defaultAction: {
38
- type: 'message' as const,
39
- text: 'Default action',
40
- },
41
- quickReplies: [],
42
- buttons: [
43
- {
44
- label: 'Safe Link',
45
- action: {
46
- type: 'uri' as const,
47
- uri: 'https://example.com',
48
- },
49
- },
50
- {
51
- label: 'Malicious Link',
52
- action: {
53
- type: 'uri' as const,
54
- uri: maliciousJsUri,
55
- },
56
- },
57
- ],
58
- };
59
-
60
- beforeEach(() => {
61
- mockUseAsgardContext.mockReturnValue({
62
- sendMessage: mockSendMessage,
63
- client: null,
64
- isOpen: false,
65
- isResetting: false,
66
- isConnecting: false,
67
- conversation: null,
68
- resetChannel: vi.fn(),
69
- closeChannel: vi.fn(),
70
- avatar: null,
71
- });
72
-
73
- mockUseAsgardTemplateContext.mockReturnValue({
74
- onTemplateBtnClick: mockOnTemplateBtnClick,
75
- defaultLinkTarget,
76
- onErrorClick: undefined,
77
- errorMessageRenderer: undefined,
78
- });
79
-
80
- mockSafeWindowOpen.mockReturnValue(null);
81
- });
82
-
83
- afterEach(() => {
84
- vi.clearAllMocks();
85
- });
86
-
87
- describe('URI security validation', () => {
88
- it('should call safeWindowOpen for URI actions instead of window.open directly', () => {
89
- render(<Card template={baseTemplate} />);
90
-
91
- const safeButton = screen.getByText('Safe Link');
92
- fireEvent.click(safeButton);
93
-
94
- expect(mockSafeWindowOpen).toHaveBeenCalledWith(
95
- 'https://example.com',
96
- '_blank'
97
- );
98
- });
99
-
100
- it('should call safeWindowOpen for malicious URIs (letting validation utility handle security)', () => {
101
- render(<Card template={baseTemplate} />);
102
-
103
- const maliciousButton = screen.getByText('Malicious Link');
104
- fireEvent.click(maliciousButton);
105
-
106
- expect(mockSafeWindowOpen).toHaveBeenCalledWith(
107
- maliciousJsUri,
108
- '_blank'
109
- );
110
- });
111
-
112
- it('should use action target if provided', () => {
113
- const templateWithTarget = {
114
- ...baseTemplate,
115
- buttons: [
116
- {
117
- label: 'Link with Target',
118
- action: {
119
- type: 'uri' as const,
120
- uri: 'https://example.com',
121
- target: '_self' as const,
122
- },
123
- },
124
- ],
125
- };
126
-
127
- render(<Card template={templateWithTarget} />);
128
-
129
- const button = screen.getByText('Link with Target');
130
- fireEvent.click(button);
131
-
132
- expect(mockSafeWindowOpen).toHaveBeenCalledWith(
133
- 'https://example.com',
134
- '_self'
135
- );
136
- });
137
-
138
- it('should fallback to defaultLinkTarget when no action target', () => {
139
- mockUseAsgardTemplateContext.mockReturnValue({
140
- onTemplateBtnClick: mockOnTemplateBtnClick,
141
- defaultLinkTarget: '_parent',
142
- });
143
-
144
- render(<Card template={baseTemplate} />);
145
-
146
- const safeButton = screen.getByText('Safe Link');
147
- fireEvent.click(safeButton);
148
-
149
- expect(mockSafeWindowOpen).toHaveBeenCalledWith(
150
- 'https://example.com',
151
- '_parent'
152
- );
153
- });
154
-
155
- it('should fallback to _blank when no action target or defaultLinkTarget', () => {
156
- mockUseAsgardTemplateContext.mockReturnValue({
157
- onTemplateBtnClick: mockOnTemplateBtnClick,
158
- defaultLinkTarget: undefined,
159
- });
160
-
161
- render(<Card template={baseTemplate} />);
162
-
163
- const safeButton = screen.getByText('Safe Link');
164
- fireEvent.click(safeButton);
165
-
166
- expect(mockSafeWindowOpen).toHaveBeenCalledWith(
167
- 'https://example.com',
168
- '_blank'
169
- );
170
- });
171
-
172
- it('should handle uppercase URI action type', () => {
173
- const templateWithUppercase = {
174
- ...baseTemplate,
175
- buttons: [
176
- {
177
- label: 'Uppercase URI',
178
- action: {
179
- type: 'URI' as const,
180
- uri: 'https://example.com',
181
- },
182
- },
183
- ],
184
- };
185
-
186
- render(<Card template={templateWithUppercase} />);
187
-
188
- const button = screen.getByText('Uppercase URI');
189
- fireEvent.click(button);
190
-
191
- expect(mockSafeWindowOpen).toHaveBeenCalledWith(
192
- 'https://example.com',
193
- '_blank'
194
- );
195
- });
196
- });
197
-
198
- describe('basic rendering', () => {
199
- it('should render card with title and description', () => {
200
- render(<Card template={baseTemplate} />);
201
-
202
- expect(screen.getByText('Test Card')).toBeDefined();
203
- expect(screen.getByText('Test description')).toBeDefined();
204
- });
205
-
206
- it('should render buttons', () => {
207
- render(<Card template={baseTemplate} />);
208
-
209
- expect(screen.getByText('Safe Link')).toBeDefined();
210
- expect(screen.getByText('Malicious Link')).toBeDefined();
211
- });
212
- });
213
- });