diff --git a/packages/dashboard/e2e/tests/widgets/text.spec.ts b/packages/dashboard/e2e/tests/widgets/text.spec.ts
index 1e936ffd3..0b03d193b 100644
--- a/packages/dashboard/e2e/tests/widgets/text.spec.ts
+++ b/packages/dashboard/e2e/tests/widgets/text.spec.ts
@@ -280,5 +280,37 @@ test.describe('Test Text Widget', () => {
await page.getByText(TEXT_WIDGET_CONTENT).click();
await page.waitForURL(`**${url}`);
});
+
+ test('when link is enabled, it should sanitize href and not link if dangerous', async ({
+ dashboardWithTextWidget,
+ configPanel,
+ page,
+ }) => {
+ const widget = dashboardWithTextWidget.gridArea.locator(
+ '[data-gesture=widget]'
+ );
+
+ const url = "javascript://%0aalert('1.com')";
+ await configPanel.collapsedButton.click();
+ await widget.dblclick();
+ await widget.getByRole('textbox').fill(TEXT_WIDGET_CONTENT);
+ await configPanel.container
+ .getByTestId('text-widget-link-header')
+ .click();
+ await configPanel.container
+ .getByText('Create link', { exact: true })
+ .click();
+ await configPanel.container
+ .getByLabel('text widget link input')
+ .fill(url);
+
+ // go to preview
+ await page.getByRole('button', { name: 'preview' }).click();
+
+ // clicking on the link
+ await page.getByText(TEXT_WIDGET_CONTENT).click();
+
+ expect(page.getByText(TEXT_WIDGET_CONTENT)).not.toHaveAttribute('href');
+ });
});
});
diff --git a/packages/dashboard/src/customization/widgets/text/__snapshots__/component.test.tsx.snap b/packages/dashboard/src/customization/widgets/text/__snapshots__/component.test.tsx.snap
deleted file mode 100644
index 2cb029223..000000000
--- a/packages/dashboard/src/customization/widgets/text/__snapshots__/component.test.tsx.snap
+++ /dev/null
@@ -1,81 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Text Widget should render {"readOnly":false,"isSelected":false,"isUrl":false} correctly 1`] = `
-
-
- {"readOnly":false,"isSelected":false,"id":"some-id","x":1,"y":2,"z":3,"height":100,"width":100,"type":"text-widget","properties":{"isUrl":false,"value":"abc"}}
-
-
-`;
-
-exports[`Text Widget should render {"readOnly":false,"isSelected":false,"isUrl":true} correctly 1`] = `
-
-
- {"isUrl":true,"readOnly":false,"isSelected":false,"id":"some-id","x":1,"y":2,"z":3,"height":100,"width":100,"type":"text-widget","properties":{"isUrl":true,"value":"abc"}}
-
-
-`;
-
-exports[`Text Widget should render {"readOnly":false,"isSelected":true,"isUrl":false} correctly 1`] = `
-
-
- {"readOnly":false,"isSelected":true,"id":"some-id","x":1,"y":2,"z":3,"height":100,"width":100,"type":"text-widget","properties":{"isUrl":false,"value":"abc"}}
-
-
-`;
-
-exports[`Text Widget should render {"readOnly":false,"isSelected":true,"isUrl":true} correctly 1`] = `
-
-
- {"isUrl":true,"readOnly":false,"isSelected":true,"id":"some-id","x":1,"y":2,"z":3,"height":100,"width":100,"type":"text-widget","properties":{"isUrl":true,"value":"abc"}}
-
-
-`;
-
-exports[`Text Widget should render {"readOnly":true,"isSelected":false,"isUrl":false} correctly 1`] = `
-
-
- {"id":"some-id","x":1,"y":2,"z":3,"height":100,"width":100,"type":"text-widget","properties":{"isUrl":false,"value":"abc"}}
-
-
-`;
-
-exports[`Text Widget should render {"readOnly":true,"isSelected":false,"isUrl":true} correctly 1`] = `
-
-
- {"id":"some-id","x":1,"y":2,"z":3,"height":100,"width":100,"type":"text-widget","properties":{"isUrl":true,"value":"abc"}}
-
-
-`;
-
-exports[`Text Widget should render {"readOnly":true,"isSelected":true,"isUrl":false} correctly 1`] = `
-
-
- {"id":"some-id","x":1,"y":2,"z":3,"height":100,"width":100,"type":"text-widget","properties":{"isUrl":false,"value":"abc"}}
-
-
-`;
-
-exports[`Text Widget should render {"readOnly":true,"isSelected":true,"isUrl":true} correctly 1`] = `
-
-
- {"id":"some-id","x":1,"y":2,"z":3,"height":100,"width":100,"type":"text-widget","properties":{"isUrl":true,"value":"abc"}}
-
-
-`;
diff --git a/packages/dashboard/src/customization/widgets/text/component.test.tsx b/packages/dashboard/src/customization/widgets/text/component.test.tsx
index 0cebb062f..5a979fe01 100644
--- a/packages/dashboard/src/customization/widgets/text/component.test.tsx
+++ b/packages/dashboard/src/customization/widgets/text/component.test.tsx
@@ -1,10 +1,8 @@
import React from 'react';
-
-import { render } from '@testing-library/react';
-import { useSelector, useDispatch } from 'react-redux';
+import { render, screen } from '@testing-library/react';
import TextWidgetComponent from './component';
-import { useIsSelected } from '~/customization/hooks/useIsSelected';
-import { onChangeDashboardGridEnabledAction } from '~/store/actions';
+import * as ReactRedux from 'react-redux';
+import type { TextProperties } from '../types';
jest.mock('~/store/actions', () => ({
...jest.requireActual('~/store/actions'),
@@ -13,88 +11,101 @@ jest.mock('~/store/actions', () => ({
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
- useSelector: jest.fn(),
- useDispatch: jest.fn(),
+ useDispatch: () => jest.fn(),
}));
jest.mock('~/customization/hooks/useIsSelected', () => ({
useIsSelected: jest.fn(),
}));
-jest.mock('./link', () => (props: unknown) => (
- {JSON.stringify(props)}
-));
-jest.mock('./styledText/textArea', () => (props: unknown) => (
- {JSON.stringify(props)}
-));
-jest.mock('./styledText/editableText', () => (props: unknown) => (
- {JSON.stringify(props)}
-));
-jest.mock('./styledText', () => (props: unknown) => (
- {JSON.stringify(props)}
-));
+function TextTextWidget(props: TextProperties) {
+ return (
+
+ );
+}
+
+function TestReadonlyTextWidget(props: TextProperties) {
+ jest.spyOn(ReactRedux, 'useSelector').mockReturnValueOnce(true);
+
+ return ;
+}
+
+function TestEditableTextWidget(props: TextProperties) {
+ jest.spyOn(ReactRedux, 'useSelector').mockReturnValueOnce(false);
+
+ return ;
+}
describe('Text Widget', () => {
- beforeEach(() => {
- jest.resetAllMocks();
+ it('should render readonly text', () => {
+ const text = 'text';
+ render();
+
+ const textWidget = screen.getByText(text);
+ expect(textWidget).toBeVisible();
+ expect(textWidget).toHaveAttribute('aria-readonly');
+ expect(screen.queryByRole('link', { name: text })).not.toBeInTheDocument();
});
- [
- { readOnly: true, isSelected: true, isUrl: true },
- { readOnly: true, isSelected: true, isUrl: false },
- { readOnly: true, isSelected: false, isUrl: false },
- { readOnly: false, isSelected: false, isUrl: false },
- { readOnly: false, isSelected: false, isUrl: true },
- { readOnly: false, isSelected: true, isUrl: true },
- { readOnly: false, isSelected: true, isUrl: false },
- { readOnly: true, isSelected: false, isUrl: true },
- ].forEach((configuration) => {
- it(`should render ${JSON.stringify(configuration)} correctly`, () => {
- const { isSelected, readOnly, isUrl } = configuration;
- (useIsSelected as jest.Mock).mockImplementation(() => isSelected);
- (useSelector as jest.Mock).mockImplementation(() => readOnly);
- (useDispatch as jest.Mock).mockReturnValue(jest.fn());
-
- const { container } = render(
-
- );
-
- expect(container).toMatchSnapshot();
- });
+ it('should render editable text', () => {
+ const text = 'text';
+ render();
+
+ const textWidget = screen.getByText(text);
+ expect(textWidget).toBeVisible();
+ expect(textWidget).not.toHaveAttribute('aria-readonly');
+ expect(screen.queryByRole('link', { name: text })).not.toBeInTheDocument();
});
- it('should exit edit mode when unmounted', () => {
- (useDispatch as jest.Mock).mockImplementation(() => jest.fn((cb) => cb())); // short curcuit dispatch
-
- (useIsSelected as jest.Mock).mockImplementation(() => false);
- (useSelector as jest.Mock).mockImplementation(() => false);
- (useDispatch as jest.Mock).mockReturnValue(jest.fn());
-
- const { unmount } = render(
-
- );
- unmount();
-
- expect(onChangeDashboardGridEnabledAction).toBeCalledWith({
- enabled: true,
- });
+ it('should render editable link', () => {
+ const text = 'text';
+ const href = 'https://test.com';
+ render();
+
+ const textWidget = screen.getByText(text);
+ expect(textWidget).toHaveTextContent(text);
+ expect(textWidget).not.toHaveAttribute('aria-readonly');
+ expect(screen.queryByRole('link', { name: text })).not.toBeInTheDocument();
+ });
+
+ it('should santize and then render unsafe link that injects javascript', () => {
+ const text = 'text';
+ const href = 'javascript://%0Aalert(1)';
+ render();
+
+ const textWidget = screen.getByText(text);
+ expect(textWidget).toBeVisible();
+ expect(textWidget).not.toHaveAttribute('href');
+ expect(screen.queryByRole('link', { name: text })).not.toBeInTheDocument();
+ });
+
+ it('should santize and then render unsafe link that injects javascript via hex format', () => {
+ const text = 'text';
+ const href = '\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x3aalert(1)';
+ render();
+
+ const textWidget = screen.getByText(text);
+ expect(textWidget).toBeVisible();
+ expect(textWidget).not.toHaveAttribute('href');
+ expect(screen.queryByRole('link', { name: text })).not.toBeInTheDocument();
+ });
+
+ it('should render a safe link', () => {
+ const text = 'text';
+ const href = 'https://test.com';
+ render();
+
+ const textWidget = screen.getByRole('link', { name: text });
+ expect(textWidget).toBeVisible();
+ expect(textWidget).toHaveAttribute('href', href);
});
});
diff --git a/packages/dashboard/src/customization/widgets/text/component.tsx b/packages/dashboard/src/customization/widgets/text/component.tsx
index 88ce0ccd2..f95dc3a6f 100644
--- a/packages/dashboard/src/customization/widgets/text/component.tsx
+++ b/packages/dashboard/src/customization/widgets/text/component.tsx
@@ -19,6 +19,7 @@ const TextWidgetComponent: React.FC = (widget) => {
const { isUrl, value } = widget.properties;
const dispatch = useDispatch();
+
const [isEditing, setIsEditing] = useState(false);
const handleSetEdit = useCallback(
@@ -50,7 +51,7 @@ const TextWidgetComponent: React.FC = (widget) => {
if (isUrl) {
return ;
} else {
- return ;
+ return ;
}
} else {
if (isUrl) {
diff --git a/packages/dashboard/src/customization/widgets/text/styledText/index.tsx b/packages/dashboard/src/customization/widgets/text/styledText/index.tsx
index 75e226d42..69885be8a 100644
--- a/packages/dashboard/src/customization/widgets/text/styledText/index.tsx
+++ b/packages/dashboard/src/customization/widgets/text/styledText/index.tsx
@@ -9,11 +9,13 @@ import { spaceScaledXs } from '@cloudscape-design/design-tokens';
type StyledTextProps = TextWidget & {
onPointerDown?: PointerEventHandler;
onPointerUp?: PointerEventHandler;
+ readonly?: boolean;
};
const StyledText: React.FC = ({
onPointerDown,
onPointerUp,
+ readonly,
...widget
}) => {
const { value } = widget.properties;
@@ -43,7 +45,12 @@ const StyledText: React.FC = ({
};
return (
-
+
{textContent}
);