diff --git a/packages/s2-core/__tests__/unit/common/icons/__snapshots__/gui-icon-spec.ts.snap b/packages/s2-core/__tests__/unit/common/icons/__snapshots__/gui-icon-spec.ts.snap
new file mode 100644
index 0000000000..4cb679838b
--- /dev/null
+++ b/packages/s2-core/__tests__/unit/common/icons/__snapshots__/gui-icon-spec.ts.snap
@@ -0,0 +1,428 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GuiIcon Tests should render correctly icon with
+ 1`] = `
+GuiIcon {
+ "_events": Object {},
+ "attrs": Object {
+ "matrix": null,
+ "opacity": 1,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ "iconImageShape": ImageShape1 {
+ "_events": Object {},
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "fillOpacity": 1,
+ "height": 20,
+ "lineAppendWidth": 0,
+ "lineWidth": 1,
+ "matrix": null,
+ "name": "test",
+ "opacity": 1,
+ "strokeOpacity": 1,
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "capture": true,
+ "visible": true,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ },
+ "isOnlineLink": [Function],
+}
+`;
+
+exports[`GuiIcon Tests should render correctly icon with SortUp 1`] = `
+GuiIcon {
+ "_events": Object {},
+ "attrs": Object {
+ "matrix": null,
+ "opacity": 1,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ "iconImageShape": ImageShape1 {
+ "_events": Object {},
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "fillOpacity": 1,
+ "height": 20,
+ "lineAppendWidth": 0,
+ "lineWidth": 1,
+ "matrix": null,
+ "name": "test",
+ "opacity": 1,
+ "strokeOpacity": 1,
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "capture": true,
+ "visible": true,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ },
+ "isOnlineLink": [Function],
+}
+`;
+
+exports[`GuiIcon Tests should render correctly icon with https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*2VvTSZmI4vYAAAAAAAAAAAAADmJ7AQ/original 1`] = `
+GuiIcon {
+ "_events": Object {},
+ "attrs": Object {
+ "matrix": null,
+ "opacity": 1,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ "iconImageShape": ImageShape1 {
+ "_events": Object {},
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "fillOpacity": 1,
+ "height": 20,
+ "lineAppendWidth": 0,
+ "lineWidth": 1,
+ "matrix": null,
+ "name": "test",
+ "opacity": 1,
+ "strokeOpacity": 1,
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "capture": true,
+ "visible": true,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ },
+ "isOnlineLink": [Function],
+}
+`;
+
+exports[`GuiIcon Tests should render correctly icon with https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*5nsESLuvc_EAAAAAAAAAAAAADmJ7AQ/fmt.webp 1`] = `
+GuiIcon {
+ "_events": Object {},
+ "attrs": Object {
+ "matrix": null,
+ "opacity": 1,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ "iconImageShape": ImageShape1 {
+ "_events": Object {},
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "fillOpacity": 1,
+ "height": 20,
+ "lineAppendWidth": 0,
+ "lineWidth": 1,
+ "matrix": null,
+ "name": "test",
+ "opacity": 1,
+ "strokeOpacity": 1,
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "capture": true,
+ "visible": true,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ },
+ "isOnlineLink": [Function],
+}
+`;
+
+exports[`GuiIcon Tests should render correctly icon with https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*P-jqT4U7YrcAAAAAAAAAAAAADmJ7AQ/original.jpg 1`] = `
+GuiIcon {
+ "_events": Object {},
+ "attrs": Object {
+ "matrix": null,
+ "opacity": 1,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ "iconImageShape": ImageShape1 {
+ "_events": Object {},
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "fillOpacity": 1,
+ "height": 20,
+ "lineAppendWidth": 0,
+ "lineWidth": 1,
+ "matrix": null,
+ "name": "test",
+ "opacity": 1,
+ "strokeOpacity": 1,
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "capture": true,
+ "visible": true,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ },
+ "isOnlineLink": [Function],
+}
+`;
+
+exports[`GuiIcon Tests should render correctly icon with https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*f6e6S4OUSdMAAAAAAAAAAAAADmJ7AQ/original.gif 1`] = `
+GuiIcon {
+ "_events": Object {},
+ "attrs": Object {
+ "matrix": null,
+ "opacity": 1,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ "iconImageShape": ImageShape1 {
+ "_events": Object {},
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "fillOpacity": 1,
+ "height": 20,
+ "lineAppendWidth": 0,
+ "lineWidth": 1,
+ "matrix": null,
+ "name": "test",
+ "opacity": 1,
+ "strokeOpacity": 1,
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "cfg": Object {
+ "animable": true,
+ "animating": false,
+ "attrs": Object {
+ "animable": true,
+ "animating": false,
+ "capture": true,
+ "children": Array [],
+ "height": 20,
+ "name": "test",
+ "type": "__GUI_ICON__",
+ "visible": true,
+ "width": 20,
+ "x": 0,
+ "y": 0,
+ "zIndex": 0,
+ },
+ "capture": true,
+ "visible": true,
+ "zIndex": 0,
+ },
+ "destroyed": false,
+ },
+ "isOnlineLink": [Function],
+}
+`;
diff --git a/packages/s2-core/__tests__/unit/common/icons/gui-icon-spec.ts b/packages/s2-core/__tests__/unit/common/icons/gui-icon-spec.ts
new file mode 100644
index 0000000000..00ca41dd5a
--- /dev/null
+++ b/packages/s2-core/__tests__/unit/common/icons/gui-icon-spec.ts
@@ -0,0 +1,85 @@
+import { Group, Shape } from '@antv/g-canvas';
+import { registerIcon } from '../../../../src/common/icons';
+import { sleep } from '../../../util/helpers';
+import { GuiIcon } from '@/common/icons/gui-icon';
+import { ArrowDown } from '@/common/icons/svg/svgs';
+
+describe('GuiIcon Tests', () => {
+ test('should get gui icon static type', () => {
+ expect(GuiIcon.type).toEqual('__GUI_ICON__');
+ });
+
+ test.each([
+ // 内置
+ 'SortUp',
+ // base64/本地文件
+ ArrowDown,
+ // 在线链接 (无后缀)
+ 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*2VvTSZmI4vYAAAAAAAAAAAAADmJ7AQ/original',
+ // 在线链接 (静态)
+ 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*P-jqT4U7YrcAAAAAAAAAAAAADmJ7AQ/original.jpg',
+ // 在线链接 (动态)
+ 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*f6e6S4OUSdMAAAAAAAAAAAAADmJ7AQ/original.gif',
+ // 在线链接 (webp)
+ 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*5nsESLuvc_EAAAAAAAAAAAAADmJ7AQ/fmt.webp',
+ ])('should render correctly icon with %s', (src) => {
+ const errSpy = jest
+ .spyOn(console, 'error')
+ .mockImplementationOnce(() => {});
+
+ registerIcon('test', src);
+
+ const icon = new GuiIcon({
+ name: 'test',
+ x: 0,
+ y: 0,
+ width: 20,
+ height: 20,
+ });
+
+ expect(icon.get('name')).toEqual('test');
+ expect(icon.iconImageShape).toBeInstanceOf(Shape.Image);
+ expect(icon).toBeInstanceOf(Group);
+ expect(icon).toMatchSnapshot();
+ expect(errSpy).not.toHaveBeenCalled();
+ });
+
+ test('should not render icon with invalid online url', async () => {
+ registerIcon('test', 'https://www.test.svg');
+
+ const errSpy = jest
+ .spyOn(console, 'error')
+ .mockImplementationOnce(() => {});
+
+ const icon = new GuiIcon({
+ name: 'test',
+ x: 0,
+ y: 0,
+ width: 20,
+ height: 20,
+ });
+
+ await sleep(300);
+
+ expect(errSpy).toHaveBeenCalled();
+ });
+
+ test('should get is online link result', () => {
+ const icon = new GuiIcon({
+ name: 'test',
+ x: 0,
+ y: 0,
+ width: 20,
+ height: 20,
+ });
+
+ expect(icon.isOnlineLink('https://www.test.png')).toBeTruthy();
+ expect(icon.isOnlineLink('https://www.test/test')).toBeTruthy();
+ expect(icon.isOnlineLink('http://www.test.png')).toBeTruthy();
+ expect(icon.isOnlineLink('//www.test.png')).toBeTruthy();
+ expect(icon.isOnlineLink('https//www.test.png')).toBeFalsy();
+ expect(icon.isOnlineLink('https//www.test.png')).toBeFalsy();
+ expect(icon.isOnlineLink('://www.test.png')).toBeFalsy();
+ expect(icon.isOnlineLink('')).toBeFalsy();
+ });
+});
diff --git a/packages/s2-core/src/common/icons/gui-icon.ts b/packages/s2-core/src/common/icons/gui-icon.ts
index 016af2c663..d8babe059a 100644
--- a/packages/s2-core/src/common/icons/gui-icon.ts
+++ b/packages/s2-core/src/common/icons/gui-icon.ts
@@ -7,6 +7,8 @@ import { getIcon } from './factory';
const STYLE_PLACEHOLDER = '