@asgard-js/react 0.0.36 → 0.0.37-canary.1

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 (128) hide show
  1. package/dist/components/chatbot/chatbot-footer/chatbot-footer.d.ts.map +1 -1
  2. package/dist/context/asgard-service-context.d.ts.map +1 -1
  3. package/dist/context/asgard-theme-context.d.ts.map +1 -1
  4. package/dist/hooks/use-react-markdown-renderer.d.ts.map +1 -1
  5. package/dist/index.js +18160 -18881
  6. package/package.json +2 -2
  7. package/.babelrc +0 -12
  8. package/dist/utils/color-utils.d.ts +0 -18
  9. package/dist/utils/color-utils.d.ts.map +0 -1
  10. package/eslint.config.cjs +0 -12
  11. package/src/components/chatbot/chatbot-body/chatbot-body.module.scss +0 -13
  12. package/src/components/chatbot/chatbot-body/chatbot-body.tsx +0 -45
  13. package/src/components/chatbot/chatbot-body/conversation-message-renderer.tsx +0 -55
  14. package/src/components/chatbot/chatbot-body/index.ts +0 -1
  15. package/src/components/chatbot/chatbot-container/chatbot-container.module.scss +0 -41
  16. package/src/components/chatbot/chatbot-container/chatbot-container.tsx +0 -49
  17. package/src/components/chatbot/chatbot-container/chatbot-full-screen-container.tsx +0 -54
  18. package/src/components/chatbot/chatbot-footer/chatbot-footer.module.scss +0 -67
  19. package/src/components/chatbot/chatbot-footer/chatbot-footer.tsx +0 -144
  20. package/src/components/chatbot/chatbot-footer/index.ts +0 -1
  21. package/src/components/chatbot/chatbot-footer/speech-input-button.tsx +0 -132
  22. package/src/components/chatbot/chatbot-header/chatbot-header.module.scss +0 -48
  23. package/src/components/chatbot/chatbot-header/chatbot-header.tsx +0 -98
  24. package/src/components/chatbot/chatbot-header/index.ts +0 -1
  25. package/src/components/chatbot/chatbot.spec.tsx +0 -8
  26. package/src/components/chatbot/chatbot.tsx +0 -118
  27. package/src/components/chatbot/profile-icon.tsx +0 -26
  28. package/src/components/index.ts +0 -2
  29. package/src/components/templates/avatar/avatar.module.scss +0 -6
  30. package/src/components/templates/avatar/avatar.tsx +0 -28
  31. package/src/components/templates/avatar/index.ts +0 -1
  32. package/src/components/templates/button-template/button-template.module.scss +0 -0
  33. package/src/components/templates/button-template/button-template.tsx +0 -45
  34. package/src/components/templates/button-template/card.module.scss +0 -58
  35. package/src/components/templates/button-template/card.spec.tsx +0 -213
  36. package/src/components/templates/button-template/card.tsx +0 -123
  37. package/src/components/templates/button-template/index.ts +0 -1
  38. package/src/components/templates/carousel-template/carousel-template.module.scss +0 -15
  39. package/src/components/templates/carousel-template/carousel-template.tsx +0 -49
  40. package/src/components/templates/carousel-template/index.ts +0 -1
  41. package/src/components/templates/chart-template/chart-template.module.scss +0 -52
  42. package/src/components/templates/chart-template/chart-template.tsx +0 -75
  43. package/src/components/templates/chart-template/index.ts +0 -1
  44. package/src/components/templates/hint-template/hint-template.module.scss +0 -39
  45. package/src/components/templates/hint-template/hint-template.tsx +0 -71
  46. package/src/components/templates/hint-template/index.ts +0 -1
  47. package/src/components/templates/image-template/image-template.module.scss +0 -67
  48. package/src/components/templates/image-template/image-template.tsx +0 -58
  49. package/src/components/templates/image-template/index.ts +0 -1
  50. package/src/components/templates/index.ts +0 -10
  51. package/src/components/templates/quick-replies/index.ts +0 -1
  52. package/src/components/templates/quick-replies/quick-replies.module.scss +0 -16
  53. package/src/components/templates/quick-replies/quick-replies.tsx +0 -44
  54. package/src/components/templates/template-box/index.ts +0 -2
  55. package/src/components/templates/template-box/template-box-content.module.scss +0 -13
  56. package/src/components/templates/template-box/template-box-content.tsx +0 -30
  57. package/src/components/templates/template-box/template-box.module.scss +0 -19
  58. package/src/components/templates/template-box/template-box.tsx +0 -48
  59. package/src/components/templates/text-template/bot-typing-box.tsx +0 -81
  60. package/src/components/templates/text-template/bot-typing-placeholder.tsx +0 -28
  61. package/src/components/templates/text-template/index.ts +0 -3
  62. package/src/components/templates/text-template/text-template.module.scss +0 -131
  63. package/src/components/templates/text-template/text-template.tsx +0 -90
  64. package/src/components/templates/text-template/use-react-markdown-renderer.spec.tsx +0 -758
  65. package/src/components/templates/time/index.ts +0 -1
  66. package/src/components/templates/time/time.module.scss +0 -6
  67. package/src/components/templates/time/time.tsx +0 -34
  68. package/src/context/asgard-app-initialization-context.tsx +0 -154
  69. package/src/context/asgard-service-context.tsx +0 -148
  70. package/src/context/asgard-template-context.tsx +0 -83
  71. package/src/context/asgard-theme-context.tsx +0 -417
  72. package/src/context/index.ts +0 -4
  73. package/src/hooks/index.ts +0 -11
  74. package/src/hooks/use-asgard-service-client.ts +0 -68
  75. package/src/hooks/use-channel.ts +0 -154
  76. package/src/hooks/use-debounce.ts +0 -18
  77. package/src/hooks/use-deep-compare-memo.ts +0 -19
  78. package/src/hooks/use-is-on-screen-keyboard-open.ts +0 -43
  79. package/src/hooks/use-on-screen-keyboard-scroll-fix.ts +0 -15
  80. package/src/hooks/use-prevent-over-scrolling.ts +0 -77
  81. package/src/hooks/use-react-markdown-renderer.tsx +0 -272
  82. package/src/hooks/use-resize-observer.tsx +0 -27
  83. package/src/hooks/use-update-vh.ts +0 -30
  84. package/src/hooks/use-viewport-size.ts +0 -51
  85. package/src/icons/add_a_photo.svg +0 -3
  86. package/src/icons/bot.svg +0 -14
  87. package/src/icons/close.svg +0 -3
  88. package/src/icons/distance.svg +0 -3
  89. package/src/icons/mic.svg +0 -3
  90. package/src/icons/photo_library.svg +0 -3
  91. package/src/icons/profile.svg +0 -28
  92. package/src/icons/refresh.svg +0 -3
  93. package/src/icons/send.svg +0 -3
  94. package/src/icons/stop.svg +0 -22
  95. package/src/icons/volume_up.svg +0 -3
  96. package/src/index.ts +0 -4
  97. package/src/models/bot-provider.ts +0 -108
  98. package/src/styles/_index.scss +0 -1
  99. package/src/styles/_styles.scss +0 -11
  100. package/src/styles/colors/_colors.scss +0 -10
  101. package/src/styles/colors/_index.scss +0 -1
  102. package/src/styles/colors/_variables.scss +0 -72
  103. package/src/styles/palette/_index.scss +0 -1
  104. package/src/styles/palette/_palette.scss +0 -42
  105. package/src/styles/palette/_variables.scss +0 -40
  106. package/src/styles/radius/_index.scss +0 -1
  107. package/src/styles/radius/_radius.scss +0 -8
  108. package/src/styles/radius/_variables.scss +0 -12
  109. package/src/styles/spacing/_index.scss +0 -1
  110. package/src/styles/spacing/_spacing.scss +0 -8
  111. package/src/styles/spacing/_variables.scss +0 -13
  112. package/src/styles/utils/_index.scss +0 -1
  113. package/src/styles/utils/_map.scss +0 -22
  114. package/src/test-setup.ts +0 -1
  115. package/src/utils/color-utils.ts +0 -38
  116. package/src/utils/deep-merge.ts +0 -26
  117. package/src/utils/extractors.ts +0 -20
  118. package/src/utils/format-time.ts +0 -8
  119. package/src/utils/index.ts +0 -1
  120. package/src/utils/is.ts +0 -72
  121. package/src/utils/selectors.ts +0 -7
  122. package/src/utils/uri-validation.spec.ts +0 -208
  123. package/src/utils/uri-validation.ts +0 -103
  124. package/tsconfig.json +0 -16
  125. package/tsconfig.lib.json +0 -63
  126. package/tsconfig.spec.json +0 -36
  127. package/tsconfig.tsbuildinfo +0 -1
  128. 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,118 +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
