2026-06-09 14:23:43 +08:00

837 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert, describe, expect, test } from 'vitest';
import type { DataSchema } from '@tmagic/schema';
import * as util from '../../src';
describe('asyncLoadJs', () => {
const url = 'https://m.www.tmagic.com/magic-ui/production/1/1625056093304/magic/magic-ui.umd.min.js';
// 在测试环境中,浏览器不会真的加载外部 script。
// 通过监听插入到 head 的 script 节点,手动派发 load 事件来驱动 Promise resolve。
const triggerScriptLoad = () => {
queueMicrotask(() => {
const scripts = document.getElementsByTagName('script');
Array.from(scripts).forEach((s) => {
if (typeof s.onload === 'function') {
(s.onload as any)(new Event('load'));
}
});
});
};
test('第一次加载asyncLoadJs带url与crossorigin参数', async () => {
const crossOrigin = 'anonymous';
const load = util.asyncLoadJs(url, crossOrigin);
triggerScriptLoad();
await load;
const script = document.getElementsByTagName('script')[0];
expect(script).not.toBeUndefined();
expect(script.type).toMatch('text/javascript');
expect(script.crossOrigin).toMatch(crossOrigin);
expect(script.src).toMatch(url);
});
test('第二次加载asyncLoadJs', async () => {
const load1 = util.asyncLoadJs(url, 'anonymous');
triggerScriptLoad();
await load1;
await util.asyncLoadJs(url, 'use-credentials');
const scriptList = document.getElementsByTagName('script');
expect(scriptList.length).toBe(1);
expect(scriptList[0].crossOrigin).toMatch('anonymous');
expect(scriptList[0].src).toMatch(url);
});
test('url无效, onerror 触发后 reject', async () => {
const promise = util.asyncLoadJs('error-url');
queueMicrotask(() => {
const scripts = document.getElementsByTagName('script');
Array.from(scripts).forEach((s) => {
if (s.src.includes('error-url') && typeof s.onerror === 'function') {
(s.onerror as any)(new Event('error'));
}
});
});
await expect(promise).rejects.toBeInstanceOf(Error);
});
});
describe('asyncLoadCss', () => {
const url = 'https://beta.m.www.tmagic.com/magic-act/css/BuyGift.75d837d2b3fd.css?max_age=864000';
const triggerLinkLoad = () => {
queueMicrotask(() => {
const links = document.getElementsByTagName('link');
Array.from(links).forEach((l) => {
if (typeof l.onload === 'function') {
(l.onload as any)(new Event('load'));
}
});
});
};
test('第一次加载asyncLoadCss', async () => {
const load = util.asyncLoadCss(url);
triggerLinkLoad();
await load;
const link = document.getElementsByTagName('link')[0];
expect(link).not.toBeUndefined();
expect(link.rel).toMatch('stylesheet');
expect(link.href).toMatch(url);
});
test('第二次加载asyncLoadCss命中缓存', async () => {
const load1 = util.asyncLoadCss(url);
triggerLinkLoad();
await load1;
await util.asyncLoadCss(url);
const linkList = document.getElementsByTagName('link');
expect(linkList.length).toBe(1);
expect(linkList[0].href).toMatch(url);
});
test('url无效, onerror 触发后 reject', async () => {
const promise = util.asyncLoadCss('error-css');
queueMicrotask(() => {
const links = document.getElementsByTagName('link');
Array.from(links).forEach((l) => {
if (l.href.includes('error-css') && typeof l.onerror === 'function') {
(l.onerror as any)(new Event('error'));
}
});
});
await expect(promise).rejects.toBeInstanceOf(Error);
});
});
describe('toLine', () => {
test('aBc', () => {
const value = util.toLine('aBc');
expect(value).toBe('a-bc');
});
test('aBC', () => {
const value = util.toLine('aBC');
expect(value).toBe('a-b-c');
});
test('ABC', () => {
const value = util.toLine('ABC');
expect(value).toBe('a-b-c');
});
});
describe('toHump', () => {
test('a-bc', () => {
const value = util.toHump('a-bc');
expect(value).toBe('aBc');
});
test('a-b-c', () => {
const value = util.toHump('a-b-c');
expect(value).toBe('aBC');
});
test('-b-c', () => {
const value = util.toHump('-b-c');
expect(value).toBe('BC');
});
});
describe('getNodePath', () => {
const root = [
{
id: 1,
type: 'container',
items: [
{
id: 11,
items: [
{
id: 111,
},
],
},
],
},
{
id: 2,
type: 'container',
items: [
{
id: 22,
items: [
{
id: 222,
},
],
},
],
},
];
test('基础', () => {
const path = util.getNodePath(111, root);
const path2 = util.getNodePath(22, root);
expect(path).toHaveLength(3);
expect(path2).toHaveLength(2);
});
test('error', () => {
const path = util.getNodePath(111, 123 as any);
const path2 = util.getNodePath(33, root);
expect(path).toHaveLength(0);
expect(path2).toHaveLength(0);
});
test('skip 跳过指定子树后查找不到其内部节点', () => {
// 跳过 id 为 2 的容器其子树22 / 222不再被遍历
const skipped = util.getNodePath(222, root, root[1]);
expect(skipped).toHaveLength(0);
// 其它子树不受影响
const path = util.getNodePath(111, root, root[1]);
expect(path).toHaveLength(3);
});
test('skip 不影响对被跳过节点自身的匹配', () => {
// skip 仅阻止递归进入其 items节点自身仍可被匹配到
const path = util.getNodePath(2, root, root[1]);
expect(path).toHaveLength(1);
expect(path[0].id).toBe(2);
});
});
describe('filterXSS', () => {
test('<>', () => {
const value = util.filterXSS('<div></div>');
expect(value).toBe('&lt;div&gt;&lt;/div&gt;');
});
test('\'"', () => {
const value = util.filterXSS('\'div\'"span"');
expect(value).toBe('&apos;div&apos;&quot;span&quot;');
});
});
describe('getUrlParam', () => {
test('正常', () => {
const url = 'http://www.tmagic.com?a=b';
const value = util.getUrlParam('a', url);
expect(value).toBe('b');
});
test('null', () => {
const url = 'http://www.tmagic.com';
const value = util.getUrlParam('a', url);
expect(value).toBe('');
});
test('emprty', () => {
const url = 'http://www.tmagic.com?a=';
const value = util.getUrlParam('a', url);
expect(value).toBe('');
});
});
describe('setUrlParam', () => {
test('正常', () => {
expect(util.setUrlParam('a', '1', 'https://www.tmagic.com')).toBe('https://www.tmagic.com?a=1');
expect(util.setUrlParam('a', '1', 'https://www.tmagic.com?c&d')).toBe('https://www.tmagic.com?c&d&a=1');
expect(util.setUrlParam('a', '1', 'https://www.tmagic.com?b=1')).toBe('https://www.tmagic.com?b=1&a=1');
});
});
describe('getSearchObj', () => {
test('正常', () => {
expect(util.getSearchObj('a=1&b=2')).toEqual({ a: '1', b: '2' });
});
});
describe('delQueStr', () => {
test('正常', () => {
expect(util.delQueStr('https://www.tmagic.com?a=1', 'a')).toBe('https://www.tmagic.com');
expect(util.delQueStr('https://www.tmagic.com?a=1&b=2', ['a', 'b'])).toBe('https://www.tmagic.com');
expect(util.delQueStr('https://www.tmagic.com?a=1&b=2', ['a'])).toBe('https://www.tmagic.com?b=2');
});
});
describe('isPop', () => {
// type 为 pop 结尾 isPop 才为 true
test('true', () => {
expect(
util.isPop({
type: 'pop',
id: 1,
}),
).toBeTruthy();
});
test('endswidth true', () => {
expect(
util.isPop({
type: 'xxxpop',
id: 1,
}),
).toBeTruthy();
});
test('false', () => {
expect(
util.isPop({
type: 'pop1',
id: 1,
}),
).toBeFalsy();
});
});
describe('isPage', () => {
test('true', () => {
expect(
util.isPage({
type: 'page',
id: 1,
}),
).toBeTruthy();
});
test('false', () => {
expect(
util.isPage({
type: 'pop1',
id: 1,
}),
).toBeFalsy();
});
});
describe('getHost', () => {
test('正常', () => {
const host = util.getHost('https://www.tmagic.com/index.html');
expect(host).toBe('www.tmagic.com');
});
});
describe('isSameDomain', () => {
test('正常', () => {
const flag = util.isSameDomain('https://www.tmagic.com/index.html', 'www.tmagic.com');
expect(flag).toBeTruthy();
});
test('不正常', () => {
const flag = util.isSameDomain('https://www.tmagic.com/index.html', 'test.www.tmagic.com');
expect(flag).toBeFalsy();
});
test('不是http', () => {
const flag = util.isSameDomain('ftp://www.tmagic.com/index.html', 'test.www.tmagic.com');
expect(flag).toBeTruthy();
});
});
describe('guid', () => {
test('获取id', () => {
const id = util.guid();
const id1 = util.guid();
expect(typeof id).toBe('string');
expect(id === id1).toBeFalsy();
});
});
describe('getValueByKeyPath', () => {
test('key', () => {
const value = util.getValueByKeyPath('a', {
a: 1,
});
expect(value).toBe(1);
});
test('keys', () => {
const value = util.getValueByKeyPath('a.b', {
a: {
b: 1,
},
});
expect(value).toBe(1);
});
test('array', () => {
const value = util.getValueByKeyPath('a.0.b', {
a: [
{
b: 1,
},
],
});
expect(value).toBe(1);
const value1 = util.getValueByKeyPath('a[0].b', {
a: [
{
b: 1,
},
],
});
expect(value1).toBe(1);
});
test('error', () => {
assert.throws(() => {
util.getValueByKeyPath('a.b.c.d', {
a: {},
});
});
assert.throws(() => {
util.getValueByKeyPath('a.b.c', {
a: {},
});
});
assert.doesNotThrow(() => {
util.getValueByKeyPath('a', {
a: {},
});
});
});
});
describe('getNodes', () => {
test('获取id', () => {
const root = [
{
id: 1,
type: 'container',
items: [
{
id: 11,
items: [
{
id: 111,
},
],
},
],
},
{
id: 2,
type: 'container',
items: [
{
id: 22,
items: [
{
id: 222,
},
],
},
],
},
{
id: 3,
type: 'container',
items: {
id: 33,
items: [
{
id: 333,
},
],
},
},
];
const nodes = util.getNodes([22, 111, 2], root);
expect(nodes.length).toBe(3);
});
});
describe('getDepKeys', () => {
test('get keys', () => {
const keys = util.getDepKeys(
{
ds_bebcb2d5: {
61705611: {
name: '文本',
keys: ['text'],
},
},
},
61705611,
);
expect(keys).toEqual(['text']);
});
});
describe('getDepNodeIds', () => {
test('get node ids', () => {
const ids = util.getDepNodeIds({
ds_bebcb2d5: {
61705611: {
name: '文本',
keys: ['text'],
},
},
});
expect(ids).toEqual(['61705611']);
});
});
describe('replaceChildNode', () => {
test('replace', () => {
const root = [
{
id: 1,
text: '',
type: 'container',
items: [
{
id: 11,
text: '',
items: [
{
id: 111,
text: '',
},
],
},
],
},
{
id: 2,
type: 'container',
text: '',
items: [
{
id: 22,
text: '',
items: [
{
id: 222,
text: '',
},
],
},
],
},
];
expect(root[1].items[0].items[0].text).toBe('');
util.replaceChildNode(
{
id: 222,
text: '文本',
},
root,
);
expect(root[1].items[0].items[0].text).toBe('文本');
});
test('replace with parent', () => {
const root = [
{
id: 1,
text: '',
type: 'container',
items: [
{
id: 11,
text: '',
items: [
{
id: 111,
text: '',
},
],
},
],
},
{
id: 2,
type: 'container',
text: '',
items: [
{
id: 22,
text: '',
items: [
{
id: 222,
text: '',
},
],
},
],
},
];
expect(root[1].items[0].items[0].text).toBe('');
util.replaceChildNode(
{
id: 222,
text: '文本',
},
root,
22,
);
expect(root[1].items[0].items[0].text).toBe('文本');
});
});
describe('compiledNode', () => {
test('compiled', () => {
const node = util.compiledNode(
(_str: string) => '123',
{
id: 61705611,
type: 'text',
text: {
value: '456',
},
},
{
ds_bebcb2d5: {
61705611: {
name: '文本',
keys: ['text.value'],
},
},
},
);
expect(node.text.value).toBe('123');
});
test('compile with source id', () => {
const node = util.compiledNode(
(_str: string) => '123',
{
id: 61705611,
type: 'text',
text: '456',
},
{
ds_bebcb2d5: {
61705611: {
name: '文本',
keys: ['text'],
},
},
},
'ds_bebcb2d5',
);
expect(node.text).toBe('123');
});
test('compile error', () => {
const node = util.compiledNode(
(_str: string) => {
throw new Error('error');
},
{
id: 61705611,
type: 'text',
text: '456',
},
{
ds_bebcb2d5: {
61705611: {
name: '文本',
keys: ['text'],
},
},
},
);
expect(node.text).toBe('');
});
test('compile 缓存命中时直接复用 cache value', () => {
const node: any = {
id: 61705611,
type: 'text',
[`${util.DSL_NODE_KEY_COPY_PREFIX}text`]: 'cached-template',
text: 'old-value',
};
const result = util.compiledNode((str: string) => `compiled-${str}`, node, {
ds_bebcb2d5: {
61705611: {
name: '文本',
keys: ['text'],
},
},
});
expect(result.text).toBe('compiled-cached-template');
});
});
describe('getDefaultValueFromFields', () => {
test('最简单', () => {
const fields = [
{
name: 'name',
},
];
const data = util.getDefaultValueFromFields(fields);
expect(data).toHaveProperty('name');
});
test('默认值为string', () => {
const fields = [
{
name: 'name',
defaultValue: 'name',
},
];
const data = util.getDefaultValueFromFields(fields);
expect(data.name).toBe('name');
});
test('type 为 object', () => {
const fields: DataSchema[] = [
{
type: 'object',
name: 'name',
},
];
const data = util.getDefaultValueFromFields(fields);
expect(data.name).toEqual({});
});
test('type 为 array', () => {
const fields: DataSchema[] = [
{
type: 'array',
name: 'name',
},
];
const data = util.getDefaultValueFromFields(fields);
expect(data.name).toEqual([]);
});
test('type 为 null', () => {
const fields: DataSchema[] = [
{
type: 'null',
name: 'name',
},
];
const data = util.getDefaultValueFromFields(fields);
expect(data.name).toBeNull();
});
test('object 嵌套', () => {
const fields: DataSchema[] = [
{
type: 'object',
name: 'name',
fields: [
{
name: 'key',
defaultValue: 'key',
},
],
},
];
const data = util.getDefaultValueFromFields(fields);
expect(data.name.key).toBe('key');
});
});
describe('compiledCond', () => {
test('is', () => {
expect(util.compiledCond('is', undefined, 1)).toBeFalsy();
expect(util.compiledCond('is', 1, 1)).toBeTruthy();
expect(util.compiledCond('is', '1', 1)).toBeFalsy();
expect(util.compiledCond('is', NaN, 1)).toBeFalsy();
expect(util.compiledCond('is', NaN, undefined)).toBeFalsy();
});
});
describe('getKeysArray', () => {
// 测试普通数组索引转换
test('应该将array[0]转换为["array", "0"]', () => {
const input = 'array[0]';
const expected = ['array', '0'];
const result = util.getKeysArray(input);
expect(result).toEqual(expected);
});
// 测试多层数组索引转换
test('应该将array[0][1]转换为["array", "0", "1"]', () => {
const input = 'array[0][1]';
const expected = ['array', '0', '1'];
const result = util.getKeysArray(input);
expect(result).toEqual(expected);
});
// 测试数字输入
test('应该将数字123转换为["123"]', () => {
const input = 123;
const expected = ['123'];
const result = util.getKeysArray(input);
expect(result).toEqual(expected);
});
// 测试不含数组索引的字符串
test('应该将普通字符串按点号分割', () => {
const input = 'a.b.c';
const expected = ['a', 'b', 'c'];
const result = util.getKeysArray(input);
expect(result).toEqual(expected);
});
// 测试空字符串
test('应该正确处理空字符串', () => {
const input = '';
const expected = [''];
const result = util.getKeysArray(input);
expect(result).toEqual(expected);
});
// 测试混合格式
test('应该正确处理混合格式的字符串', () => {
const input = 'obj.array[0].prop[1]';
const expected = ['obj', 'array', '0', 'prop', '1'];
const result = util.getKeysArray(input);
expect(result).toEqual(expected);
});
// 测试特殊字符情况
test('应该正确处理包含特殊字符的字符串', () => {
const input = 'obj.array[0].prop-with-dash[1]';
const expected = ['obj', 'array', '0', 'prop-with-dash', '1'];
const result = util.getKeysArray(input);
expect(result).toEqual(expected);
});
// 测试只有数组索引的情况
test('应该正确处理只有数组索引的情况', () => {
const input = '[0][1]';
const expected = ['', '0', '1'];
const result = util.getKeysArray(input);
expect(result).toEqual(expected);
});
});