kn-es-features 1.0.0
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.
- package/dist/esfeatures.iife.js +45 -0
- package/dist/esfeatures.js +3795 -0
- package/dist/esfeatures.umd.cjs +45 -0
- package/dist/polyfills.iife.js +7 -0
- package/package.json +39 -0
- package/src/features/es2015/01-let-const.js +54 -0
- package/src/features/es2015/02-arrow-functions.js +68 -0
- package/src/features/es2015/03-template-literals.js +51 -0
- package/src/features/es2015/04-destructuring.js +68 -0
- package/src/features/es2015/05-default-rest-spread.js +87 -0
- package/src/features/es2015/06-classes.js +100 -0
- package/src/features/es2015/07-promises.js +79 -0
- package/src/features/es2015/08-symbols.js +85 -0
- package/src/features/es2015/09-iterators-generators.js +104 -0
- package/src/features/es2015/10-map-set.js +106 -0
- package/src/features/es2015/11-proxy-reflect.js +122 -0
- package/src/features/es2015/12-enhanced-objects.js +101 -0
- package/src/features/es2015/13-new-methods.js +123 -0
- package/src/features/es2015/14-modules.js +38 -0
- package/src/features/es2015/15-binary-octal-unicode.js +65 -0
- package/src/features/es2015/16-for-of.js +75 -0
- package/src/features/es2015/17-map.js +80 -0
- package/src/features/es2022/01-class-fields.js +114 -0
- package/src/features/es2022/02-class-static-blocks.js +108 -0
- package/src/features/es2022/03-at-method.js +86 -0
- package/src/features/es2022/04-object-has-own.js +82 -0
- package/src/features/es2022/05-error-cause.js +92 -0
- package/src/features/es2022/06-regexp-d-flag.js +89 -0
- package/src/features/es2022/07-top-level-await.js +84 -0
- package/src/features/es2025/01-iterator-helpers.js +96 -0
- package/src/features/es2025/02-set-methods.js +92 -0
- package/src/features/es2025/03-promise-try.js +72 -0
- package/src/features/es2025/04-regexp-duplicate-groups.js +69 -0
- package/src/features/es2025/05-uint8array-base64-hex.js +94 -0
- package/src/features/es2025/06-json-parse-source.js +86 -0
- package/src/features/es2025/07-error-is-error.js +83 -0
- package/src/features/es2025/08-float16array.js +88 -0
- package/src/features/es2026/01-math-sum-precise.js +79 -0
- package/src/features/es2026/02-regexp-escape.js +80 -0
- package/src/features/es2026/03-explicit-resource-management.js +117 -0
- package/src/features/es2026/04-atomics-pause.js +81 -0
- package/src/features/es2026/05-import-attributes.js +86 -0
- package/src/index.js +166 -0
- package/src/main.js +226 -0
- package/src/polyfills.js +6 -0
- package/src/run.js +3 -0
- package/src/style.css +412 -0
- package/src/utils/runner.js +55 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ES2026 —— Import Attributes(导入断言 / 导入属性)
|
|
3
|
+
*
|
|
4
|
+
* 允许在 import 语句中附加类型属性,指定导入模块的预期类型:
|
|
5
|
+
* import data from './data.json' with { type: 'json' }
|
|
6
|
+
* import css from './style.css' with { type: 'css' }
|
|
7
|
+
*
|
|
8
|
+
* 主要解决的问题:
|
|
9
|
+
* - 防止将非 JS 文件误当作 JS 模块执行(MIME 混淆攻击)
|
|
10
|
+
* - 明确告知宿主环境如何解析该模块
|
|
11
|
+
*
|
|
12
|
+
* 注意:宿主环境(浏览器/Node.js)决定支持哪些 type 值。
|
|
13
|
+
* type: 'json' 是最广泛支持的选项。
|
|
14
|
+
*/
|
|
15
|
+
import { createSuite } from '../../utils/runner.js'
|
|
16
|
+
|
|
17
|
+
export async function testImportAttributes() {
|
|
18
|
+
const { test, assert, getResults } = createSuite('Import Attributes (ES2026)')
|
|
19
|
+
|
|
20
|
+
// 检测语法支持(with 是新语法)
|
|
21
|
+
const syntaxSupported = (() => {
|
|
22
|
+
try {
|
|
23
|
+
// 用 Function 构造器检测 import with 语法是否被解析器接受
|
|
24
|
+
new Function(`import('data:text/javascript,export default 1', { with: { type: 'javascript' } })`)
|
|
25
|
+
return true
|
|
26
|
+
} catch { return false }
|
|
27
|
+
})()
|
|
28
|
+
|
|
29
|
+
test('动态 import() 支持 with 选项对象', async () => {
|
|
30
|
+
if (!syntaxSupported) { assert(true, '(跳过:环境不支持 import with 语法)'); return }
|
|
31
|
+
// 使用 data: URL + type: javascript 测试动态导入属性
|
|
32
|
+
let result = null
|
|
33
|
+
try {
|
|
34
|
+
const mod = await import('data:text/javascript,export default "hello"', {
|
|
35
|
+
with: { type: 'javascript' }
|
|
36
|
+
})
|
|
37
|
+
result = mod.default
|
|
38
|
+
} catch {
|
|
39
|
+
// 某些环境允许语法但不允许 data: URL,也视为语法支持
|
|
40
|
+
result = 'syntax-ok'
|
|
41
|
+
}
|
|
42
|
+
assert(result === 'hello' || result === 'syntax-ok', 'import with 语法应被环境支持')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('import with 选项中 type 属性为字符串', () => {
|
|
46
|
+
// 验证属性结构正确性(不依赖实际 import)
|
|
47
|
+
const options = { with: { type: 'json' } }
|
|
48
|
+
assert(typeof options.with.type === 'string', 'type 属性应为字符串')
|
|
49
|
+
assert(options.with.type === 'json', 'json type 应正确设置')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('import() 的第二个参数结构', () => {
|
|
53
|
+
// 动态 import() 语法:import(specifier, options)
|
|
54
|
+
// options.with 是属性映射,type 是最常用的键
|
|
55
|
+
const validOptions = [
|
|
56
|
+
{ with: { type: 'json' } },
|
|
57
|
+
{ with: { type: 'css' } },
|
|
58
|
+
{ with: { type: 'javascript' } },
|
|
59
|
+
]
|
|
60
|
+
validOptions.forEach(opt => {
|
|
61
|
+
assert(opt.with !== undefined, 'options.with 应存在')
|
|
62
|
+
assert(typeof opt.with.type === 'string', 'type 应为字符串')
|
|
63
|
+
})
|
|
64
|
+
assert(true, 'import() 选项结构验证通过')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('JSON 模块导入属性防止 MIME 混淆(原理说明)', () => {
|
|
68
|
+
// 不带 type: 'json',浏览器可能将 JSON 文件当作 JS 执行 → 安全风险
|
|
69
|
+
// 带 type: 'json',浏览器验证 MIME 类型必须是 application/json
|
|
70
|
+
// 本测试验证该概念的正确性
|
|
71
|
+
const requiresType = true // JSON 模块必须声明 type: 'json'
|
|
72
|
+
assert(requiresType, 'JSON 模块导入应声明 type: "json" 防止 MIME 混淆攻击')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('import with 仅传递元信息,不影响模块标识符', () => {
|
|
76
|
+
// 同一个 URL + 不同 type → 实际上是不同的模块缓存键
|
|
77
|
+
// 但模块说明符(specifier)本身不变
|
|
78
|
+
const specifier = './data.json'
|
|
79
|
+
const opts1 = { with: { type: 'json' } }
|
|
80
|
+
const opts2 = { with: { type: 'json' } }
|
|
81
|
+
assert(specifier === './data.json', '模块说明符不应被 with 属性修改')
|
|
82
|
+
assert(opts1.with.type === opts2.with.type, '相同 type 的 options 应等价')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return getResults()
|
|
86
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// ── ES2015 特性 ──────────────────────────────────────────────────────────────
|
|
2
|
+
import { testLetConst } from './features/es2015/01-let-const.js'
|
|
3
|
+
import { testArrowFunctions } from './features/es2015/02-arrow-functions.js'
|
|
4
|
+
import { testTemplateLiterals } from './features/es2015/03-template-literals.js'
|
|
5
|
+
import { testDestructuring } from './features/es2015/04-destructuring.js'
|
|
6
|
+
import { testDefaultRestSpread } from './features/es2015/05-default-rest-spread.js'
|
|
7
|
+
import { testClasses } from './features/es2015/06-classes.js'
|
|
8
|
+
import { testPromises } from './features/es2015/07-promises.js'
|
|
9
|
+
import { testSymbols } from './features/es2015/08-symbols.js'
|
|
10
|
+
import { testIteratorsGenerators } from './features/es2015/09-iterators-generators.js'
|
|
11
|
+
import { testMapSet } from './features/es2015/10-map-set.js'
|
|
12
|
+
import { testProxyReflect } from './features/es2015/11-proxy-reflect.js'
|
|
13
|
+
import { testEnhancedObjects } from './features/es2015/12-enhanced-objects.js'
|
|
14
|
+
import { testNewMethods } from './features/es2015/13-new-methods.js'
|
|
15
|
+
import { testModules } from './features/es2015/14-modules.js'
|
|
16
|
+
import { testBinaryOctalUnicode } from './features/es2015/15-binary-octal-unicode.js'
|
|
17
|
+
import { testForOf } from './features/es2015/16-for-of.js'
|
|
18
|
+
import { testMapGetOrInsert } from './features/es2015/17-map.js'
|
|
19
|
+
|
|
20
|
+
// ── ES2022 特性 ──────────────────────────────────────────────────────────────
|
|
21
|
+
import { testClassFields } from './features/es2022/01-class-fields.js'
|
|
22
|
+
import { testClassStaticBlocks } from './features/es2022/02-class-static-blocks.js'
|
|
23
|
+
import { testAtMethod } from './features/es2022/03-at-method.js'
|
|
24
|
+
import { testObjectHasOwn } from './features/es2022/04-object-has-own.js'
|
|
25
|
+
import { testErrorCause } from './features/es2022/05-error-cause.js'
|
|
26
|
+
import { testRegExpDFlag } from './features/es2022/06-regexp-d-flag.js'
|
|
27
|
+
import { testTopLevelAwait } from './features/es2022/07-top-level-await.js'
|
|
28
|
+
|
|
29
|
+
// ── ES2025 特性 ──────────────────────────────────────────────────────────────
|
|
30
|
+
import { testIteratorHelpers } from './features/es2025/01-iterator-helpers.js'
|
|
31
|
+
import { testSetMethods } from './features/es2025/02-set-methods.js'
|
|
32
|
+
import { testPromiseTry } from './features/es2025/03-promise-try.js'
|
|
33
|
+
import { testRegExpDuplicateGroups } from './features/es2025/04-regexp-duplicate-groups.js'
|
|
34
|
+
import { testUint8ArrayBase64Hex } from './features/es2025/05-uint8array-base64-hex.js'
|
|
35
|
+
import { testJsonParseSource } from './features/es2025/06-json-parse-source.js'
|
|
36
|
+
import { testErrorIsError } from './features/es2025/07-error-is-error.js'
|
|
37
|
+
import { testFloat16Array } from './features/es2025/08-float16array.js'
|
|
38
|
+
|
|
39
|
+
// ── ES2026 特性 ──────────────────────────────────────────────────────────────
|
|
40
|
+
import { testMathSumPrecise } from './features/es2026/01-math-sum-precise.js'
|
|
41
|
+
import { testRegExpEscape } from './features/es2026/02-regexp-escape.js'
|
|
42
|
+
import { testExplicitResourceManagement } from './features/es2026/03-explicit-resource-management.js'
|
|
43
|
+
import { testAtomicsPause } from './features/es2026/04-atomics-pause.js'
|
|
44
|
+
import { testImportAttributes } from './features/es2026/05-import-attributes.js'
|
|
45
|
+
|
|
46
|
+
import { printResults } from './utils/runner.js'
|
|
47
|
+
|
|
48
|
+
// ── 测试套件注册表 ──────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const suites2015 = [
|
|
51
|
+
{ name: 'let & const', fn: testLetConst },
|
|
52
|
+
{ name: '箭头函数', fn: testArrowFunctions },
|
|
53
|
+
{ name: '模板字符串', fn: testTemplateLiterals },
|
|
54
|
+
{ name: '解构赋值', fn: testDestructuring },
|
|
55
|
+
{ name: '默认参数 / Rest / Spread', fn: testDefaultRestSpread },
|
|
56
|
+
{ name: '类(Class)', fn: testClasses },
|
|
57
|
+
{ name: 'Promise & async/await', fn: testPromises },
|
|
58
|
+
{ name: 'Symbol', fn: testSymbols },
|
|
59
|
+
{ name: '迭代器与生成器', fn: testIteratorsGenerators },
|
|
60
|
+
{ name: 'Map & Set & WeakMap & WeakSet', fn: testMapSet },
|
|
61
|
+
{ name: 'Proxy & Reflect', fn: testProxyReflect },
|
|
62
|
+
{ name: '增强对象字面量', fn: testEnhancedObjects },
|
|
63
|
+
{ name: '新增内置方法', fn: testNewMethods },
|
|
64
|
+
{ name: '模块(Modules)', fn: testModules },
|
|
65
|
+
{ name: '进制字面量与 Unicode', fn: testBinaryOctalUnicode },
|
|
66
|
+
{ name: 'for...of', fn: testForOf },
|
|
67
|
+
{ name: 'Map getOrInsert', fn: testMapGetOrInsert },
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const suites2022 = [
|
|
71
|
+
{ name: 'Class Fields(私有字段 / 静态字段)', fn: testClassFields },
|
|
72
|
+
{ name: 'Class Static Blocks', fn: testClassStaticBlocks },
|
|
73
|
+
{ name: 'Array / String .at()', fn: testAtMethod },
|
|
74
|
+
{ name: 'Object.hasOwn()', fn: testObjectHasOwn },
|
|
75
|
+
{ name: 'Error Cause', fn: testErrorCause },
|
|
76
|
+
{ name: 'RegExp /d flag(匹配索引)', fn: testRegExpDFlag },
|
|
77
|
+
{ name: 'Top-level await', fn: testTopLevelAwait },
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
const suites2025 = [
|
|
81
|
+
{ name: 'Iterator Helpers', fn: testIteratorHelpers },
|
|
82
|
+
{ name: 'New Set Methods', fn: testSetMethods },
|
|
83
|
+
{ name: 'Promise.try', fn: testPromiseTry },
|
|
84
|
+
{ name: 'RegExp Duplicate Named Capture Groups', fn: testRegExpDuplicateGroups },
|
|
85
|
+
{ name: 'Uint8Array Base64 / Hex', fn: testUint8ArrayBase64Hex },
|
|
86
|
+
{ name: 'JSON.parse Source Text Access', fn: testJsonParseSource },
|
|
87
|
+
{ name: 'Error.isError', fn: testErrorIsError },
|
|
88
|
+
{ name: 'Float16Array', fn: testFloat16Array },
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
const suites2026 = [
|
|
92
|
+
{ name: 'Math.sumPrecise()', fn: testMathSumPrecise },
|
|
93
|
+
{ name: 'RegExp.escape()', fn: testRegExpEscape },
|
|
94
|
+
{ name: 'Explicit Resource Management', fn: testExplicitResourceManagement },
|
|
95
|
+
{ name: 'Atomics.pause()', fn: testAtomicsPause },
|
|
96
|
+
{ name: 'Import Attributes', fn: testImportAttributes },
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
// ── 核心运行函数 ──────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
async function runSuites(suites) {
|
|
102
|
+
const allResults = []
|
|
103
|
+
for (const suite of suites) {
|
|
104
|
+
const results = await suite.fn()
|
|
105
|
+
allResults.push(...results)
|
|
106
|
+
}
|
|
107
|
+
return allResults
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 运行 ES2015 测试 */
|
|
111
|
+
export async function runAll2015() { return runSuites(suites2015) }
|
|
112
|
+
|
|
113
|
+
/** 运行 ES2022 测试 */
|
|
114
|
+
export async function runAll2022() { return runSuites(suites2022) }
|
|
115
|
+
|
|
116
|
+
/** 运行 ES2025 测试 */
|
|
117
|
+
export async function runAll2025() { return runSuites(suites2025) }
|
|
118
|
+
|
|
119
|
+
/** 运行 ES2026 测试 */
|
|
120
|
+
export async function runAll2026() { return runSuites(suites2026) }
|
|
121
|
+
|
|
122
|
+
/** 运行全部测试(所有版本) */
|
|
123
|
+
export async function runAll() {
|
|
124
|
+
return runSuites([...suites2015, ...suites2022, ...suites2025, ...suites2026])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 运行全部测试并分版本打印结果 */
|
|
128
|
+
export async function runAndPrint() {
|
|
129
|
+
for (const [label, fn] of [
|
|
130
|
+
['ES2015', runAll2015],
|
|
131
|
+
['ES2022', runAll2022],
|
|
132
|
+
['ES2025', runAll2025],
|
|
133
|
+
['ES2026', runAll2026],
|
|
134
|
+
]) {
|
|
135
|
+
console.log(`\n=== ${label} 特性测试 ===\n`)
|
|
136
|
+
printResults(await fn())
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── 按版本导出各测试函数 ─────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
// ES2015
|
|
143
|
+
export {
|
|
144
|
+
testLetConst, testArrowFunctions, testTemplateLiterals, testDestructuring,
|
|
145
|
+
testDefaultRestSpread, testClasses, testPromises, testSymbols,
|
|
146
|
+
testIteratorsGenerators, testMapSet, testProxyReflect, testEnhancedObjects,
|
|
147
|
+
testNewMethods, testModules, testBinaryOctalUnicode, testForOf, testMapGetOrInsert,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ES2022
|
|
151
|
+
export {
|
|
152
|
+
testClassFields, testClassStaticBlocks, testAtMethod,
|
|
153
|
+
testObjectHasOwn, testErrorCause, testRegExpDFlag, testTopLevelAwait,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ES2025
|
|
157
|
+
export {
|
|
158
|
+
testIteratorHelpers, testSetMethods, testPromiseTry, testRegExpDuplicateGroups,
|
|
159
|
+
testUint8ArrayBase64Hex, testJsonParseSource, testErrorIsError, testFloat16Array,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ES2026
|
|
163
|
+
export {
|
|
164
|
+
testMathSumPrecise, testRegExpEscape, testExplicitResourceManagement,
|
|
165
|
+
testAtomicsPause, testImportAttributes,
|
|
166
|
+
}
|
package/src/main.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/* ── 版本配置(数据驱动,新增版本只需加这里)─────────────────── */
|
|
5
|
+
const VERSIONS = [
|
|
6
|
+
{ key: '2015', run: function () { return EsFeatures.runAll2015() } },
|
|
7
|
+
{ key: '2022', run: function () { return EsFeatures.runAll2022() } },
|
|
8
|
+
{ key: '2025', run: function () { return EsFeatures.runAll2025() } },
|
|
9
|
+
{ key: '2026', run: function () { return EsFeatures.runAll2026() } },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
/* ── DOM 引用 ────────────────────────────────────────────────────── */
|
|
13
|
+
const btnRun = document.getElementById('btn-run')
|
|
14
|
+
const valTotal = document.getElementById('val-total')
|
|
15
|
+
const tabs = document.querySelectorAll('.tab')
|
|
16
|
+
|
|
17
|
+
/* ── Tab 切换 ────────────────────────────────────────────────────── */
|
|
18
|
+
tabs.forEach(function (tab) {
|
|
19
|
+
tab.addEventListener('click', function () {
|
|
20
|
+
tabs.forEach(function (t) { t.classList.remove('tab--active') })
|
|
21
|
+
tab.classList.add('tab--active')
|
|
22
|
+
document.querySelectorAll('.panel').forEach(function (p) {
|
|
23
|
+
p.classList.remove('panel--active')
|
|
24
|
+
})
|
|
25
|
+
document.getElementById(tab.dataset.panel).classList.add('panel--active')
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/* ── 工具函数 ────────────────────────────────────────────────────── */
|
|
30
|
+
|
|
31
|
+
function escape(str) {
|
|
32
|
+
return String(str)
|
|
33
|
+
.replace(/&/g, '&')
|
|
34
|
+
.replace(/</g, '<')
|
|
35
|
+
.replace(/>/g, '>')
|
|
36
|
+
.replace(/"/g, '"')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function groupBySuite(results) {
|
|
40
|
+
const suites = []
|
|
41
|
+
const map = Object.create(null)
|
|
42
|
+
results.forEach(function (r) {
|
|
43
|
+
if (!map[r.suite]) {
|
|
44
|
+
const s = { name: r.suite, cases: [] }
|
|
45
|
+
map[r.suite] = s
|
|
46
|
+
suites.push(s)
|
|
47
|
+
}
|
|
48
|
+
map[r.suite].cases.push(r)
|
|
49
|
+
})
|
|
50
|
+
return suites
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function calcStats(results) {
|
|
54
|
+
let pass = 0, fail = 0
|
|
55
|
+
results.forEach(function (r) { r.status === 'pass' ? pass++ : fail++ })
|
|
56
|
+
return { pass: pass, fail: fail, total: results.length }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ── 渲染函数 ────────────────────────────────────────────────────── */
|
|
60
|
+
|
|
61
|
+
function renderCase(c) {
|
|
62
|
+
const li = document.createElement('li')
|
|
63
|
+
li.className = 'case case--' + c.status
|
|
64
|
+
|
|
65
|
+
const icon = document.createElement('span')
|
|
66
|
+
icon.className = 'case__icon'
|
|
67
|
+
icon.textContent = c.status === 'pass' ? '✓' : '✗'
|
|
68
|
+
|
|
69
|
+
const body = document.createElement('span')
|
|
70
|
+
body.className = 'case__body'
|
|
71
|
+
|
|
72
|
+
const name = document.createElement('span')
|
|
73
|
+
name.className = 'case__name'
|
|
74
|
+
name.textContent = c.case
|
|
75
|
+
body.appendChild(name)
|
|
76
|
+
|
|
77
|
+
if (c.error) {
|
|
78
|
+
const err = document.createElement('span')
|
|
79
|
+
err.className = 'case__error'
|
|
80
|
+
err.textContent = c.error
|
|
81
|
+
body.appendChild(err)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
li.appendChild(icon)
|
|
85
|
+
li.appendChild(body)
|
|
86
|
+
return li
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderSuite(suite) {
|
|
90
|
+
let pass = 0, fail = 0
|
|
91
|
+
suite.cases.forEach(function (c) { c.status === 'pass' ? pass++ : fail++ })
|
|
92
|
+
const allPass = fail === 0
|
|
93
|
+
const stateKey = allPass ? 'pass' : 'fail'
|
|
94
|
+
|
|
95
|
+
const card = document.createElement('div')
|
|
96
|
+
card.className = 'suite'
|
|
97
|
+
|
|
98
|
+
const header = document.createElement('div')
|
|
99
|
+
header.className = 'suite__header'
|
|
100
|
+
header.innerHTML =
|
|
101
|
+
'<div class="suite__header-left">' +
|
|
102
|
+
'<span class="suite__indicator suite__indicator--' + stateKey + '"></span>' +
|
|
103
|
+
'<span class="suite__name">' + escape(suite.name) + '</span>' +
|
|
104
|
+
'</div>' +
|
|
105
|
+
'<div class="suite__header-right">' +
|
|
106
|
+
'<span class="suite__stat suite__stat--' + stateKey + '">' +
|
|
107
|
+
pass + ' / ' + suite.cases.length +
|
|
108
|
+
'</span>' +
|
|
109
|
+
'<span class="suite__chevron">▾</span>' +
|
|
110
|
+
'</div>'
|
|
111
|
+
|
|
112
|
+
header.addEventListener('click', function () {
|
|
113
|
+
card.classList.toggle('suite--collapsed')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const ul = document.createElement('ul')
|
|
117
|
+
ul.className = 'suite__cases'
|
|
118
|
+
suite.cases.forEach(function (c) { ul.appendChild(renderCase(c)) })
|
|
119
|
+
|
|
120
|
+
card.appendChild(header)
|
|
121
|
+
card.appendChild(ul)
|
|
122
|
+
return card
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function showLoader(panel) {
|
|
126
|
+
panel.innerHTML =
|
|
127
|
+
'<div class="loader">' +
|
|
128
|
+
'<span class="loader__spinner"></span>' +
|
|
129
|
+
'<span>正在运行测试…</span>' +
|
|
130
|
+
'</div>'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderPanel(panel, results) {
|
|
134
|
+
panel.innerHTML = ''
|
|
135
|
+
if (!results || results.length === 0) {
|
|
136
|
+
panel.innerHTML =
|
|
137
|
+
'<div class="placeholder">' +
|
|
138
|
+
'<span class="placeholder__icon">📭</span>' +
|
|
139
|
+
'<p class="placeholder__text">暂无测试结果</p>' +
|
|
140
|
+
'</div>'
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
const frag = document.createDocumentFragment()
|
|
144
|
+
groupBySuite(results).forEach(function (s) { frag.appendChild(renderSuite(s)) })
|
|
145
|
+
panel.appendChild(frag)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function updateSummaryValue(id, stats) {
|
|
149
|
+
const el = document.getElementById('val-' + id)
|
|
150
|
+
if (!el) return
|
|
151
|
+
if (!stats) { el.textContent = '—'; el.className = 'summary__value'; return }
|
|
152
|
+
el.textContent = stats.pass + ' / ' + stats.total
|
|
153
|
+
el.className = 'summary__value summary__value--' + (stats.fail === 0 ? 'pass' : 'fail')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function updateTabBadge(id, stats) {
|
|
157
|
+
const el = document.getElementById('badge-' + id)
|
|
158
|
+
if (!el) return
|
|
159
|
+
if (!stats) { el.textContent = ''; el.className = 'tab__badge'; return }
|
|
160
|
+
if (stats.fail === 0) {
|
|
161
|
+
el.textContent = '全部通过'
|
|
162
|
+
el.className = 'tab__badge tab__badge--pass'
|
|
163
|
+
} else {
|
|
164
|
+
el.textContent = stats.fail + ' 失败'
|
|
165
|
+
el.className = 'tab__badge tab__badge--fail'
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ── 主运行流程 ──────────────────────────────────────────────────── */
|
|
170
|
+
async function runTests() {
|
|
171
|
+
btnRun.disabled = true
|
|
172
|
+
btnRun.classList.add('btn-run--running')
|
|
173
|
+
btnRun.querySelector('.btn-run__label').textContent = '运行中…'
|
|
174
|
+
|
|
175
|
+
// 重置统计
|
|
176
|
+
VERSIONS.forEach(function (v) {
|
|
177
|
+
updateSummaryValue(v.key, null)
|
|
178
|
+
updateTabBadge(v.key, null)
|
|
179
|
+
showLoader(document.getElementById('panel-' + v.key))
|
|
180
|
+
})
|
|
181
|
+
updateSummaryValue('total', null)
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// 并行运行所有版本
|
|
185
|
+
const resultSets = await Promise.all(
|
|
186
|
+
VERSIONS.map(function (v) { return v.run() })
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
let totalPass = 0, totalFail = 0, totalCount = 0
|
|
190
|
+
|
|
191
|
+
VERSIONS.forEach(function (v, i) {
|
|
192
|
+
const results = resultSets[i]
|
|
193
|
+
const stats = calcStats(results)
|
|
194
|
+
renderPanel(document.getElementById('panel-' + v.key), results)
|
|
195
|
+
updateSummaryValue(v.key, stats)
|
|
196
|
+
updateTabBadge(v.key, stats)
|
|
197
|
+
totalPass += stats.pass
|
|
198
|
+
totalFail += stats.fail
|
|
199
|
+
totalCount += stats.total
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
updateSummaryValue('total', {
|
|
203
|
+
pass: totalPass, fail: totalFail, total: totalCount
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
} catch (err) {
|
|
207
|
+
VERSIONS.forEach(function (v) {
|
|
208
|
+
const panel = document.getElementById('panel-' + v.key)
|
|
209
|
+
if (panel) {
|
|
210
|
+
panel.innerHTML =
|
|
211
|
+
'<div class="placeholder">' +
|
|
212
|
+
'<span class="placeholder__icon">⚠️</span>' +
|
|
213
|
+
'<p class="placeholder__text">运行出错:' + escape(err.message) + '</p>' +
|
|
214
|
+
'</div>'
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
console.error('[ES 测试]', err)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
btnRun.disabled = false
|
|
221
|
+
btnRun.classList.remove('btn-run--running')
|
|
222
|
+
btnRun.querySelector('.btn-run__label').textContent = '重新运行'
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
btnRun.addEventListener('click', runTests)
|
|
226
|
+
})()
|
package/src/polyfills.js
ADDED
package/src/run.js
ADDED