- enableLoadConfigFromService?: boolean;
34
- maintainConnectionWhenClosed?: boolean;
35
- asyncInitializers?: Record<string, () => Promise<unknown>>;
36
- onReset?: () => void;
37
- onClose?: () => void;
38
- loadingComponent?: ReactNode;
39
- defaultLinkTarget?: '_blank' | '_self' | '_parent' | '_top';
40
- }
41
-
42
- export interface ChatbotRef {
43
- serviceContext?: AsgardServiceContextValue;
44
- }
45
-
46
- export const Chatbot = forwardRef(function Chatbot(
47
- props: ChatbotProps,
48
- ref: ForwardedRef<ChatbotRef>
49
- ): ReactNode {
50
- const {
51
- title,
52
- customActions,
53
- theme,
54
- config,
55
- customChannelId,
56
- initMessages,
57
- onSseMessage,
58
- fullScreen = false,
59
- avatar,
60
- botTypingPlaceholder,
61
- enableLoadConfigFromService = false,
62
- maintainConnectionWhenClosed = false,
63
- asyncInitializers = {},
64
- loadingComponent,
65
- onReset,
66
- onClose,
67
- onTemplateBtnClick,
68
- onErrorClick,
69
- errorMessageRenderer,
70
- className,
71
- style,
72
- defaultLinkTarget,
73
- } = props;
74
-
75
- return (
76
- <AsgardAppInitializationContextProvider
77
- enabled={enableLoadConfigFromService}
78
- config={config}
79
- asyncInitializers={asyncInitializers}
80
- loadingComponent={loadingComponent}
81
- >
82
- <AsgardThemeContextProvider theme={theme}>
83
- <AsgardServiceContextProvider
84
- parentRef={ref}
85
- avatar={avatar}
86
- config={config}
87
- customChannelId={customChannelId}
88
- initMessages={initMessages}
89
- onSseMessage={onSseMessage}
90
- botTypingPlaceholder={botTypingPlaceholder}
91
- >
92
- <ChatbotContainer
93
- fullScreen={fullScreen}
94
- className={className}
95
- style={style}
96
- >
97
- <ChatbotHeader
98
- title={title}
99
- onReset={onReset}
100
- onClose={onClose}
101
- customActions={customActions}
102
- maintainConnectionWhenClosed={maintainConnectionWhenClosed}
103
- />
104
- <AsgardTemplateContextProvider
105
- onErrorClick={onErrorClick}
106
- errorMessageRenderer={errorMessageRenderer}
107
- onTemplateBtnClick={onTemplateBtnClick}
108
- defaultLinkTarget={defaultLinkTarget}
109
- >
110
- <ChatbotBody />
111
- </AsgardTemplateContextProvider>
112
- <ChatbotFooter />
113
- </ChatbotContainer>
114
- </AsgardServiceContextProvider>
115
- </AsgardThemeContextProvider>
116
- </AsgardAppInitializationContextProvider>
117
- );
118
- });
@@ -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
- });