diff --git a/package.json b/package.json index ccb65ea02..106e17acc 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,19 @@ "author": "Mahiru ", "scripts": { "build": "yarn parser:build && yarn webgal:build", + "build:test": "yarn parser:build && yarn webgal:build:test", "build-ci": "yarn parser:build-ci && yarn webgal:build", "dev": "yarn parser:build && yarn webgal:dev", + "dev:test": "yarn parser:build && cd packages/webgal && yarn dev:test", "webgal:dev": "cd packages/webgal && yarn dev", "webgal:build": "cd packages/webgal && yarn build", + "webgal:build:test": "cd packages/webgal && yarn build:test", "parser:test": "cd packages/parser && yarn test", "parser:test-coverage": "cd packages/parser && yarn coverage", "parser:build": "cd packages/parser && yarn build", - "parser:build-ci": "cd packages/parser && yarn build-ci" + "parser:build-ci": "cd packages/parser && yarn build-ci", + "test:integration": "cd packages/webgal-test && yarn test", + "test:integration:watch": "cd packages/webgal-test && yarn test:watch" }, "license": "MPL-2.0", "workspaces": { diff --git a/packages/webgal-test/README.md b/packages/webgal-test/README.md new file mode 100644 index 000000000..7a28570f6 --- /dev/null +++ b/packages/webgal-test/README.md @@ -0,0 +1,233 @@ +# WebGAL Integration Test Framework + +集成测试框架,通过 Playwright 浏览器自动化验证 WebGAL 引擎核心功能。 + +## 运行方式 + +### 一键运行(本地开发 / CI) + +在仓库根目录运行: + +```bash +# globalSetup 会自动检测 dist 是否存在,不存在则自动构建 +yarn test:integration +``` + +如果当前目录已经是 `packages/webgal-test`,运行: + +```bash +yarn test +``` + +### 分步运行(CI 推荐,可缓存构建产物) + +```bash +# Step 1: 构建测试包(包含 window.webgalTest 暴露) +yarn build:test + +# Step 2: 运行测试(从仓库根目录执行,从 dist 启动静态服务器 + 无头浏览器) +yarn test:integration +``` + +或在 `packages/webgal-test` 目录执行: + +```bash +yarn test +``` + +### 本地调试(显示浏览器窗口) + +```bash +WEBGAL_TEST_HEADLESS=false yarn test:integration +``` + +## 架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ yarn build:test │ +│ → parser:build → webgal:build (WEBGAL_TEST=true) │ +│ → packages/webgal/dist/ (含 window.webgalTest 暴露) │ +└──────────────────────┬──────────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────────┐ +│ vitest globalSetup │ +│ → 检测 dist 是否存在(不存在则自动构建) │ +│ → 启动内建静态服务器 (localhost:4173) │ +│ → 服务 packages/webgal/dist/ │ +└──────────────────────┬──────────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────────┐ +│ vitest test workers │ +│ → Playwright 启动无头 Chromium │ +│ → 导航到 localhost:4173 │ +│ → page.evaluate() 调用 window.webgalTest.* │ +│ → 验证状态快照、场景注入、Pixi 舞台、演出管理 等 │ +└─────────────────────────────────────────────────────────┘ +``` + +### 文件结构 + +``` +packages/webgal (被测应用) + └── src/test/ + ├── exposeTestAPI.ts 暴露内核到 window.webgalTest + └── types.ts API 类型定义 + +packages/webgal-test (测试包 - 本目录) + └── src/ + ├── server.ts 内建静态文件服务器 + ├── globalSetup.ts vitest 全局设置(构建检测 + 启动服务器) + ├── setup.ts worker 级 setup + ├── utils/ + │ ├── bridge.ts page.evaluate 桥接工具函数 + │ └── fixture.ts Playwright 浏览器生命周期管理 + ├── types/ + │ └── global.d.ts window.webgalTest 类型声明 + └── tests/ + ├── auto-mode.test.ts 自动模式测试 + ├── fast-mode.test.ts 快进模式测试 + ├── random-click.test.ts 随机点击测试 + ├── save-load.test.ts 存档/读档一致性测试 + ├── backlog.test.ts Backlog 回溯一致性测试 + ├── scene-injection.test.ts 场景注入测试 + ├── pixi-stage.test.ts Pixi 舞台状态测试 + ├── perform-manager.test.ts 演出管理器测试 + ├── test-mode-exposure.test.ts 测试构建暴露校验 + ├── click-settle-semantics.test.ts 点击先终止当前演出语义 + ├── animation-transform-backlog.test.ts 复杂变换与 backlog 测试 + └── complex-state-consistency.test.ts 复杂演出多路径状态收敛测试 +``` + +## 暴露的 API + +`window.webgalTest` 在测试模式下暴露以下模块(可通过 `page.evaluate()` 访问): + +| 模块 | 说明 | +|------|------| +| `core` | WebGAL 核心实例(sceneManager, backlogManager, animationManager, gameplay, events) | +| `live2d` | Live2D 核心实例 | +| `store` | Redux store(getState, dispatch, subscribe) | +| `pixiStage` | Pixi 舞台(figureObjects, backgroundObjects, containers, 动画注册) | +| `pixiApp` | Pixi Application 实例 | +| `sceneManager` | 场景管理器快捷访问 | +| `backlogManager` | Backlog 管理器快捷访问 | +| `animationManager` | 动画管理器快捷访问 | +| `performController` | 演出管理器(performList, arrangeNewPerform, removeAllPerform) | +| `gameplay` | Gameplay 状态(isAuto, isFast) | +| `events` | 事件系统(textSettle, userInteractNext, styleUpdate 等) | +| `controllers` | 游戏控制函数(nextSentence, switchAuto, saveGame, loadGame, changeScene, syncWithOrigine 等) | +| `sceneTools` | 场景解析/注入(sceneParser, injectScene, injectSceneAndRun, injectParsedScene;注入时可指定 sceneUrl) | +| `dispatch` | Redux dispatch 快捷方法(setStage, resetStageState, setVisibility) | +| `config` | 系统配置(SYSTEM_CONFIG, PERFORM_CONFIG) | +| `takeSnapshot()` | 完整状态快照(Redux, scene, backlog, performs, pixi, gameplay, animations) | +| `metadata` | 测试构建元数据(testMode, apiVersion, locationHref) | +| `testTools` | 测试专用工具(resetRuntime, settleText, settleAnimations, 文本状态、动画队列、锁定目标、内部 effects、选项设置) | +| `utils` | 工具函数(cloneDeep) | + +## 环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `WEBGAL_TEST_URL` | `http://localhost:4173` | 由 globalSetup 自动设置,也可手动覆盖 | +| `WEBGAL_TEST_PORT` | `4173` | 静态服务器端口 | +| `WEBGAL_TEST_HEADLESS` | `true` | 设为 `false` 显示浏览器窗口(调试用) | +| `WEBGAL_TEST_SKIP_BUILD` | `false` | 设为 `true` 时,dist 不存在或不是测试构建会直接报错而非自动构建 | +| `WEBGAL_TEST_FORCE_BUILD` | `false` | 设为 `true` 时,忽略现有 dist 并强制重新构建测试产物 | + +## 测试说明 + +### test-mode-exposure.test.ts +- 验证浏览器实际加载的是含 `window.webgalTest` 的测试构建产物 +- 验证测试元数据、Pixi 动画队列、动画锁、文本渐显状态等运行时观测 API 可用 + +### click-settle-semantics.test.ts +- 验证对话文字仍在渐显时,第一次点击只触发当前对话终态 +- 验证当前对话终态后,再次点击才推进到下一句 + +### animation-transform-backlog.test.ts +- 验证复杂 `setTransform` 动画会锁定目标,并能通过测试 API 观察 active animation +- 验证点击中断动画会写入 Pixi 容器终态和 stage `effects` 内部变换 +- 验证复杂变换后的 backlog 跳转能恢复同一终态 + +### complex-state-consistency.test.ts +- 构造多立绘、背景、交叠动画、复杂滤镜/位移/缩放/透明度变换的同一场景 +- 验证手动点击、Fast Skip、Auto、编辑器 `syncWithOrigine` 快速同步、`loadGameFromStageData` 到达同一 checkpoint 后,稳定舞台状态与 Pixi 关键状态一致 +- 验证从 checkpoint 继续推进后,存读档和 backlog 跳转都能恢复同一稳定状态 +- 稳定状态比较会排除随机动画名、对话 key、时间戳等不稳定字段,但保留 `effects`、Pixi transform、锁定目标、活动动画、文本终态、backlog 长度等关键属性 + +### auto-mode.test.ts (2 tests) +- 验证自动模式开启后游戏自动推进 +- 验证停止后不再推进 + +### fast-mode.test.ts (3 tests) +- 验证快进模式快速推进(比自动模式快) +- 验证停止后不再推进 + +### random-click.test.ts (4 tests) +- 模拟用户不规律点击,验证游戏不崩溃 +- 模拟疯狂连点,验证不出现状态损坏 +- 验证随机操作中存档/读档一致 + +### save-load.test.ts (4 tests) +- 验证存档/读档核心状态完全一致 +- 验证从存档点重放与原始执行到达相同状态 +- 验证多个存档槽位互不干扰 +- 验证存档数据包含正确的 backlog 和场景信息 + +### backlog.test.ts (4 tests) +- 验证推进过程中 backlog 正确累积 +- 验证 backlog 跳转后状态恢复 +- 验证连续多次跳转的一致性 +- 验证跳转后继续推进,backlog 正确截断续接 + +### scene-injection.test.ts (5 tests) +- 注入原始 WebGAL 脚本文本并验证场景数据更新 +- 注入后通过 nextSentence 依次推进 +- 注入含 changeBg 等指令的场景 +- 验证注入后 backlog 正确记录 +- 验证注入后快照数据完整 + +### pixi-stage.test.ts (6 tests) +- 读取立绘列表及 transform 属性(x, y, scale, rotation, alpha, visible) +- 读取背景列表 +- 验证 snapshot 中 pixiState 结构 +- 注入含 changeFigure 的场景后验证舞台对象结构 +- 验证 performs 在快照中的数据结构 +- 验证 animations 在快照中的数据结构 + +### perform-manager.test.ts (4 tests) +- 读取当前演出列表 +- 注入对话场景后验证演出状态 +- removeAllPerform 清空演出列表 +- 验证 perform 的阻塞属性 + +## 工作原理 + +1. **编译标志**:`WEBGAL_TEST=true` 环境变量触发 Vite `define` 注入 `__WEBGAL_TEST__` +2. **API 暴露**:测试模式下 `main.tsx` 动态加载 `src/test/`,将 WebGAL 核心、Redux store、Pixi 舞台、控制器、场景工具等绑定到 `window.webgalTest` +3. **自动构建**:vitest `globalSetup` 检测 `packages/webgal/dist/` 是否存在,且会扫描构建产物确认其中包含测试 API;缺失或不是测试构建则自动执行 `yarn build:test` +4. **内建服务器**:globalSetup 启动零依赖 Node.js 静态文件服务器(`src/server.ts`),服务构建产物 +5. **浏览器桥接**:vitest 通过 Playwright 打开无头 Chromium,导航到内建服务器,通过 `page.evaluate()` 调用 `window.webgalTest` 上的方法 +6. **场景注入**:`injectSceneAndRun()` 会先清理演出、动画、backlog、舞台对象和场景状态,再直接将 WebGAL 脚本文本解析为场景并替换当前场景 +7. **状态快照**:`takeSnapshot()` 深拷贝 Redux state、场景数据、backlog、演出列表、Pixi 舞台对象、active animations、动画锁、文本渐显状态、gameplay 状态、动画列表 +8. **一致性比较**:`compareSnapshots()` 排除不稳定字段(PerformList、currentDialogKey 等)后进行递归稳定 JSON 比较;复杂视觉路径使用 `compareStableRuntimeSnapshots()` 比较稳定舞台/Pixi 关键字段 +9. **自动清理**:测试完成后 globalSetup teardown 自动关闭静态服务器 + +## CI 示例(GitHub Actions) + +```yaml +- name: Install dependencies + run: yarn install --frozen-lockfile + +- name: Install Playwright + run: npx playwright install chromium + +- name: Build test bundle + run: yarn build:test + +- name: Run integration tests + run: yarn test:integration + env: + WEBGAL_TEST_SKIP_BUILD: 'true' +``` diff --git a/packages/webgal-test/package.json b/packages/webgal-test/package.json new file mode 100644 index 000000000..993c0491f --- /dev/null +++ b/packages/webgal-test/package.json @@ -0,0 +1,17 @@ +{ + "name": "webgal-test", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "WebGAL integration test framework", + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui" + }, + "devDependencies": { + "vitest": "^3.2.1", + "playwright": "^1.52.0", + "typescript": "^5.8.3" + } +} diff --git a/packages/webgal-test/src/globalSetup.ts b/packages/webgal-test/src/globalSetup.ts new file mode 100644 index 000000000..b334bc0b1 --- /dev/null +++ b/packages/webgal-test/src/globalSetup.ts @@ -0,0 +1,117 @@ +/** + * vitest globalSetup + * + * 流程: + * 1. 检查 WebGAL 测试构建产物是否存在(packages/webgal/dist) + * 2. 若不存在,自动执行 build:test 构建 + * 3. 启动内建静态服务器,在固定端口服务 dist + * 4. 测试结束后停止服务器 + * + * CI 用法: + * 先 yarn build:test,再 yarn test:integration(跳过构建直接起服务) + * 本地用法: + * 直接 yarn test:integration(自动触发构建) + */ +import { execSync } from 'node:child_process'; +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import { resolve, dirname, join, extname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { startServer } from './server'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const PORT = Number(process.env.WEBGAL_TEST_PORT) || 4173; + +let server: import('node:http').Server | null = null; + +function listFiles(dir: string): string[] { + if (!existsSync(dir)) return []; + const files: string[] = []; + for (const entry of readdirSync(dir)) { + const filePath = join(dir, entry); + const stat = statSync(filePath); + if (stat.isDirectory()) { + files.push(...listFiles(filePath)); + } else { + files.push(filePath); + } + } + return files; +} + +function hasTestExposureInBundle(distDir: string): boolean { + const bundleFiles = listFiles(distDir).filter((file) => ['.js', '.mjs', '.html'].includes(extname(file))); + return bundleFiles.some((file) => { + const content = readFileSync(file, 'utf8'); + return content.includes('webgalTest') && content.includes('WebGAL Test Mode Active'); + }); +} + +function buildTestBundle(rootDir: string): void { + execSync('yarn parser:build && yarn webgal:build:test', { + cwd: rootDir, + stdio: 'inherit', + }); +} + +export async function setup() { + const testPkgDir = resolve(__dirname, '..'); + const rootDir = resolve(testPkgDir, '../..'); + const webgalDist = resolve(rootDir, 'packages/webgal/dist'); + + const forceBuild = process.env.WEBGAL_TEST_FORCE_BUILD === 'true'; + const skipBuild = process.env.WEBGAL_TEST_SKIP_BUILD === 'true'; + const hasDist = existsSync(webgalDist); + const hasTestExposure = hasDist && hasTestExposureInBundle(webgalDist); + + // dist 可能来自普通生产构建;必须确认测试 API 真在构建产物中。 + if (forceBuild || !hasDist || !hasTestExposure) { + if (skipBuild) { + const reason = !hasDist + ? 'WebGAL test dist not found at ' + : 'WebGAL dist exists but does not contain the test API exposure at '; + throw new Error(reason + webgalDist + '\nRun `yarn build:test` at the project root first.'); + } + if (!hasDist) { + console.log('\nWebGAL dist not found, building in test mode...'); + } else if (!hasTestExposure) { + console.log('\nWebGAL dist is not a test-mode bundle, rebuilding...'); + } else { + console.log('\nWEBGAL_TEST_FORCE_BUILD=true, rebuilding test bundle...'); + } + buildTestBundle(rootDir); + } + + if (!existsSync(webgalDist)) { + throw new Error('WebGAL dist still not found after build. Check build logs.'); + } + + if (!hasTestExposureInBundle(webgalDist)) { + if (process.env.WEBGAL_TEST_SKIP_BUILD === 'true') { + throw new Error( + 'WebGAL dist does not contain window.webgalTest exposure at ' + + webgalDist + + '\nRun `yarn build:test` at the project root first.', + ); + } + throw new Error('WebGAL dist still does not expose window.webgalTest after build. Check build logs.'); + } + + // 启动静态服务器 + server = await startServer(webgalDist, PORT); + const address = server.address(); + const actualPort = typeof address === 'object' && address ? address.port : PORT; + const url = `http://localhost:${actualPort}`; + console.log(`\nServing WebGAL test dist at ${url}`); + + // 传递给 test worker(vitest forks 继承父进程环境变量) + process.env.WEBGAL_TEST_URL = url; +} + +export async function teardown() { + if (server) { + server.close(); + console.log('\n🛑 Static server stopped.'); + } +} diff --git a/packages/webgal-test/src/server.ts b/packages/webgal-test/src/server.ts new file mode 100644 index 000000000..52ad0d887 --- /dev/null +++ b/packages/webgal-test/src/server.ts @@ -0,0 +1,101 @@ +/** + * 轻量级静态文件服务器(Node 内建,零依赖) + * + * 用于在测试期间服务 WebGAL 的构建产物 (dist) + */ +import http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; +import { URL } from 'node:url'; + +const MIME: Record = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript', + '.mjs': 'text/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.webp': 'image/webp', + '.avif': 'image/avif', + '.mp3': 'audio/mpeg', + '.ogg': 'audio/ogg', + '.wav': 'audio/wav', + '.flac': 'audio/flac', + '.mp4': 'video/mp4', + '.webm': 'video/webm', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.ttf': 'font/ttf', + '.otf': 'font/otf', + '.wasm': 'application/wasm', + '.txt': 'text/plain; charset=utf-8', + '.xml': 'application/xml', + '.map': 'application/json', +}; + +function createStaticServer(root: string): http.Server { + const normalizedRoot = path.resolve(root); + return http.createServer((req, res) => { + const url = new URL(req.url || '/', `http://${req.headers.host}`); + const pathname = decodeURIComponent(url.pathname); + + let filePath = path.resolve(normalizedRoot, `.${pathname}`); + if (filePath !== normalizedRoot && !filePath.startsWith(normalizedRoot + path.sep)) { + res.writeHead(403, { 'Content-Type': 'text/plain' }); + res.end('Forbidden'); + return; + } + + // 目录 → index.html + try { + const stat = fs.statSync(filePath); + if (stat.isDirectory()) { + filePath = path.join(filePath, 'index.html'); + } + } catch { + // 文件不存在,SPA 回退到 index.html + const indexPath = path.join(normalizedRoot, 'index.html'); + if (fs.existsSync(indexPath)) { + filePath = indexPath; + } + } + + try { + const data = fs.readFileSync(filePath); + const ext = path.extname(filePath).toLowerCase(); + res.writeHead(200, { + 'Content-Type': MIME[ext] || 'application/octet-stream', + 'Content-Length': data.length, + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + }); + res.end(data); + } catch { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } + }); +} + +export function startServer(root: string, port: number): Promise { + const server = createStaticServer(root); + return new Promise((resolve, reject) => { + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + console.warn(`Port ${port} in use, trying ${port + 1}...`); + server.close(); + startServer(root, port + 1).then(resolve, reject); + } else { + reject(err); + } + }); + server.listen(port, () => { + resolve(server); + }); + }); +} diff --git a/packages/webgal-test/src/setup.ts b/packages/webgal-test/src/setup.ts new file mode 100644 index 000000000..fa6fecb88 --- /dev/null +++ b/packages/webgal-test/src/setup.ts @@ -0,0 +1,7 @@ +/** + * Vitest setupFile — 每个 test worker 启动时执行 + */ +console.log('🧪 WebGAL Integration Test'); +console.log(` URL: ${process.env.WEBGAL_TEST_URL ?? 'http://localhost:4173'}`); +console.log(` Headless: ${process.env.WEBGAL_TEST_HEADLESS !== 'false'}`); +console.log(''); diff --git a/packages/webgal-test/src/tests/animation-transform-backlog.test.ts b/packages/webgal-test/src/tests/animation-transform-backlog.test.ts new file mode 100644 index 000000000..52639b0b0 --- /dev/null +++ b/packages/webgal-test/src/tests/animation-transform-backlog.test.ts @@ -0,0 +1,138 @@ +/** + * 复杂变换、动画终态与 backlog 测试 + * + * 覆盖内部变换 effects、Pixi 动画锁、点击终止动画、backlog 跳转恢复同一终态。 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + clickStage, + closeBrowser, + createTestPage, + flushBrowserTasks, + getBacklog, + getCurrentSentenceId, + getLockedTargets, + getStageEffect, + getStageObjectByKey, + injectSceneAndRun, + callJumpFromBacklog, + setOptionData, + waitForActiveAnimationOnTarget, + waitForNoActiveAnimationOnTarget, + waitForNoTransientPerforms, + waitForSentenceAdvance, +} from '../utils'; + +async function waitForShownText(page: Page, text: string): Promise { + await page.waitForFunction( + (expectedText) => window.webgalTest!.store.getState().stage.showText.includes(expectedText), + text, + { timeout: 10_000 }, + ); +} + +function transformOf(effect: Awaited>): Record { + expect(effect).not.toBeNull(); + return effect!.transform as Record; +} + +describe('Animation Transform Backlog', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + }); + + afterAll(async () => { + if (page) { + await page.context().close(); + } + await closeBrowser(); + }); + + it('should settle transform animation to end state before the next click advances', async () => { + await setOptionData(page, 'textSpeed', 20); + + const enterTransform = + '{"position":{"x":-160,"y":30},"scale":{"x":0.85,"y":0.85},"alpha":0.6,"rotation":0.08,"blur":2,"brightness":1.1,"contrast":1.05,"saturation":1.2}'; + const movedTransform = + '{"position":{"x":280,"y":-40},"scale":{"x":1.2,"y":1.2},"alpha":0.75,"rotation":0.18,"blur":6,"brightness":1.35,"contrast":1.2,"saturation":1.4,"bloom":0.7,"bloomBrightness":1.3,"bloomBlur":8}'; + const resetTransform = + '{"position":{"x":-60,"y":10},"scale":{"x":0.9,"y":0.9},"alpha":1,"rotation":0,"blur":0,"brightness":1,"contrast":1,"saturation":1}'; + + await injectSceneAndRun( + page, + [ + 'changeBg:bg.webp -next -duration=300 -transform={"position":{"x":40,"y":-20},"scale":{"x":1.1,"y":1.1},"alpha":0.8};', + `changeFigure:stand.webp -left -id=hero -next -duration=400 -transform=${enterTransform};`, + '演出一:复杂进入动画后,文字仍然应遵守先终止再推进;', + `setTransform:${movedTransform} -target=hero -duration=900;`, + '演出二:上一句动画的终态必须写入内部变换和 backlog;', + `setTransform:${resetTransform} -target=hero -duration=300;`, + '演出三:如果能看到这里,说明已经越过第二个终态;', + ].join('\n'), + 'animation-transform-backlog', + ); + + await waitForShownText(page, '演出一'); + await flushBrowserTasks(page, 50); + + await clickStage(page); + await waitForNoTransientPerforms(page); + const firstDialogueId = await getCurrentSentenceId(page); + + await clickStage(page); + await waitForActiveAnimationOnTarget(page, 'hero'); + const idWhileTransforming = await getCurrentSentenceId(page); + expect(idWhileTransforming).toBeGreaterThan(firstDialogueId); + expect(await getLockedTargets(page)).toContain('hero'); + + const effectWhileTransforming = transformOf(await getStageEffect(page, 'hero')); + expect(effectWhileTransforming.position.x).toBe(280); + expect(effectWhileTransforming.position.y).toBe(-40); + expect(effectWhileTransforming.blur).toBe(6); + expect(effectWhileTransforming.bloom).toBe(0.7); + + await clickStage(page); + await waitForNoActiveAnimationOnTarget(page, 'hero'); + await waitForNoTransientPerforms(page); + const idAfterSettleClick = await getCurrentSentenceId(page); + expect(idAfterSettleClick).toBe(idWhileTransforming); + + const heroAfterSettle = await getStageObjectByKey(page, 'hero'); + expect(heroAfterSettle?.transform?.x).toBeCloseTo(280, 0); + expect(heroAfterSettle?.transform?.y).toBeCloseTo(-40, 0); + expect(heroAfterSettle?.transform?.alpha).toBeCloseTo(0.75, 2); + + await clickStage(page); + await waitForSentenceAdvance(page, idAfterSettleClick); + await waitForShownText(page, '演出二'); + await flushBrowserTasks(page, 100); + + const backlogAfterSecondDialogue = await getBacklog(page); + const secondDialogueBacklogIndex = backlogAfterSecondDialogue.length - 1; + expect(secondDialogueBacklogIndex).toBeGreaterThanOrEqual(0); + + await clickStage(page); + await waitForNoTransientPerforms(page); + const idBeforeResetTransform = await getCurrentSentenceId(page); + + await clickStage(page); + await waitForActiveAnimationOnTarget(page, 'hero'); + await clickStage(page); + await waitForNoActiveAnimationOnTarget(page, 'hero'); + const resetEffect = transformOf(await getStageEffect(page, 'hero')); + expect(resetEffect.position.x).toBe(-60); + + await callJumpFromBacklog(page, secondDialogueBacklogIndex, false); + await flushBrowserTasks(page, 200); + await waitForShownText(page, '演出二'); + + const restoredEffect = transformOf(await getStageEffect(page, 'hero')); + expect(restoredEffect.position.x).toBe(280); + expect(restoredEffect.position.y).toBe(-40); + expect(restoredEffect.blur).toBe(6); + expect(await getCurrentSentenceId(page)).toBeGreaterThanOrEqual(idBeforeResetTransform); + }); +}); diff --git a/packages/webgal-test/src/tests/auto-mode.test.ts b/packages/webgal-test/src/tests/auto-mode.test.ts new file mode 100644 index 000000000..b944c116a --- /dev/null +++ b/packages/webgal-test/src/tests/auto-mode.test.ts @@ -0,0 +1,78 @@ +/** + * 自动模式(Auto)测试 + * + * 验证: + * 1. 开启自动模式后,游戏能自动推进 + * 2. 推进过程中状态正常更新(backlog 增长、sentenceId 递增) + * 3. 停止自动模式后游戏停止推进 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + createTestPage, + startGameAndWait, + closeBrowser, + callSwitchAuto, + callStopAuto, + callStopAll, + injectSceneAndRun, + getCurrentSentenceId, + getBacklogLength, + getIsAuto, + delay, + generateTestScene, +} from '../utils'; + +describe('Auto Mode Test', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + await startGameAndWait(page); + // 注入含 100 条语句的测试场景,确保 auto 模式有足够内容推进 + await injectSceneAndRun(page, generateTestScene(100, 'auto'), 'auto-test'); + await delay(500); + }); + + afterAll(async () => { + if (page) { + await callStopAll(page).catch(() => {}); + await page.context().close(); + } + await closeBrowser(); + }); + + it('should activate auto mode and advance sentences', async () => { + const initialId = await getCurrentSentenceId(page); + const initialBacklog = await getBacklogLength(page); + + await callSwitchAuto(page); + expect(await getIsAuto(page)).toBe(true); + + // auto 每次间隔 250-1750ms(取决于速度设置),等 10 秒应有多步推进 + await delay(10000); + + await callStopAuto(page); + expect(await getIsAuto(page)).toBe(false); + + const afterId = await getCurrentSentenceId(page); + const afterBacklog = await getBacklogLength(page); + + // 应当推进了至少 2 句 + expect(afterId).toBeGreaterThan(initialId); + expect(afterBacklog).toBeGreaterThanOrEqual(initialBacklog); + }); + + it('should stop advancing after stopping auto mode', async () => { + await callSwitchAuto(page); + await delay(5000); + await callStopAuto(page); + + const idAfterStop = await getCurrentSentenceId(page); + await delay(3000); + const idLater = await getCurrentSentenceId(page); + + // 停止后不应继续推进 + expect(idLater).toBe(idAfterStop); + }); +}); diff --git a/packages/webgal-test/src/tests/backlog.test.ts b/packages/webgal-test/src/tests/backlog.test.ts new file mode 100644 index 000000000..274b848f9 --- /dev/null +++ b/packages/webgal-test/src/tests/backlog.test.ts @@ -0,0 +1,120 @@ +/** + * Backlog / 回溯一致性测试 + * + * 验证: + * 1. 推进过程中 backlog 正确记录 + * 2. jumpFromBacklog 跳转到历史记录后,状态正确变化 + * 3. 跳转后继续推进,backlog 正确续接 + * 4. backlog 跳转与 save/load 在同一点时结果一致 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + createTestPage, + startGameAndWait, + closeBrowser, + callNextSentence, + callStopAll, + callJumpFromBacklog, + callSaveGame, + callLoadGame, + injectSceneAndRun, + takeSnapshot, + getCurrentSentenceId, + getBacklogLength, + getBacklog, + delay, + compareSnapshots, + generateTestScene, +} from '../utils'; + +/** 推进 N 步 */ +async function advanceSteps(page: Page, steps: number): Promise { + for (let i = 0; i < steps; i++) { + await callNextSentence(page); + await delay(200); + await callNextSentence(page); + await delay(300); + } +} + +describe('Backlog Consistency Test', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + await startGameAndWait(page); + await injectSceneAndRun(page, generateTestScene(100, '回溯'), 'backlog-test'); + await delay(500); + }); + + afterAll(async () => { + if (page) { + await callStopAll(page).catch(() => {}); + await page.context().close(); + } + await closeBrowser(); + }); + + it('should accumulate backlog entries as game advances', async () => { + const initialLength = await getBacklogLength(page); + + await advanceSteps(page, 8); + + const afterLength = await getBacklogLength(page); + expect(afterLength).toBeGreaterThan(initialLength); + }); + + it('should restore state when jumping from backlog', async () => { + await advanceSteps(page, 5); + + const backlog = await getBacklog(page); + expect(backlog.length).toBeGreaterThanOrEqual(2); + + const targetIndex = backlog.length - 2; + const beforeJump = await takeSnapshot(page); + + await callJumpFromBacklog(page, targetIndex); + await delay(2000); + + const afterJump = await takeSnapshot(page); + expect(afterJump.backlogLength).toBeLessThanOrEqual(beforeJump.backlogLength); + }); + + it('should correctly truncate and continue backlog after jump', async () => { + await advanceSteps(page, 8); + + const backlogBefore = await getBacklog(page); + if (backlogBefore.length < 3) return; + + const jumpIndex = Math.floor(backlogBefore.length / 2); + await callJumpFromBacklog(page, jumpIndex); + await delay(2000); + + const backlogAfterJump = await getBacklog(page); + expect(backlogAfterJump.length).toBeLessThanOrEqual(backlogBefore.length); + + await advanceSteps(page, 5); + + const backlogAfterAdvance = await getBacklog(page); + expect(backlogAfterAdvance.length).toBeGreaterThanOrEqual(backlogAfterJump.length); + }); + + it('should be consistent between backlog jump and save/load at same point', async () => { + await advanceSteps(page, 4); + + const snapshotAtSavePoint = await takeSnapshot(page); + await callSaveGame(page, 96); + await delay(500); + + await advanceSteps(page, 5); + + await callLoadGame(page, 96); + await delay(2000); + const loadedSnapshot = await takeSnapshot(page); + + const { match, diffs } = compareSnapshots(snapshotAtSavePoint, loadedSnapshot); + expect(diffs).toEqual([]); + expect(match).toBe(true); + }); +}); diff --git a/packages/webgal-test/src/tests/click-settle-semantics.test.ts b/packages/webgal-test/src/tests/click-settle-semantics.test.ts new file mode 100644 index 000000000..8192b48b3 --- /dev/null +++ b/packages/webgal-test/src/tests/click-settle-semantics.test.ts @@ -0,0 +1,70 @@ +/** + * 鼠标点击推进哲学测试 + * + * Galgame 常见语义:当前句子还有演出或文字渐显时,第一次点击只把当前状态推到终态; + * 当前状态已经终态时,再次点击才进入下一句。 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + clickStage, + closeBrowser, + createTestPage, + getCurrentSentenceId, + getStageState, + injectSceneAndRun, + setOptionData, + waitForSentenceAdvance, + waitForTextPending, + waitForTextSettled, + waitForNoTransientPerforms, + waitForTransientPerform, +} from '../utils'; + +describe('Click To Settle Semantics', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + }); + + afterAll(async () => { + if (page) { + await page.context().close(); + } + await closeBrowser(); + }); + + it('should settle a revealing dialogue before advancing to the next sentence', async () => { + await setOptionData(page, 'textSpeed', 0); + await injectSceneAndRun( + page, + [ + '第一句很长很长很长很长很长很长很长很长很长,用来确保文字仍在渐显;', + '第二句只有在第一句终态后再次点击才应该出现;', + ].join('\n'), + 'click-settle-dialogue', + ); + + await waitForTransientPerform(page); + const pendingText = await waitForTextPending(page); + const sentenceIdWhileRevealing = await getCurrentSentenceId(page); + expect(pendingText.shownText).toContain('第一句'); + + await clickStage(page); + await waitForNoTransientPerforms(page); + const settledText = await waitForTextSettled(page); + const sentenceIdAfterSettleClick = await getCurrentSentenceId(page); + const stageAfterSettleClick = await getStageState(page); + + expect(sentenceIdAfterSettleClick).toBe(sentenceIdWhileRevealing); + expect(settledText.pendingElements).toBe(0); + expect(stageAfterSettleClick.showText).toContain('第一句'); + + await clickStage(page); + await waitForSentenceAdvance(page, sentenceIdAfterSettleClick); + const stageAfterAdvanceClick = await getStageState(page); + + expect(stageAfterAdvanceClick.showText).toContain('第二句'); + }); +}); diff --git a/packages/webgal-test/src/tests/complex-state-consistency.test.ts b/packages/webgal-test/src/tests/complex-state-consistency.test.ts new file mode 100644 index 000000000..0dfd8009a --- /dev/null +++ b/packages/webgal-test/src/tests/complex-state-consistency.test.ts @@ -0,0 +1,328 @@ +/** + * 复杂演出状态收敛测试 + * + * 同一段多立绘、多动画、多变换的演出,到达同一个 checkpoint 后, + * 手动点击、快进、自动播放、编辑器快速同步、stageData 读入、存读档和回溯都应恢复一致的稳定舞台/Pixi 状态。 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + callGenerateCurrentStageData, + callJumpFromBacklog, + callLoadGame, + callLoadGameFromStageData, + callSaveGame, + callStopAll, + callStopAuto, + callStopFast, + callSwitchAuto, + callSwitchFast, + callSyncWithOrigine, + clickStage, + closeBrowser, + compareStableRuntimeSnapshots, + createTestPage, + flushBrowserTasks, + getActiveAnimations, + getBacklog, + getCurrentSentenceId, + getPerformList, + getStageEffect, + getStageObjectByKey, + getTextState, + injectSceneAndRun, + resetRuntime, + routeScene, + setOptionData, + settleAnimations, + settleText, + takeSnapshot, + toStableRuntimeSnapshot, + waitForNoActiveAnimations, + waitForNoTransientPerforms, + waitForTextSettled, + type StableRuntimeSnapshot, +} from '../utils'; + +const SCENE_NAME = 'complex-state-consistency.txt'; +const SCENE_URL = `./game/scene/${SCENE_NAME}`; +const CHECKPOINT_TEXT = '复杂状态检查点:所有路径应该收敛到同一舞台'; +const AFTER_CHECKPOINT_TEXT = '检查点之后:用于证明读档和回溯确实回退'; + +const bgEnter = + '{"position":{"x":60,"y":-28},"scale":{"x":1.12,"y":1.12},"alpha":0.82,"rotation":0.03,"blur":2,"brightness":1.08,"contrast":1.06,"saturation":1.18}'; +const heroEnter = + '{"position":{"x":-320,"y":30},"scale":{"x":0.82,"y":0.82},"alpha":0.7,"rotation":-0.08,"blur":1,"brightness":1.12,"contrast":1.08,"saturation":1.16}'; +const rivalEnter = + '{"position":{"x":320,"y":10},"scale":{"x":0.88,"y":0.88},"alpha":0.72,"rotation":0.08,"blur":2,"brightness":0.96,"contrast":1.16,"saturation":0.9}'; +const allyEnter = + '{"position":{"x":0,"y":120},"scale":{"x":0.78,"y":0.78},"alpha":0.65,"rotation":0,"blur":0,"brightness":1.18,"contrast":1.04,"saturation":1.1}'; +const heroTemp = + '[{"position":{"x":-320,"y":30},"scale":{"x":0.82,"y":0.82},"alpha":0.7,"rotation":-0.08,"duration":0},{"position":{"x":-80,"y":-30},"scale":{"x":1.08,"y":1.08},"alpha":1,"rotation":0.12,"blur":4,"duration":280},{"position":{"x":140,"y":-20},"scale":{"x":1.02,"y":1.02},"alpha":0.9,"rotation":-0.04,"blur":1,"duration":360}]'; +const rivalMid = + '{"position":{"x":-180,"y":24},"scale":{"x":1.04,"y":1.04},"alpha":0.86,"rotation":-0.18,"blur":5,"brightness":0.9,"contrast":1.24,"saturation":0.78,"bloom":0.45,"bloomBrightness":1.18,"bloomBlur":5}'; +const allyMid = + '{"position":{"x":60,"y":52},"scale":{"x":1.16,"y":1.16},"alpha":0.92,"rotation":0.06,"blur":3,"brightness":1.24,"contrast":1.1,"saturation":1.26}'; +const bgMid = + '{"position":{"x":-70,"y":36},"scale":{"x":1.2,"y":1.2},"alpha":0.88,"rotation":-0.02,"blur":4,"brightness":0.94,"contrast":1.22,"saturation":0.82,"oldFilm":0.2}'; +const heroFinal = + '{"position":{"x":220,"y":-60},"scale":{"x":1.1,"y":1.1},"alpha":0.95,"rotation":0.1,"blur":0,"brightness":1.18,"contrast":1.12,"saturation":1.24,"bloom":0.35,"bloomBrightness":1.14,"bloomBlur":4}'; +const rivalFinal = + '{"position":{"x":-260,"y":20},"scale":{"x":0.92,"y":0.92},"alpha":0.8,"rotation":-0.12,"blur":4,"brightness":0.88,"contrast":1.2,"saturation":0.75,"rgbFilm":0.18}'; +const allyFinal = + '{"position":{"x":0,"y":80},"scale":{"x":1.05,"y":1.05},"alpha":1,"rotation":0,"blur":0,"brightness":1.05,"contrast":1,"saturation":1.08}'; +const bgFinal = + '{"position":{"x":10,"y":-20},"scale":{"x":1.08,"y":1.08},"alpha":0.9,"rotation":0,"blur":1,"brightness":1.02,"contrast":1.04,"saturation":1.15}'; +const postCheckpoint = + '{"position":{"x":-360,"y":120},"scale":{"x":0.7,"y":0.7},"alpha":0.45,"rotation":-0.3,"blur":8,"brightness":0.7,"contrast":1.4,"saturation":0.5}'; + +const COMPLEX_SCENE_LINES = [ + 'setVar:route=complex;', + 'setVar:checkpoint=0;', + `changeBg:bg.webp -next -duration=520 -transform=${bgEnter};`, + `changeFigure:stand.webp -left -id=hero -next -duration=480 -transform=${heroEnter} -zIndex=11;`, + `changeFigure:stand2.webp -right -id=rival -next -duration=560 -transform=${rivalEnter} -zIndex=9;`, + `changeFigure:stand.webp -center -id=ally -next -duration=440 -transform=${allyEnter} -zIndex=10;`, + 'miniAvatar:miniavatar.webp;', + 'filmMode:cinema;', + 'setTextbox:show -next;', + '复杂演出开始:第一段对话用于生成 backlog;', + `setTempAnimation:${heroTemp} -target=hero -next;`, + `setTransform:${rivalMid} -target=rival -duration=620 -next;`, + `setTransform:${allyMid} -target=ally -duration=580 -next;`, + `setTransform:${bgMid} -target=bg-main -duration=540 -next;`, + '复杂演出中段:多个目标动画正在交叠;', + 'setVar:checkpoint=1;', + `setTransform:${heroFinal} -target=hero -duration=760 -next;`, + `setTransform:${rivalFinal} -target=rival -duration=700 -next;`, + `setTransform:${allyFinal} -target=ally -duration=680 -next;`, + `setTransform:${bgFinal} -target=bg-main -duration=620 -next;`, + `${CHECKPOINT_TEXT};`, + `setTransform:${postCheckpoint} -target=hero -duration=300 -next;`, + `${AFTER_CHECKPOINT_TEXT};`, +]; + +const COMPLEX_SCENE = COMPLEX_SCENE_LINES.join('\n'); +const CHECKPOINT_SENTENCE_ID = COMPLEX_SCENE_LINES.findIndex((line) => line.includes(CHECKPOINT_TEXT)) + 1; + +async function waitForShownText(page: Page, text: string, timeout = 20_000): Promise { + await page.waitForFunction( + (expectedText) => window.webgalTest!.store.getState().stage.showText.includes(expectedText), + text, + { timeout, polling: 10 }, + ); +} + +async function waitForShownTextAndStopMode( + page: Page, + text: string, + mode: 'auto' | 'fast', + timeout = 20_000, +): Promise { + await page.waitForFunction( + ([expectedText, stopMode]) => { + const isShown = window.webgalTest!.store.getState().stage.showText.includes(expectedText); + if (isShown) { + if (stopMode === 'auto') window.webgalTest!.controllers.stopAuto(); + else window.webgalTest!.controllers.stopFast(); + } + return isShown; + }, + [text, mode] as const, + { timeout, polling: 10 }, + ); +} + +async function isShownText(page: Page, text: string): Promise { + return page.evaluate((expectedText) => window.webgalTest!.store.getState().stage.showText.includes(expectedText), text); +} + +async function settleCurrentPresentation(page: Page): Promise { + const [textState, performs, animations] = await Promise.all([ + getTextState(page), + getPerformList(page), + getActiveAnimations(page), + ]); + const hasTransientPerform = performs.some((perform) => !perform.isHoldOn && !perform.skipNextCollect); + + if (hasTransientPerform) { + await clickStage(page); + } else { + if (textState.pendingElements > 0) { + await settleText(page); + } + if (animations.length > 0) { + await settleAnimations(page); + } + } + + await flushBrowserTasks(page, 150); + await waitForNoTransientPerforms(page); + await waitForNoActiveAnimations(page); + await waitForTextSettled(page); +} + +async function prepareComplexScene(page: Page): Promise { + await callStopAll(page).catch(() => {}); + await resetRuntime(page); + await setOptionData(page, 'textSpeed', 25); + await setOptionData(page, 'autoSpeed', 0); + await injectSceneAndRun(page, COMPLEX_SCENE, SCENE_NAME, SCENE_URL); + await flushBrowserTasks(page, 50); +} + +async function driveManualToCheckpoint(page: Page): Promise { + await prepareComplexScene(page); + + for (let i = 0; i < 120; i++) { + if (await isShownText(page, CHECKPOINT_TEXT)) break; + await clickStage(page); + await flushBrowserTasks(page, 80); + } + + await waitForShownText(page, CHECKPOINT_TEXT); + return captureStableCheckpoint(page); +} + +async function driveFastToCheckpoint(page: Page): Promise { + await prepareComplexScene(page); + await callSwitchFast(page); + await waitForShownTextAndStopMode(page, CHECKPOINT_TEXT, 'fast'); + return captureStableCheckpoint(page); +} + +async function driveAutoToCheckpoint(page: Page): Promise { + await prepareComplexScene(page); + await callSwitchAuto(page); + await waitForShownTextAndStopMode(page, CHECKPOINT_TEXT, 'auto', 45_000); + return captureStableCheckpoint(page); +} + +async function driveEditorSyncToCheckpoint(page: Page): Promise { + await callStopAll(page).catch(() => {}); + await resetRuntime(page); + await setOptionData(page, 'textSpeed', 25); + await callSyncWithOrigine(page, SCENE_NAME, CHECKPOINT_SENTENCE_ID, false); + await waitForShownText(page, CHECKPOINT_TEXT); + return captureStableCheckpoint(page); +} + +async function captureStableCheckpoint(page: Page): Promise { + await settleCurrentPresentation(page); + expect(await getCurrentSentenceId(page)).toBe(CHECKPOINT_SENTENCE_ID); + + const snapshot = await takeSnapshot(page); + const stable = toStableRuntimeSnapshot(snapshot); + + expect(stable.text.shownText).toContain(CHECKPOINT_TEXT); + expect(stable.text.pendingElements).toBe(0); + expect(stable.pixi?.activeAnimations).toEqual([]); + expect(stable.pixi?.lockedTargets).toEqual([]); + expect(stable.performs).toEqual([]); + + await expectTargetTransform(page, 'hero', 220, -60, 0.95); + await expectTargetTransform(page, 'rival', -260, 20, 0.8); + await expectTargetTransform(page, 'ally', 0, 80, 1); + await expectTargetTransform(page, 'bg-main', 10, -20, 0.9); + + return stable; +} + +async function expectTargetTransform( + page: Page, + target: string, + expectedX: number, + expectedY: number, + expectedAlpha: number, +): Promise { + await page.waitForFunction((targetKey) => window.webgalTest!.testTools.getStageObjectByKey(targetKey) != null, target, { + timeout: 10_000, + }); + + const effect = await getStageEffect(page, target); + const transform = effect?.transform as { position?: { x: number; y: number }; alpha?: number } | undefined; + expect(transform?.position?.x).toBe(expectedX); + expect(transform?.position?.y).toBe(expectedY); + expect(transform?.alpha).toBeCloseTo(expectedAlpha, 2); + + const object = await getStageObjectByKey(page, target); + expect(object?.transform?.x).toBeCloseTo(expectedX, 0); + expect(object?.transform?.y).toBeCloseTo(expectedY, 0); + expect(object?.transform?.alpha).toBeCloseTo(expectedAlpha, 2); +} + +async function advancePastCheckpoint(page: Page): Promise { + for (let i = 0; i < 20; i++) { + if (await isShownText(page, AFTER_CHECKPOINT_TEXT)) break; + await clickStage(page); + await flushBrowserTasks(page, 80); + } + await waitForShownText(page, AFTER_CHECKPOINT_TEXT); + await settleCurrentPresentation(page); +} + +function expectStableMatch(expected: StableRuntimeSnapshot, actual: StableRuntimeSnapshot): void { + const result = compareStableRuntimeSnapshots(expected, actual); + expect(result.diffs).toEqual([]); + expect(result.match).toBe(true); +} + +describe('Complex State Consistency', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + await routeScene(page, SCENE_NAME, COMPLEX_SCENE); + }); + + afterAll(async () => { + if (page) { + await callStopAll(page).catch(() => {}); + await page.context().close(); + } + await closeBrowser(); + }); + + it('should converge to the same checkpoint through manual, fast, auto, editor sync, and stageData load', async () => { + const manual = await driveManualToCheckpoint(page); + + const fast = await driveFastToCheckpoint(page); + expectStableMatch(manual, fast); + + const auto = await driveAutoToCheckpoint(page); + expectStableMatch(manual, auto); + + const editorSync = await driveEditorSyncToCheckpoint(page); + expectStableMatch(manual, editorSync); + + await driveManualToCheckpoint(page); + const stageData = await callGenerateCurrentStageData(page, 88, false); + await resetRuntime(page); + await callLoadGameFromStageData(page, stageData); + await waitForShownText(page, CHECKPOINT_TEXT); + const loadedFromStageData = await captureStableCheckpoint(page); + expectStableMatch(manual, loadedFromStageData); + }); + + it('should restore the same checkpoint after save/load and backlog jump from a later complex state', async () => { + const checkpoint = await driveManualToCheckpoint(page); + const checkpointBacklogIndex = (await getBacklog(page)).length - 1; + + await callSaveGame(page, 89); + await flushBrowserTasks(page, 300); + + await advancePastCheckpoint(page); + expect(await isShownText(page, AFTER_CHECKPOINT_TEXT)).toBe(true); + + await callLoadGame(page, 89); + await waitForShownText(page, CHECKPOINT_TEXT); + const loaded = await captureStableCheckpoint(page); + expectStableMatch(checkpoint, loaded); + + await advancePastCheckpoint(page); + await callJumpFromBacklog(page, checkpointBacklogIndex, false); + await waitForShownText(page, CHECKPOINT_TEXT); + const restoredFromBacklog = await captureStableCheckpoint(page); + expectStableMatch(checkpoint, restoredFromBacklog); + }); +}); diff --git a/packages/webgal-test/src/tests/fast-mode.test.ts b/packages/webgal-test/src/tests/fast-mode.test.ts new file mode 100644 index 000000000..eb53a46b3 --- /dev/null +++ b/packages/webgal-test/src/tests/fast-mode.test.ts @@ -0,0 +1,85 @@ +/** + * 快进模式(Fast Skip)测试 + * + * 验证: + * 1. 开启快进后游戏快速推进(比 auto 更快) + * 2. 快进推进的步数明显多于同时长 auto + * 3. 停止快进后游戏停止推进 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + createTestPage, + startGameAndWait, + closeBrowser, + callSwitchFast, + callStopFast, + callStopAll, + injectSceneAndRun, + getCurrentSentenceId, + getIsFast, + delay, + generateTestScene, +} from '../utils'; + +describe('Fast Mode Test', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + await startGameAndWait(page); + // 注入含 200 条语句的测试场景,fast 模式推进很快 + await injectSceneAndRun(page, generateTestScene(200, 'fast'), 'fast-test'); + await delay(500); + }); + + afterAll(async () => { + if (page) { + await callStopAll(page).catch(() => {}); + await page.context().close(); + } + await closeBrowser(); + }); + + it('should activate fast mode and rapidly advance', async () => { + const initialId = await getCurrentSentenceId(page); + + await callSwitchFast(page); + expect(await getIsFast(page)).toBe(true); + + // Fast 模式每 50ms 推进一次,3 秒应该能推进很多 + await delay(3000); + + await callStopFast(page); + expect(await getIsFast(page)).toBe(false); + + const afterId = await getCurrentSentenceId(page); + + // 快进应推进很多句(至少 5 句) + expect(afterId - initialId).toBeGreaterThanOrEqual(5); + }); + + it('should advance faster than auto mode', async () => { + // 用 Fast 模式测试 3 秒 + const fastStartId = await getCurrentSentenceId(page); + await callSwitchFast(page); + await delay(3000); + await callStopFast(page); + const fastAdvance = (await getCurrentSentenceId(page)) - fastStartId; + + // Fast 至少推进了很多步(远超 auto 的 250-1750ms 间隔) + expect(fastAdvance).toBeGreaterThanOrEqual(5); + }); + + it('should stop advancing after stopping fast mode', async () => { + await callSwitchFast(page); + await delay(2000); + await callStopFast(page); + + const idAfterStop = await getCurrentSentenceId(page); + await delay(2000); + const idLater = await getCurrentSentenceId(page); + + expect(idLater).toBe(idAfterStop); + }); +}); diff --git a/packages/webgal-test/src/tests/perform-manager.test.ts b/packages/webgal-test/src/tests/perform-manager.test.ts new file mode 100644 index 000000000..8e20181bd --- /dev/null +++ b/packages/webgal-test/src/tests/perform-manager.test.ts @@ -0,0 +1,99 @@ +/** + * 演出管理器测试 + * + * 验证 performController 相关功能: + * 1. 读取当前 perform 列表 + * 2. 注入带有演出的场景后验证 perform 状态 + * 3. removeAllPerform 清空演出列表 + * 4. perform 的阻塞属性正确反映 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + createTestPage, + startGameAndWait, + closeBrowser, + callNextSentence, + callStopAll, + injectSceneAndRun, + getPerformList, + removeAllPerforms, + delay, +} from '../utils'; + +describe('Perform Manager Test', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + await startGameAndWait(page); + }); + + afterAll(async () => { + if (page) { + await callStopAll(page).catch(() => {}); + await page.context().close(); + } + await closeBrowser(); + }); + + it('should read perform list', async () => { + const performs = await getPerformList(page); + expect(Array.isArray(performs)).toBe(true); + }); + + it('should have performs after executing dialogue', async () => { + // 注入对话场景 + const script = [ + '这是一句很长的对话,会触发文字演出;', + '第二句对话;', + ].join('\n'); + + await injectSceneAndRun(page, script, 'perform-dialogue'); + // 不要等太久,演出可能还在进行中 + await delay(200); + + // 文字演出应该在列表中 + const performs = await getPerformList(page); + // 执行后可能有演出(取决于引擎的演出调度时机) + // 我们主要验证结构 + for (const p of performs) { + expect(typeof p.performName).toBe('string'); + expect(typeof p.duration).toBe('number'); + expect(typeof p.isHoldOn).toBe('boolean'); + expect(typeof p.blockingNext).toBe('boolean'); + expect(typeof p.blockingAuto).toBe('boolean'); + } + }); + + it('should clear all performs with removeAllPerform', async () => { + // 注入场景并让演出开始 + const script = '这是一句用于测试清除的对话;'; + await injectSceneAndRun(page, script, 'perform-clear'); + await delay(200); + + // 清除所有演出 + await removeAllPerforms(page); + await delay(100); + + const performs = await getPerformList(page); + expect(performs.length).toBe(0); + }); + + it('should report correct blocking state for performs', async () => { + const script = [ + '这是一句有阻塞性的对话;', + ].join('\n'); + + await injectSceneAndRun(page, script, 'perform-blocking'); + await delay(200); + + const performs = await getPerformList(page); + // 对于每个 perform,blockingNext 和 blockingAuto 应该是布尔值 + for (const p of performs) { + expect(typeof p.blockingNext).toBe('boolean'); + expect(typeof p.blockingAuto).toBe('boolean'); + expect(typeof p.goNextWhenOver).toBe('boolean'); + } + }); +}); diff --git a/packages/webgal-test/src/tests/pixi-stage.test.ts b/packages/webgal-test/src/tests/pixi-stage.test.ts new file mode 100644 index 000000000..84b337daf --- /dev/null +++ b/packages/webgal-test/src/tests/pixi-stage.test.ts @@ -0,0 +1,121 @@ +/** + * Pixi 舞台状态测试 + * + * 验证通过 pixiStage API 可以读取舞台可视化状态: + * 1. 获取立绘列表及其 transform + * 2. 获取背景列表 + * 3. 注入包含 changeBg / changeFigure 的场景后验证舞台对象 + * 4. 验证 snapshot 中 pixiState 与直接查询一致 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + createTestPage, + startGameAndWait, + closeBrowser, + callNextSentence, + callStopAll, + injectSceneAndRun, + takeSnapshot, + getFigureObjects, + getBackgroundObjects, + delay, +} from '../utils'; + +describe('Pixi Stage State Test', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + await startGameAndWait(page); + }); + + afterAll(async () => { + if (page) { + await callStopAll(page).catch(() => {}); + await page.context().close(); + } + await closeBrowser(); + }); + + it('should be able to read figure objects list', async () => { + const figures = await getFigureObjects(page); + // figures 可以为空(还没有立绘),但应该是数组 + expect(Array.isArray(figures)).toBe(true); + }); + + it('should be able to read background objects list', async () => { + const bgs = await getBackgroundObjects(page); + expect(Array.isArray(bgs)).toBe(true); + }); + + it('should reflect pixi state in snapshot', async () => { + const snapshot = await takeSnapshot(page); + + // pixiState 可能为 null(如果 pixiStage 未初始化),但通常已初始化 + if (snapshot.pixiState) { + expect(Array.isArray(snapshot.pixiState.figureObjects)).toBe(true); + expect(Array.isArray(snapshot.pixiState.backgroundObjects)).toBe(true); + expect(typeof snapshot.pixiState.stageWidth).toBe('number'); + expect(typeof snapshot.pixiState.stageHeight).toBe('number'); + } + }); + + it('should list figure objects with valid structure after changeFigure', async () => { + // 注入含有 changeFigure 的场景 + // 注意:实际资源可能不存在,但 pixi 应该创建 stageObject 条目 + const script = [ + 'changeFigure:test-figure.png -id=testFig;', + '等待一下;', + ].join('\n'); + + await injectSceneAndRun(page, script, 'figure-test'); + await delay(1500); + + const figures = await getFigureObjects(page); + // 由于资源可能不存在,我们主要验证结构是否正确 + // 如果有 figure 被创建,验证其属性结构 + for (const fig of figures) { + expect(fig).toHaveProperty('uuid'); + expect(fig).toHaveProperty('key'); + expect(fig).toHaveProperty('sourceUrl'); + expect(fig).toHaveProperty('sourceType'); + expect(fig).toHaveProperty('isExiting'); + // transform 可以为 null(container 未创建) + if (fig.transform) { + expect(typeof fig.transform.x).toBe('number'); + expect(typeof fig.transform.y).toBe('number'); + expect(typeof fig.transform.scaleX).toBe('number'); + expect(typeof fig.transform.scaleY).toBe('number'); + expect(typeof fig.transform.alpha).toBe('number'); + expect(typeof fig.transform.visible).toBe('boolean'); + } + } + }); + + it('should report performs in snapshot', async () => { + const snapshot = await takeSnapshot(page); + expect(Array.isArray(snapshot.performs)).toBe(true); + expect(typeof snapshot.performListLength).toBe('number'); + expect(snapshot.performListLength).toBe(snapshot.performs.length); + + for (const perf of snapshot.performs) { + expect(perf).toHaveProperty('performName'); + expect(perf).toHaveProperty('duration'); + expect(perf).toHaveProperty('isHoldOn'); + expect(perf).toHaveProperty('blockingNext'); + expect(perf).toHaveProperty('blockingAuto'); + } + }); + + it('should report animations in snapshot', async () => { + const snapshot = await takeSnapshot(page); + expect(Array.isArray(snapshot.animations)).toBe(true); + + for (const anim of snapshot.animations) { + expect(anim).toHaveProperty('name'); + expect(anim).toHaveProperty('frameCount'); + expect(typeof anim.frameCount).toBe('number'); + } + }); +}); diff --git a/packages/webgal-test/src/tests/random-click.test.ts b/packages/webgal-test/src/tests/random-click.test.ts new file mode 100644 index 000000000..82f6aacdc --- /dev/null +++ b/packages/webgal-test/src/tests/random-click.test.ts @@ -0,0 +1,109 @@ +/** + * 随机点击测试 + * + * 模拟用户不规律地点击推进,验证: + * 1. 随机间隔 nextSentence 推进游戏不会崩溃 + * 2. 每次推进后状态有效(sentenceId 递增或场景切换) + * 3. backlog 正确记录历史 + * 4. 极速连续点击不会损坏状态 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + createTestPage, + startGameAndWait, + closeBrowser, + callNextSentence, + callStopAll, + injectSceneAndRun, + takeSnapshot, + getCurrentSentenceId, + getBacklogLength, + delay, + generateTestScene, + type GameStateSnapshot, +} from '../utils'; + +function randomDelay(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +describe('Random Click Test', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + await startGameAndWait(page); + // 注入含 200 条语句的测试场景,确保有足够内容被点击 + await injectSceneAndRun(page, generateTestScene(200, '随机'), 'random-test'); + await delay(500); + }); + + afterAll(async () => { + if (page) { + await callStopAll(page).catch(() => {}); + await page.context().close(); + } + await closeBrowser(); + }); + + it('should handle random-interval clicks without crashing', async () => { + const CLICK_COUNT = 20; + const snapshots: GameStateSnapshot[] = []; + + for (let i = 0; i < CLICK_COUNT; i++) { + await callNextSentence(page); + const waitMs = randomDelay(100, 800); + await delay(waitMs); + + if (i % 5 === 4) { + snapshots.push(await takeSnapshot(page)); + } + } + + const apiAvailable = await page.evaluate(() => window.webgalTest != null); + expect(apiAvailable).toBe(true); + expect(snapshots.length).toBeGreaterThanOrEqual(1); + }); + + it('should advance sentence ID through random clicks', async () => { + const startId = await getCurrentSentenceId(page); + + for (let i = 0; i < 10; i++) { + await callNextSentence(page); + await delay(randomDelay(50, 500)); + } + + const endId = await getCurrentSentenceId(page); + expect(endId).toBeGreaterThan(startId); + }); + + it('should build backlog through random clicks', async () => { + const startBacklog = await getBacklogLength(page); + + for (let i = 0; i < 15; i++) { + await callNextSentence(page); + await delay(randomDelay(100, 600)); + } + + const endBacklog = await getBacklogLength(page); + expect(endBacklog).toBeGreaterThanOrEqual(startBacklog); + }); + + it('should handle rapid-fire clicks without corruption', async () => { + const before = await takeSnapshot(page); + + for (let i = 0; i < 30; i++) { + await callNextSentence(page); + if (Math.random() > 0.5) { + await delay(randomDelay(10, 50)); + } + } + + await delay(1000); + + const after = await takeSnapshot(page); + expect(after.sceneState.currentSentenceId).toBeGreaterThanOrEqual(0); + expect(after.backlogLength).toBeGreaterThanOrEqual(before.backlogLength); + }); +}); diff --git a/packages/webgal-test/src/tests/save-load.test.ts b/packages/webgal-test/src/tests/save-load.test.ts new file mode 100644 index 000000000..c00c074a4 --- /dev/null +++ b/packages/webgal-test/src/tests/save-load.test.ts @@ -0,0 +1,153 @@ +/** + * 存档/读档一致性测试 + * + * 验证: + * 1. 任意步骤保存后读取,核心状态完全一致 + * 2. 从读档点继续推进与不读档继续推进后再读档,最终到达同一步骤时状态一致 + * 3. 多个存档槽位互不干扰 + * 4. 存档数据包含正确的 backlog 和 scene 信息 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + createTestPage, + startGameAndWait, + closeBrowser, + callNextSentence, + callStopAll, + callSaveGame, + callLoadGame, + callGenerateCurrentStageData, + injectSceneAndRun, + takeSnapshot, + getCurrentSentenceId, + getBacklogLength, + delay, + compareSnapshots, + generateTestScene, +} from '../utils'; + +/** 推进 N 步(每步双击模式:第一次完成动画,第二次推进) */ +async function advanceSteps(page: Page, steps: number): Promise { + for (let i = 0; i < steps; i++) { + await callNextSentence(page); + await delay(200); + await callNextSentence(page); + await delay(300); + } +} + +describe('Save/Load Consistency Test', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + await startGameAndWait(page); + await injectSceneAndRun(page, generateTestScene(100, '存档'), 'save-test'); + await delay(500); + }); + + afterAll(async () => { + if (page) { + await callStopAll(page).catch(() => {}); + await page.context().close(); + } + await closeBrowser(); + }); + + it('should save and load with identical core state', async () => { + await advanceSteps(page, 5); + + const snapshotBeforeSave = await takeSnapshot(page); + await callSaveGame(page, 90); + await delay(500); + + await advanceSteps(page, 5); + + const snapshotAfterAdvance = await takeSnapshot(page); + expect(snapshotAfterAdvance.sceneState.currentSentenceId).not.toBe( + snapshotBeforeSave.sceneState.currentSentenceId, + ); + + await callLoadGame(page, 90); + await delay(2000); + + const snapshotAfterLoad = await takeSnapshot(page); + const { match, diffs } = compareSnapshots(snapshotBeforeSave, snapshotAfterLoad); + expect(diffs).toEqual([]); + expect(match).toBe(true); + }); + + it('should produce consistent state when replaying from a save point', async () => { + await advanceSteps(page, 3); + + await callSaveGame(page, 91); + await delay(500); + const savedSnapshot = await takeSnapshot(page); + + const STEPS = 4; + await advanceSteps(page, STEPS); + const targetSnapshot = await takeSnapshot(page); + + await callLoadGame(page, 91); + await delay(2000); + + await advanceSteps(page, STEPS); + const replaySnapshot = await takeSnapshot(page); + + expect(replaySnapshot.sceneState.currentSentenceId).toBe( + targetSnapshot.sceneState.currentSentenceId, + ); + expect(replaySnapshot.sceneState.sceneName).toBe(targetSnapshot.sceneState.sceneName); + }); + + it('should support multiple save slots independently', async () => { + await advanceSteps(page, 2); + const snapshotA = await takeSnapshot(page); + await callSaveGame(page, 92); + await delay(500); + + await advanceSteps(page, 4); + const snapshotB = await takeSnapshot(page); + await callSaveGame(page, 93); + await delay(500); + + await callLoadGame(page, 92); + await delay(2000); + const loadedA = await takeSnapshot(page); + const resultA = compareSnapshots(snapshotA, loadedA); + expect(resultA.diffs).toEqual([]); + + await callLoadGame(page, 93); + await delay(2000); + const loadedB = await takeSnapshot(page); + const resultB = compareSnapshots(snapshotB, loadedB); + expect(resultB.diffs).toEqual([]); + }); + + it('should preserve backlog and scene data in save', async () => { + await advanceSteps(page, 5); + + const backlogBefore = await getBacklogLength(page); + const sentenceIdBefore = await getCurrentSentenceId(page); + + await callSaveGame(page, 94); + await delay(500); + + const saveData = (await callGenerateCurrentStageData(page, 94)) as Record; + expect(saveData).toHaveProperty('nowStageState'); + expect(saveData).toHaveProperty('backlog'); + expect(saveData).toHaveProperty('sceneData'); + + await advanceSteps(page, 3); + + await callLoadGame(page, 94); + await delay(2000); + + const backlogAfter = await getBacklogLength(page); + const sentenceIdAfter = await getCurrentSentenceId(page); + + expect(backlogAfter).toBe(backlogBefore); + expect(sentenceIdAfter).toBe(sentenceIdBefore); + }); +}); diff --git a/packages/webgal-test/src/tests/scene-injection.test.ts b/packages/webgal-test/src/tests/scene-injection.test.ts new file mode 100644 index 000000000..1188003cb --- /dev/null +++ b/packages/webgal-test/src/tests/scene-injection.test.ts @@ -0,0 +1,155 @@ +/** + * 场景注入测试 + * + * 验证通过 sceneTools API 直接注入 WebGAL 脚本并执行: + * 1. 注入原始脚本文本后,场景数据正确更新 + * 2. 注入并执行后,第一条语句被执行 + * 3. 注入后可以通过 nextSentence 依次推进 + * 4. 注入包含多种指令的复杂脚本 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + createTestPage, + startGameAndWait, + closeBrowser, + callNextSentence, + callStopAll, + injectSceneAndRun, + takeSnapshot, + getCurrentSentenceId, + getCurrentSceneName, + getSentenceCount, + getBacklogLength, + delay, +} from '../utils'; + +describe('Scene Injection Test', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + await startGameAndWait(page); + }); + + afterAll(async () => { + if (page) { + await callStopAll(page).catch(() => {}); + await page.context().close(); + } + await closeBrowser(); + }); + + it('should inject a simple scene and start from sentence 0', async () => { + const script = [ + '你好,这是注入的测试场景;', + '第二句话;', + '第三句话;', + ].join('\n'); + + await injectSceneAndRun(page, script, 'injected-test'); + await delay(500); + + const sceneName = await getCurrentSceneName(page); + expect(sceneName).toBe('injected-test'); + + // injectSceneAndRun 设置 sentenceId=0 后立即执行 scriptExecutor, + // 所以第一条语句已被执行,sentenceId 推进到 1 + const sentenceId = await getCurrentSentenceId(page); + expect(sentenceId).toBeGreaterThanOrEqual(0); + + const sentenceCount = await getSentenceCount(page); + expect(sentenceCount).toBeGreaterThanOrEqual(3); + }); + + it('should advance through injected scene with nextSentence', async () => { + const script = [ + '第一句;', + '第二句;', + '第三句;', + '第四句;', + ].join('\n'); + + await injectSceneAndRun(page, script, 'advance-test'); + await delay(500); + + const id0 = await getCurrentSentenceId(page); + + // 第一次 nextSentence 可能只是完成当前文字动画,第二次才推进 + await callNextSentence(page); + await delay(300); + await callNextSentence(page); + await delay(300); + const id1 = await getCurrentSentenceId(page); + expect(id1).toBeGreaterThan(id0); + + await callNextSentence(page); + await delay(300); + await callNextSentence(page); + await delay(300); + const id2 = await getCurrentSentenceId(page); + expect(id2).toBeGreaterThan(id1); + }); + + it('should inject a scene with changeBg command', async () => { + const script = [ + 'changeBg:none;', + '这是一个有背景切换的场景;', + '继续对话;', + ].join('\n'); + + await injectSceneAndRun(page, script, 'bg-test'); + await delay(1000); + + // 场景应该被正确注入 + const sceneName = await getCurrentSceneName(page); + expect(sceneName).toBe('bg-test'); + + // API 仍然可用 + const apiAvailable = await page.evaluate(() => window.webgalTest != null); + expect(apiAvailable).toBe(true); + }); + + it('should track backlog after injecting a new scene', async () => { + const script = [ + '注入场景的第一句话;', + '注入场景的第二句话;', + '注入场景的第三句话;', + ].join('\n'); + + await injectSceneAndRun(page, script, 'backlog-inject-test'); + await delay(500); + + const backlogBefore = await getBacklogLength(page); + + // 推进几步,应该积累 backlog + for (let i = 0; i < 3; i++) { + await callNextSentence(page); + await delay(500); + } + + const backlogAfter = await getBacklogLength(page); + expect(backlogAfter).toBeGreaterThanOrEqual(backlogBefore); + }); + + it('should produce valid snapshot after scene injection', async () => { + const script = [ + '快照测试第一句;', + '快照测试第二句;', + ].join('\n'); + + await injectSceneAndRun(page, script, 'snapshot-inject-test'); + await delay(500); + + const snapshot = await takeSnapshot(page); + + expect(snapshot.sceneState.sceneName).toBe('snapshot-inject-test'); + // sentenceId ≥ 0 即可(injectSceneAndRun 会执行第一句) + expect(snapshot.sceneState.currentSentenceId).toBeGreaterThanOrEqual(0); + expect(snapshot.sceneState.sentenceCount).toBeGreaterThanOrEqual(2); + expect(snapshot.timestamp).toBeGreaterThan(0); + expect(snapshot.gameplayState).toBeDefined(); + expect(snapshot.gameplayState.isAuto).toBe(false); + expect(snapshot.gameplayState.isFast).toBe(false); + }); +}); diff --git a/packages/webgal-test/src/tests/test-mode-exposure.test.ts b/packages/webgal-test/src/tests/test-mode-exposure.test.ts new file mode 100644 index 000000000..db76ffefe --- /dev/null +++ b/packages/webgal-test/src/tests/test-mode-exposure.test.ts @@ -0,0 +1,55 @@ +/** + * 测试模式暴露与构建产物校验 + * + * 这些断言验证当前浏览器实际加载的是 build:test 产物,而不是陈旧的普通生产 dist。 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import type { Page } from 'playwright'; +import { + closeBrowser, + createTestPage, + getActiveAnimations, + getLockedTargets, + getTestMetadata, + getTextState, + resetRuntime, + takeSnapshot, +} from '../utils'; + +describe('Test Mode Exposure', () => { + let page: Page; + + beforeAll(async () => { + page = await createTestPage(); + }); + + afterAll(async () => { + if (page) { + await page.context().close(); + } + await closeBrowser(); + }); + + it('should expose the test kernel from the built bundle', async () => { + const metadata = await getTestMetadata(page); + + expect(metadata.testMode).toBe(true); + expect(metadata.apiVersion).toBeGreaterThanOrEqual(2); + expect(metadata.locationHref).toContain('localhost'); + }); + + it('should expose runtime observability helpers', async () => { + await resetRuntime(page); + + const snapshot = await takeSnapshot(page); + const textState = await getTextState(page); + const activeAnimations = await getActiveAnimations(page); + const lockedTargets = await getLockedTargets(page); + + expect(snapshot.metadata.testMode).toBe(true); + expect(snapshot.pixiState).not.toBeNull(); + expect(snapshot.textState.pendingElements).toBe(textState.pendingElements); + expect(activeAnimations).toEqual([]); + expect(lockedTargets).toEqual([]); + }); +}); diff --git a/packages/webgal-test/src/types/global.d.ts b/packages/webgal-test/src/types/global.d.ts new file mode 100644 index 000000000..d4d0866bf --- /dev/null +++ b/packages/webgal-test/src/types/global.d.ts @@ -0,0 +1,394 @@ +/** + * window.webgalTest 的类型声明(vitest 端) + * + * 通过 page.evaluate 访问时,只能传递可序列化数据。 + * 但在 evaluate 回调内部可以直接调用这些方法。 + */ + +// ─── Pixi 相关 ─── + +interface PixiContainer { + x: number; + y: number; + scale: { x: number; y: number }; + rotation: number; + alpha: number; + visible: boolean; + zIndex: number; + width: number; + height: number; + children: unknown[]; +} + +interface StageObject { + uuid: string; + key: string; + pixiContainer: PixiContainer | null; + sourceUrl: string; + sourceExt: string; + sourceType: 'img' | 'live2d' | 'spine' | 'gif' | 'video' | 'stage'; + spineAnimation?: string; + isExiting?: boolean; +} + +interface PixiStageInstance { + currentApp: { + stage: unknown; + renderer: { view: HTMLCanvasElement }; + ticker: unknown; + view: HTMLCanvasElement; + } | null; + mainStageContainer: PixiContainer; + foregroundEffectsContainer: PixiContainer; + backgroundEffectsContainer: PixiContainer; + figureContainer: PixiContainer; + backgroundContainer: PixiContainer; + figureObjects: StageObject[]; + backgroundObjects: StageObject[]; + mainStageObject: StageObject; + stageWidth: number; + stageHeight: number; + frameDuration: number; + addBg(key: string, url: string): void; + addFigure(key: string, url: string, presetPosition?: string): void; + addLive2dFigure(key: string, jsonPath: string, pos: string): void; + addSpineFigure(key: string, url: string, presetPosition: string): void; + registerAnimation(animationObject: unknown, key: string, target?: string): void; + removeAnimation(key: string): void; + removeAllAnimations(): void; + getAllStageObj(): StageObject[]; + getFigureObjects(): StageObject[]; + getStageObjByKey(key: string): StageObject | undefined; + removeStageObjectByKey(key: string): void; +} + +// ─── 演出管理器 ─── + +interface Perform { + performName: string; + duration: number; + isHoldOn: boolean; + stopFunction: () => void; + blockingNext: () => boolean; + blockingAuto: () => boolean; + goNextWhenOver?: boolean; + skipNextCollect?: boolean; +} + +interface PerformControllerInstance { + performList: Perform[]; + arrangeNewPerform(perform: unknown, script: unknown): void; + unmountPerform(name: string, force?: boolean): void; + removeAllPerform(): void; +} + +// ─── 场景管理器 ─── + +interface SceneData { + currentSentenceId: number; + currentScene: { + sceneName: string; + sceneUrl: string; + sentenceList: unknown[]; + assetsList: unknown[]; + subSceneList: string[]; + }; + sceneStack: Array<{ sceneName: string; sceneUrl: string; continueLine: number }>; +} + +interface SceneManagerInstance { + sceneData: SceneData; + settledScenes: string[]; + settledAssets: string[]; + lockSceneWrite: boolean; + resetScene(): void; +} + +// ─── Backlog 管理器 ─── + +interface BacklogManagerInstance { + isSaveBacklogNext: boolean; + getBacklog(): unknown[]; + makeBacklogEmpty(): void; + insertBacklogItem(item: unknown): void; + saveCurrentStateToBacklog(): void; + editLastBacklogItemEffect(effects: unknown[]): void; +} + +// ─── 动画管理器 ─── + +interface AnimationManagerInstance { + addAnimation(animation: unknown): void; + getAnimations(): Array<{ name: string; effects: unknown[] }>; +} + +// ─── 事件系统 ─── + +interface WebgalEvent { + on(callback: (message?: unknown) => void, id?: string): void; + off(callback: (message?: unknown) => void, id?: string): void; + emit(message?: unknown, id?: string): void; +} + +interface EventsInstance { + textSettle: WebgalEvent; + userInteractNext: WebgalEvent; + fullscreenDbClick: WebgalEvent; + styleUpdate: WebgalEvent; + afterStyleUpdate: WebgalEvent; +} + +// ─── Gameplay ─── + +interface GameplayInstance { + isAuto: boolean; + isFast: boolean; + pixiStage: PixiStageInstance | null; + performController: PerformControllerInstance; + autoInterval: unknown; + fastInterval: unknown; + autoTimeout: unknown; + resetGamePlay(): void; +} + +// ─── Live2D ─── + +interface Live2DInstance { + isAvailable: boolean; + Live2DModel: unknown; + legacyExpressionBlendMode: boolean; +} + +// ─── Redux Store ─── + +interface WebGALStore { + getState(): { + stage: Record; + GUI: Record; + userData: Record; + saveData: Record; + }; + dispatch(action: unknown): unknown; + subscribe(listener: () => void): () => void; +} + +// ─── 核心实例 ─── + +interface WebGALCoreInstance { + sceneManager: SceneManagerInstance; + backlogManager: BacklogManagerInstance; + animationManager: AnimationManagerInstance; + gameplay: GameplayInstance; + gameName: string; + gameKey: string; + events: EventsInstance; + steam: unknown; + template: unknown; + styleObjects: Map; +} + +// ─── 完整 API ─── + +interface WebGALTestAPI { + readonly metadata: { + testMode: true; + apiVersion: number; + exposedAt: number; + locationHref: string; + }; + + // 核心实例 + core: WebGALCoreInstance; + live2d: Live2DInstance; + store: WebGALStore; + + // Pixi 舞台(直接引用) + readonly pixiStage: PixiStageInstance | null; + readonly pixiApp: { + stage: unknown; + renderer: { view: HTMLCanvasElement }; + ticker: unknown; + view: HTMLCanvasElement; + } | null; + + // 子模块快捷访问 + readonly sceneManager: SceneManagerInstance; + readonly backlogManager: BacklogManagerInstance; + readonly animationManager: AnimationManagerInstance; + readonly performController: PerformControllerInstance; + readonly gameplay: GameplayInstance; + readonly events: EventsInstance; + + // 控制器 + controllers: { + nextSentence(): void; + scriptExecutor(): void; + switchAuto(): void; + stopAuto(): void; + switchFast(): void; + stopFast(): void; + stopAll(): void; + startGame(): void; + continueGame(): Promise; + + saveGame(index: number): void; + loadGame(index: number): void; + loadGameFromStageData(stageData: unknown): void; + jumpFromBacklog(index: number, refetchScene?: boolean): void; + generateCurrentStageData(index: number, isSavePreviewImage?: boolean): unknown; + + resetStage(resetBacklog?: boolean, resetSceneAndVar?: boolean): void; + playBgm(url: string, enter?: number, volume?: number): void; + + changeScene(sceneUrl: string, sceneName: string): void; + callScene(sceneUrl: string, sceneName: string): void; + restoreScene(entry: unknown): void; + syncWithOrigine(sceneName: string, sentenceId: number, experimental?: boolean): void; + }; + + // 场景解析 & 注入 + sceneTools: { + sceneParser(rawScene: string, sceneName: string, sceneUrl: string): unknown; + sceneFetcher(sceneUrl: string): Promise; + webgalParser: unknown; + injectScene(rawSceneText: string, sceneName?: string, sceneUrl?: string): void; + injectSceneAndRun(rawSceneText: string, sceneName?: string, sceneUrl?: string): void; + injectParsedScene(sentenceList: unknown[], sceneName?: string): void; + }; + + // Redux dispatch helpers + dispatch: { + setStage(key: string, value: unknown): void; + resetStageState(state: unknown): void; + setVisibility(component: string, visibility: boolean): void; + raw(action: unknown): void; + }; + + // 配置 + config: { + SYSTEM_CONFIG: { backlog_size: number; fast_timeout: number }; + PERFORM_CONFIG: { textInitialDelay: number }; + }; + + // 状态快照 + takeSnapshot(): { + metadata: { + testMode: true; + apiVersion: number; + exposedAt: number; + locationHref: string; + }; + stageState: Record; + guiState: Record; + userData: Record; + sceneState: { + currentSentenceId: number; + sceneName: string; + sceneUrl: string; + sentenceCount: number; + sceneStackLength: number; + sceneStack: Array<{ sceneName: string; sceneUrl: string; continueLine: number }>; + }; + backlog: unknown[]; + backlogLength: number; + performs: Array<{ + performName: string; + duration: number; + isHoldOn: boolean; + blockingNext: boolean; + blockingAuto: boolean; + goNextWhenOver: boolean; + }>; + performListLength: number; + pixiState: { + figureObjects: Array<{ + uuid: string; + key: string; + sourceUrl: string; + sourceExt: string; + sourceType: string; + isExiting: boolean; + transform: { + x: number; + y: number; + scaleX: number; + scaleY: number; + rotation: number; + alpha: number; + visible: boolean; + zIndex: number; + } | null; + }>; + backgroundObjects: Array<{ + uuid: string; + key: string; + sourceUrl: string; + sourceExt: string; + sourceType: string; + isExiting: boolean; + transform: { + x: number; + y: number; + scaleX: number; + scaleY: number; + rotation: number; + alpha: number; + visible: boolean; + zIndex: number; + } | null; + }>; + allObjects: unknown[]; + activeAnimations: Array<{ key: string; targetKey: string; type: 'common' | 'preset' }>; + lockedTargets: string[]; + stageWidth: number; + stageHeight: number; + } | null; + textState: { + shownText: string; + shownName: string; + currentDialogKey: string; + totalElements: number; + pendingElements: number; + settledElements: number; + visibleText: string; + }; + gameplayState: { + isAuto: boolean; + isFast: boolean; + }; + animations: Array<{ name: string; frameCount: number }>; + timestamp: number; + }; + + testTools: { + resetRuntime(): void; + settleText(): void; + settleAnimations(): void; + getTextState(): { + shownText: string; + shownName: string; + currentDialogKey: string; + totalElements: number; + pendingElements: number; + settledElements: number; + visibleText: string; + }; + getActiveAnimations(): Array<{ key: string; targetKey: string; type: 'common' | 'preset' }>; + getLockedTargets(): string[]; + getStageObjectByKey(key: string): unknown; + getEffectByTarget(target: string): unknown; + setOptionData(key: string, value: unknown): void; + flushBrowserTasks(ms?: number): Promise; + }; + + // 工具 + utils: { + cloneDeep(value: T): T; + }; +} + +interface Window { + webgalTest?: WebGALTestAPI; + __PIXI_APP__?: unknown; + PIXIapp?: unknown; +} diff --git a/packages/webgal-test/src/utils/bridge.ts b/packages/webgal-test/src/utils/bridge.ts new file mode 100644 index 000000000..be633c791 --- /dev/null +++ b/packages/webgal-test/src/utils/bridge.ts @@ -0,0 +1,880 @@ +/** + * WebGAL 测试工具 - 浏览器桥接层 + * + * 在 playwright Page 上下文中执行 window.webgalTest 调用 + */ +import type { Page } from 'playwright'; + +// ═══════════════════════════════════════════════ +// 基础等待 +// ═══════════════════════════════════════════════ + +/** + * 等待 WebGAL 测试 API 可用 + */ +export async function waitForTestAPI(page: Page, timeout = 30_000): Promise { + await page.waitForFunction(() => window.webgalTest != null, { timeout }); +} + +/** + * 等待指定毫秒 + */ +export function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * 等待句子 ID 变化 + */ +export async function waitForSentenceAdvance( + page: Page, + currentId: number, + timeout = 10_000, + pollInterval = 100, +): Promise { + const deadline = Date.now() + timeout; + while (Date.now() < deadline) { + const newId = await getCurrentSentenceId(page); + if (newId !== currentId) return newId; + await delay(pollInterval); + } + throw new Error(`Sentence did not advance from ${currentId} within ${timeout}ms`); +} + +async function waitForPredicate( + read: () => Promise, + predicate: (value: T) => boolean, + description: string, + timeout = 10_000, + pollInterval = 50, +): Promise { + const deadline = Date.now() + timeout; + let lastValue: T | undefined; + while (Date.now() < deadline) { + lastValue = await read(); + if (predicate(lastValue)) return lastValue; + await delay(pollInterval); + } + throw new Error(`${description} within ${timeout}ms. Last value: ${JSON.stringify(lastValue)}`); +} + +// ═══════════════════════════════════════════════ +// 游戏流程控制 +// ═══════════════════════════════════════════════ + +export async function callNextSentence(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.controllers.nextSentence()); +} + +export async function callStartGame(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.controllers.startGame()); +} + +export async function callContinueGame(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.controllers.continueGame()); +} + +export async function callScriptExecutor(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.controllers.scriptExecutor()); +} + +export async function clickStage(page: Page): Promise { + await page.dispatchEvent('#FullScreenClick', 'click'); +} + +// ═══════════════════════════════════════════════ +// 自动/快进模式 +// ═══════════════════════════════════════════════ + +export async function callSwitchAuto(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.controllers.switchAuto()); +} + +export async function callStopAuto(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.controllers.stopAuto()); +} + +export async function callSwitchFast(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.controllers.switchFast()); +} + +export async function callStopFast(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.controllers.stopFast()); +} + +export async function callStopAll(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.controllers.stopAll()); +} + +// ═══════════════════════════════════════════════ +// 存档/读档 +// ═══════════════════════════════════════════════ + +export async function callSaveGame(page: Page, index: number): Promise { + await page.evaluate((i) => window.webgalTest!.controllers.saveGame(i), index); +} + +export async function callLoadGame(page: Page, index: number): Promise { + await page.evaluate((i) => window.webgalTest!.controllers.loadGame(i), index); +} + +export async function callJumpFromBacklog(page: Page, index: number, refetchScene = true): Promise { + await page.evaluate(([i, refetch]) => window.webgalTest!.controllers.jumpFromBacklog(i, refetch), [ + index, + refetchScene, + ] as const); +} + +export async function callGenerateCurrentStageData(page: Page, index: number, savePreviewImage = false): Promise { + return page.evaluate(([i, preview]) => { + const data = window.webgalTest!.controllers.generateCurrentStageData(i, preview); + return JSON.parse(JSON.stringify(data)); + }, [index, savePreviewImage] as const); +} + +export async function callLoadGameFromStageData(page: Page, stageData: unknown): Promise { + await page.evaluate((data) => window.webgalTest!.controllers.loadGameFromStageData(data), stageData); +} + +// ═══════════════════════════════════════════════ +// 舞台控制 +// ═══════════════════════════════════════════════ + +export async function callResetStage(page: Page, resetBacklog = true, resetSceneAndVar = true): Promise { + await page.evaluate( + ([rb, rs]) => window.webgalTest!.controllers.resetStage(rb, rs), + [resetBacklog, resetSceneAndVar] as const, + ); +} + +export async function callPlayBgm(page: Page, url: string, enter = 0, volume = 100): Promise { + await page.evaluate(([u, e, v]) => window.webgalTest!.controllers.playBgm(u, e, v), [url, enter, volume] as const); +} + +// ═══════════════════════════════════════════════ +// 场景管理 +// ═══════════════════════════════════════════════ + +export async function callChangeScene(page: Page, sceneUrl: string, sceneName: string): Promise { + await page.evaluate(([url, name]) => window.webgalTest!.controllers.changeScene(url, name), [sceneUrl, sceneName]); +} + +export async function callCallScene(page: Page, sceneUrl: string, sceneName: string): Promise { + await page.evaluate(([url, name]) => window.webgalTest!.controllers.callScene(url, name), [sceneUrl, sceneName]); +} + +export async function callSyncWithOrigine( + page: Page, + sceneName: string, + sentenceId: number, + experimental = false, +): Promise { + await page.evaluate( + ([name, id, exp]) => window.webgalTest!.controllers.syncWithOrigine(name, id, exp), + [sceneName, sentenceId, experimental] as const, + ); +} + +// ═══════════════════════════════════════════════ +// 场景注入(测试专用) +// ═══════════════════════════════════════════════ + +export async function routeScene(page: Page, sceneName: string, rawSceneText: string): Promise { + await page.route(`**/game/scene/${sceneName}`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/plain; charset=utf-8', + body: rawSceneText, + }); + }); +} + +/** + * 注入原始脚本文本为测试场景(仅设置,不执行) + */ +export async function injectScene( + page: Page, + rawSceneText: string, + sceneName = '__test__', + sceneUrl?: string, +): Promise { + await page.evaluate( + ([text, name, url]) => window.webgalTest!.sceneTools.injectScene(text, name, url), + [rawSceneText, sceneName, sceneUrl], + ); +} + +/** + * 注入原始脚本文本为测试场景并开始执行 + */ +export async function injectSceneAndRun( + page: Page, + rawSceneText: string, + sceneName = '__test__', + sceneUrl?: string, +): Promise { + await page.evaluate( + ([text, name, url]) => window.webgalTest!.sceneTools.injectSceneAndRun(text, name, url), + [rawSceneText, sceneName, sceneUrl], + ); +} + +export async function resetRuntime(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.testTools.resetRuntime()); +} + +export async function settleText(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.testTools.settleText()); +} + +export async function settleAnimations(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.testTools.settleAnimations()); +} + +export async function flushBrowserTasks(page: Page, ms = 0): Promise { + await page.evaluate((delayMs) => window.webgalTest!.testTools.flushBrowserTasks(delayMs), ms); +} + +// ═══════════════════════════════════════════════ +// 状态快照 +// ═══════════════════════════════════════════════ + +export async function takeSnapshot(page: Page): Promise { + return page.evaluate(() => { + const snap = window.webgalTest!.takeSnapshot(); + return JSON.parse(JSON.stringify(snap)); + }); +} + +export async function getTestMetadata(page: Page): Promise { + return page.evaluate(() => JSON.parse(JSON.stringify(window.webgalTest!.metadata))); +} + +export async function getTextState(page: Page): Promise { + return page.evaluate(() => JSON.parse(JSON.stringify(window.webgalTest!.testTools.getTextState()))); +} + +export async function waitForTextPending(page: Page, timeout = 10_000): Promise { + return waitForPredicate( + () => getTextState(page), + (state) => state.pendingElements > 0, + 'Text did not enter pending reveal state', + timeout, + ); +} + +export async function waitForTextSettled(page: Page, timeout = 10_000): Promise { + return waitForPredicate( + () => getTextState(page), + (state) => state.pendingElements === 0 && state.totalElements > 0, + 'Text did not settle', + timeout, + ); +} + +// ═══════════════════════════════════════════════ +// 场景状态读取 +// ═══════════════════════════════════════════════ + +export async function getCurrentSentenceId(page: Page): Promise { + return page.evaluate(() => window.webgalTest!.sceneManager.sceneData.currentSentenceId); +} + +export async function getCurrentSceneName(page: Page): Promise { + return page.evaluate(() => window.webgalTest!.sceneManager.sceneData.currentScene.sceneName); +} + +export async function getSceneStack(page: Page): Promise { + return page.evaluate(() => JSON.parse(JSON.stringify(window.webgalTest!.sceneManager.sceneData.sceneStack))); +} + +export async function getSentenceCount(page: Page): Promise { + return page.evaluate(() => window.webgalTest!.sceneManager.sceneData.currentScene.sentenceList.length); +} + +// ═══════════════════════════════════════════════ +// Backlog 读取 +// ═══════════════════════════════════════════════ + +export async function getBacklogLength(page: Page): Promise { + return page.evaluate(() => window.webgalTest!.backlogManager.getBacklog().length); +} + +export async function getBacklog(page: Page): Promise { + return page.evaluate(() => JSON.parse(JSON.stringify(window.webgalTest!.backlogManager.getBacklog()))); +} + +// ═══════════════════════════════════════════════ +// 游戏状态读取 +// ═══════════════════════════════════════════════ + +export async function getIsAuto(page: Page): Promise { + return page.evaluate(() => window.webgalTest!.gameplay.isAuto); +} + +export async function getIsFast(page: Page): Promise { + return page.evaluate(() => window.webgalTest!.gameplay.isFast); +} + +// ═══════════════════════════════════════════════ +// Redux 状态读取 +// ═══════════════════════════════════════════════ + +export async function getStageState(page: Page): Promise> { + return page.evaluate(() => JSON.parse(JSON.stringify(window.webgalTest!.store.getState().stage))); +} + +export async function getGuiState(page: Page): Promise> { + return page.evaluate(() => JSON.parse(JSON.stringify(window.webgalTest!.store.getState().GUI))); +} + +// ═══════════════════════════════════════════════ +// 演出管理器读取 +// ═══════════════════════════════════════════════ + +export async function getPerformList(page: Page): Promise { + return page.evaluate(() => + window.webgalTest!.performController.performList.map((p) => ({ + performName: p.performName, + duration: p.duration, + isHoldOn: p.isHoldOn, + blockingNext: p.blockingNext(), + blockingAuto: p.blockingAuto(), + goNextWhenOver: p.goNextWhenOver ?? false, + skipNextCollect: p.skipNextCollect ?? false, + })), + ); +} + +export async function waitForNoTransientPerforms(page: Page, timeout = 10_000): Promise { + return waitForPredicate( + () => getPerformList(page), + (performs) => performs.every((perform) => perform.isHoldOn || perform.skipNextCollect), + 'Transient performs did not settle', + timeout, + ); +} + +export async function waitForTransientPerform(page: Page, timeout = 10_000): Promise { + return waitForPredicate( + () => getPerformList(page), + (performs) => performs.some((perform) => !perform.isHoldOn && !perform.skipNextCollect), + 'Transient perform did not start', + timeout, + ); +} + +export async function removeAllPerforms(page: Page): Promise { + await page.evaluate(() => window.webgalTest!.performController.removeAllPerform()); +} + +// ═══════════════════════════════════════════════ +// Pixi 舞台读取 +// ═══════════════════════════════════════════════ + +export async function getFigureObjects(page: Page): Promise { + return page.evaluate(() => { + const ps = window.webgalTest!.pixiStage; + if (!ps) return []; + return ps.figureObjects.map((obj) => ({ + uuid: obj.uuid, + key: obj.key, + sourceUrl: obj.sourceUrl, + sourceExt: obj.sourceExt, + sourceType: obj.sourceType, + isExiting: obj.isExiting ?? false, + transform: obj.pixiContainer + ? { + x: obj.pixiContainer.x, + y: obj.pixiContainer.y, + scaleX: obj.pixiContainer.scale.x, + scaleY: obj.pixiContainer.scale.y, + rotation: obj.pixiContainer.rotation, + alpha: obj.pixiContainer.alpha, + visible: obj.pixiContainer.visible, + zIndex: obj.pixiContainer.zIndex, + } + : null, + })); + }); +} + +export async function getBackgroundObjects(page: Page): Promise { + return page.evaluate(() => { + const ps = window.webgalTest!.pixiStage; + if (!ps) return []; + return ps.backgroundObjects.map((obj) => ({ + uuid: obj.uuid, + key: obj.key, + sourceUrl: obj.sourceUrl, + sourceType: obj.sourceType, + isExiting: obj.isExiting ?? false, + })); + }); +} + +export async function getActiveAnimations(page: Page): Promise { + return page.evaluate(() => JSON.parse(JSON.stringify(window.webgalTest!.testTools.getActiveAnimations()))); +} + +export async function waitForActiveAnimationOnTarget( + page: Page, + targetKey: string, + timeout = 10_000, +): Promise { + return waitForPredicate( + () => getActiveAnimations(page), + (animations) => animations.some((animation) => animation.targetKey === targetKey), + `Animation on ${targetKey} did not start`, + timeout, + ); +} + +export async function waitForNoActiveAnimationOnTarget( + page: Page, + targetKey: string, + timeout = 10_000, +): Promise { + return waitForPredicate( + () => getActiveAnimations(page), + (animations) => animations.every((animation) => animation.targetKey !== targetKey), + `Animation on ${targetKey} did not stop`, + timeout, + ); +} + +export async function waitForNoActiveAnimations(page: Page, timeout = 10_000): Promise { + return waitForPredicate( + () => getActiveAnimations(page), + (animations) => animations.length === 0, + 'Active animations did not stop', + timeout, + ); +} + +export async function getLockedTargets(page: Page): Promise { + return page.evaluate(() => JSON.parse(JSON.stringify(window.webgalTest!.testTools.getLockedTargets()))); +} + +export async function getStageObjectByKey( + page: Page, + key: string, +): Promise { + return page.evaluate((targetKey) => { + const obj = window.webgalTest!.testTools.getStageObjectByKey(targetKey); + return obj ? JSON.parse(JSON.stringify(obj)) : null; + }, key); +} + +export async function getStageEffect(page: Page, target: string): Promise { + return page.evaluate((targetKey) => { + const effect = window.webgalTest!.testTools.getEffectByTarget(targetKey); + return effect ? JSON.parse(JSON.stringify(effect)) : null; + }, target); +} + +// ═══════════════════════════════════════════════ +// 配置读取 +// ═══════════════════════════════════════════════ + +export async function getSystemConfig(page: Page): Promise<{ backlog_size: number; fast_timeout: number }> { + return page.evaluate(() => JSON.parse(JSON.stringify(window.webgalTest!.config.SYSTEM_CONFIG))); +} + +// ═══════════════════════════════════════════════ +// Redux dispatch +// ═══════════════════════════════════════════════ + +export async function dispatchSetStage(page: Page, key: string, value: unknown): Promise { + await page.evaluate(({ k, v }) => window.webgalTest!.dispatch.setStage(k, v), { k: key, v: value }); +} + +export async function dispatchSetVisibility(page: Page, component: string, visibility: boolean): Promise { + await page.evaluate(({ c, v }) => window.webgalTest!.dispatch.setVisibility(c, v), { c: component, v: visibility }); +} + +export async function setOptionData(page: Page, key: string, value: unknown): Promise { + await page.evaluate(([optionKey, optionValue]) => window.webgalTest!.testTools.setOptionData(optionKey, optionValue), [ + key, + value, + ] as const); +} + +// ═══════════════════════════════════════════════ +// 类型定义 +// ═══════════════════════════════════════════════ + +export interface PerformSnapshot { + performName: string; + duration: number; + isHoldOn: boolean; + blockingNext: boolean; + blockingAuto: boolean; + goNextWhenOver: boolean; + skipNextCollect?: boolean; +} + +export interface FigureObjectSnapshot { + uuid: string; + key: string; + sourceUrl: string; + sourceExt: string; + sourceType: string; + isExiting: boolean; + transform: { + x: number; + y: number; + scaleX: number; + scaleY: number; + rotation: number; + alpha: number; + visible: boolean; + zIndex: number; + } | null; +} + +export interface BackgroundObjectSnapshot { + uuid: string; + key: string; + sourceUrl: string; + sourceExt: string; + sourceType: string; + isExiting: boolean; + transform: FigureObjectSnapshot['transform']; +} + +export interface RuntimeAnimationSnapshot { + key: string; + targetKey: string; + type: 'common' | 'preset'; +} + +export interface TextRuntimeSnapshot { + shownText: string; + shownName: string; + currentDialogKey: string; + totalElements: number; + pendingElements: number; + settledElements: number; + visibleText: string; +} + +export interface TestMetadata { + testMode: true; + apiVersion: number; + exposedAt: number; + locationHref: string; +} + +export interface StageEffectSnapshot { + target: string; + transform?: Record; +} + +export interface GameStateSnapshot { + metadata: TestMetadata; + stageState: Record; + guiState: Record; + userData: Record; + sceneState: { + currentSentenceId: number; + sceneName: string; + sceneUrl: string; + sentenceCount: number; + sceneStackLength: number; + sceneStack: Array<{ sceneName: string; sceneUrl: string; continueLine: number }>; + }; + backlog: unknown[]; + backlogLength: number; + performs: PerformSnapshot[]; + performListLength: number; + pixiState: { + figureObjects: FigureObjectSnapshot[]; + backgroundObjects: BackgroundObjectSnapshot[]; + allObjects: Array; + activeAnimations: RuntimeAnimationSnapshot[]; + lockedTargets: string[]; + stageWidth: number; + stageHeight: number; + } | null; + textState: TextRuntimeSnapshot; + gameplayState: { + isAuto: boolean; + isFast: boolean; + }; + animations: Array<{ name: string; frameCount: number }>; + timestamp: number; +} + +export interface StableRuntimeSnapshot { + scene: { + currentSentenceId: number; + sceneName: string; + sceneUrl: string; + sceneStackLength: number; + }; + stage: { + bgName: unknown; + figName: unknown; + figNameLeft: unknown; + figNameRight: unknown; + freeFigure: unknown; + figureAssociatedAnimation: unknown; + showText: unknown; + showTextSize: unknown; + showName: unknown; + command: unknown; + miniAvatar: unknown; + bgm: unknown; + GameVar: unknown; + effects: unknown; + bgTransform: unknown; + bgFilter: unknown; + enableFilm: unknown; + isDisableTextbox: unknown; + figureMetaData: unknown; + animationSettings: unknown; + }; + text: { + shownText: string; + shownName: string; + pendingElements: number; + totalElements: number; + }; + pixi: { + stageWidth: number; + stageHeight: number; + figureObjects: unknown; + backgroundObjects: unknown; + activeAnimations: RuntimeAnimationSnapshot[]; + lockedTargets: string[]; + } | null; + performs: PerformSnapshot[]; + backlogLength: number; + gameplayState: GameStateSnapshot['gameplayState']; +} + +/** + * 不稳定字段,在快照比较时需要排除 + */ +const UNSTABLE_STAGE_FIELDS = [ + 'PerformList', + 'currentDialogKey', + 'playVocal', + 'currentPerformRuntime', + 'live2dMotion', + 'live2dExpression', +]; + +function stableStringify(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map((item) => stableStringify(item)).join(',')}]`; + } + if (value && typeof value === 'object') { + const entries = Object.entries(value as Record).sort(([a], [b]) => a.localeCompare(b)); + return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(',')}}`; + } + return JSON.stringify(value); +} + +function roundNumber(value: number): number { + return Math.round(value * 1000) / 1000; +} + +function stableClone(value: unknown): unknown { + if (typeof value === 'number') return roundNumber(value); + if (Array.isArray(value)) return value.map((item) => stableClone(item)); + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value as Record) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, item]) => [key, stableClone(item)]), + ); + } + return value; +} + +function normalizeEffects(effects: unknown): unknown { + if (!Array.isArray(effects)) return []; + return effects + .map((effect) => stableClone(effect)) + .sort((a, b) => + String((a as Record).target ?? '').localeCompare( + String((b as Record).target ?? ''), + ), + ); +} + +function normalizeAnimationSettings(settings: unknown): unknown { + if (!Array.isArray(settings)) return []; + return settings + .map((setting) => { + const item = setting as Record; + return stableClone({ + target: item.target, + enterDuration: item.enterDuration, + exitDuration: item.exitDuration, + hasEnterAnimation: Boolean(item.enterAnimationName), + hasExitAnimation: Boolean(item.exitAnimationName), + }); + }) + .sort((a, b) => + String((a as Record).target ?? '').localeCompare( + String((b as Record).target ?? ''), + ), + ); +} + +function normalizeStageObjects(objects: unknown): unknown { + if (!Array.isArray(objects)) return []; + return objects + .map((object) => { + const item = object as FigureObjectSnapshot | BackgroundObjectSnapshot; + return stableClone({ + key: item.key, + sourceUrl: item.sourceUrl, + sourceExt: item.sourceExt, + sourceType: item.sourceType, + isExiting: item.isExiting, + transform: item.transform, + }); + }) + .sort((a, b) => { + const left = a as Record; + const right = b as Record; + return `${left.key}:${left.sourceUrl}:${left.isExiting}`.localeCompare( + `${right.key}:${right.sourceUrl}:${right.isExiting}`, + ); + }); +} + +export function toStableRuntimeSnapshot(snapshot: GameStateSnapshot): StableRuntimeSnapshot { + const stage = snapshot.stageState; + return { + scene: { + currentSentenceId: snapshot.sceneState.currentSentenceId, + sceneName: snapshot.sceneState.sceneName, + sceneUrl: snapshot.sceneState.sceneUrl, + sceneStackLength: snapshot.sceneState.sceneStackLength, + }, + stage: { + bgName: stage.bgName, + figName: stage.figName, + figNameLeft: stage.figNameLeft, + figNameRight: stage.figNameRight, + freeFigure: stableClone(stage.freeFigure), + figureAssociatedAnimation: stableClone(stage.figureAssociatedAnimation), + showText: stage.showText, + showTextSize: stage.showTextSize, + showName: stage.showName, + command: stage.command, + miniAvatar: stage.miniAvatar, + bgm: stableClone(stage.bgm), + GameVar: stableClone(stage.GameVar), + effects: normalizeEffects(stage.effects), + bgTransform: stage.bgTransform, + bgFilter: stage.bgFilter, + enableFilm: stage.enableFilm, + isDisableTextbox: stage.isDisableTextbox, + figureMetaData: stableClone(stage.figureMetaData), + animationSettings: normalizeAnimationSettings(stage.animationSettings), + }, + text: { + shownText: snapshot.textState.shownText, + shownName: snapshot.textState.shownName, + pendingElements: snapshot.textState.pendingElements, + totalElements: snapshot.textState.totalElements, + }, + pixi: snapshot.pixiState + ? { + stageWidth: snapshot.pixiState.stageWidth, + stageHeight: snapshot.pixiState.stageHeight, + figureObjects: normalizeStageObjects(snapshot.pixiState.figureObjects), + backgroundObjects: normalizeStageObjects(snapshot.pixiState.backgroundObjects), + activeAnimations: [...snapshot.pixiState.activeAnimations].sort((a, b) => + `${a.targetKey}:${a.type}`.localeCompare(`${b.targetKey}:${b.type}`), + ), + lockedTargets: [...snapshot.pixiState.lockedTargets].sort(), + } + : null, + performs: snapshot.performs + .filter((perform) => !perform.isHoldOn && !perform.skipNextCollect) + .map((perform) => ({ + ...perform, + duration: roundNumber(perform.duration), + })) + .sort((a, b) => a.performName.localeCompare(b.performName)), + backlogLength: snapshot.backlogLength, + gameplayState: snapshot.gameplayState, + }; +} + +export function compareStableRuntimeSnapshots( + a: GameStateSnapshot | StableRuntimeSnapshot, + b: GameStateSnapshot | StableRuntimeSnapshot, +): { match: boolean; diffs: string[] } { + const stableA = 'timestamp' in a ? toStableRuntimeSnapshot(a) : a; + const stableB = 'timestamp' in b ? toStableRuntimeSnapshot(b) : b; + const diffs: string[] = []; + + for (const key of Object.keys(stableA) as Array) { + const left = stableStringify(stableA[key]); + const right = stableStringify(stableB[key]); + if (left !== right) { + diffs.push(`${String(key)} differs`); + } + } + + return { match: diffs.length === 0, diffs }; +} + +/** + * 生成包含 N 条 say 语句的测试脚本 + */ +export function generateTestScene(count: number, prefix = '测试语句'): string { + return Array.from({ length: count }, (_, i) => `${prefix}${i + 1};`).join('\n'); +} + +/** + * 比较两个快照的核心状态是否一致(排除不稳定字段) + */ +export function compareSnapshots( + a: GameStateSnapshot, + b: GameStateSnapshot, +): { match: boolean; diffs: string[] } { + const diffs: string[] = []; + + // 比较 scene state + if (a.sceneState.currentSentenceId !== b.sceneState.currentSentenceId) { + diffs.push(`sceneState.currentSentenceId: ${a.sceneState.currentSentenceId} vs ${b.sceneState.currentSentenceId}`); + } + if (a.sceneState.sceneName !== b.sceneState.sceneName) { + diffs.push(`sceneState.sceneName: ${a.sceneState.sceneName} vs ${b.sceneState.sceneName}`); + } + if (a.sceneState.sceneUrl !== b.sceneState.sceneUrl) { + diffs.push(`sceneState.sceneUrl: ${a.sceneState.sceneUrl} vs ${b.sceneState.sceneUrl}`); + } + if (a.sceneState.sceneStackLength !== b.sceneState.sceneStackLength) { + diffs.push(`sceneState.sceneStackLength: ${a.sceneState.sceneStackLength} vs ${b.sceneState.sceneStackLength}`); + } + + // 比较 backlog 长度 + if (a.backlogLength !== b.backlogLength) { + diffs.push(`backlogLength: ${a.backlogLength} vs ${b.backlogLength}`); + } + + // 比较 stage state(排除不稳定字段) + const stageA = { ...a.stageState }; + const stageB = { ...b.stageState }; + for (const field of UNSTABLE_STAGE_FIELDS) { + delete stageA[field]; + delete stageB[field]; + } + const stageJsonA = stableStringify(stageA); + const stageJsonB = stableStringify(stageB); + if (stageJsonA !== stageJsonB) { + diffs.push(`stageState differs (after excluding unstable fields)`); + } + + return { match: diffs.length === 0, diffs }; +} diff --git a/packages/webgal-test/src/utils/fixture.ts b/packages/webgal-test/src/utils/fixture.ts new file mode 100644 index 000000000..846088ee0 --- /dev/null +++ b/packages/webgal-test/src/utils/fixture.ts @@ -0,0 +1,60 @@ +/** + * WebGAL 测试 - 浏览器生命周期管理 + * + * 提供 playwright 浏览器和页面的共享管理 + */ +import { chromium, type Browser, type Page, type BrowserContext } from 'playwright'; +import { waitForTestAPI, delay } from './bridge'; + +const WEBGAL_URL = process.env.WEBGAL_TEST_URL ?? 'http://localhost:4173'; + +let _browser: Browser | null = null; + +/** + * 获取(或懒启动)共享的 chromium 浏览器实例 + */ +export async function getBrowser(): Promise { + if (!_browser) { + _browser = await chromium.launch({ + headless: process.env.WEBGAL_TEST_HEADLESS !== 'false', + }); + } + return _browser; +} + +/** + * 创建一个新的测试页面,导航到 WebGAL 并等待测试 API 就绪 + */ +export async function createTestPage(): Promise { + const browser = await getBrowser(); + const context: BrowserContext = await browser.newContext({ + viewport: { width: 1280, height: 720 }, + }); + const page = await context.newPage(); + await page.goto(WEBGAL_URL, { waitUntil: 'domcontentloaded' }); + await waitForTestAPI(page, 30_000); + await page.waitForSelector('#FullScreenClick', { timeout: 30_000 }); + const metadata = await page.evaluate(() => window.webgalTest?.metadata); + if (!metadata?.testMode) { + throw new Error('window.webgalTest is present but test metadata is missing.'); + } + return page; +} + +/** + * 在测试页面上开始游戏并等待场景初始化 + */ +export async function startGameAndWait(page: Page, settleMs = 2000): Promise { + await page.evaluate(() => window.webgalTest!.controllers.startGame()); + await delay(settleMs); +} + +/** + * 关闭共享浏览器 + */ +export async function closeBrowser(): Promise { + if (_browser) { + await _browser.close(); + _browser = null; + } +} diff --git a/packages/webgal-test/src/utils/index.ts b/packages/webgal-test/src/utils/index.ts new file mode 100644 index 000000000..cf23fcbbb --- /dev/null +++ b/packages/webgal-test/src/utils/index.ts @@ -0,0 +1,102 @@ +export { + // 基础等待 + waitForTestAPI, + delay, + waitForSentenceAdvance, + clickStage, + // 流程控制 + callNextSentence, + callStartGame, + callContinueGame, + callScriptExecutor, + // 自动/快进 + callSwitchAuto, + callStopAuto, + callSwitchFast, + callStopFast, + callStopAll, + // 存档/读档 + callSaveGame, + callLoadGame, + callLoadGameFromStageData, + callJumpFromBacklog, + callGenerateCurrentStageData, + // 舞台控制 + callResetStage, + callPlayBgm, + // 场景管理 + callChangeScene, + callCallScene, + callSyncWithOrigine, + // 场景注入 + routeScene, + injectScene, + injectSceneAndRun, + resetRuntime, + settleText, + settleAnimations, + flushBrowserTasks, + // 状态快照 + takeSnapshot, + compareSnapshots, + compareStableRuntimeSnapshots, + toStableRuntimeSnapshot, + getTestMetadata, + getTextState, + waitForTextPending, + waitForTextSettled, + // 场景状态 + getCurrentSentenceId, + getCurrentSceneName, + getSceneStack, + getSentenceCount, + // Backlog + getBacklogLength, + getBacklog, + // 游戏状态 + getIsAuto, + getIsFast, + // Redux 状态 + getStageState, + getGuiState, + // 演出管理 + getPerformList, + removeAllPerforms, + waitForNoTransientPerforms, + waitForTransientPerform, + // Pixi 舞台 + getFigureObjects, + getBackgroundObjects, + getActiveAnimations, + waitForActiveAnimationOnTarget, + waitForNoActiveAnimationOnTarget, + waitForNoActiveAnimations, + getLockedTargets, + getStageObjectByKey, + getStageEffect, + // 配置 + getSystemConfig, + // Redux dispatch + dispatchSetStage, + dispatchSetVisibility, + setOptionData, + // 场景生成 + generateTestScene, + // 类型 + type GameStateSnapshot, + type PerformSnapshot, + type FigureObjectSnapshot, + type BackgroundObjectSnapshot, + type RuntimeAnimationSnapshot, + type TextRuntimeSnapshot, + type TestMetadata, + type StageEffectSnapshot, + type StableRuntimeSnapshot, +} from './bridge'; + +export { + getBrowser, + createTestPage, + startGameAndWait, + closeBrowser, +} from './fixture'; diff --git a/packages/webgal-test/tsconfig.json b/packages/webgal-test/tsconfig.json new file mode 100644 index 000000000..91bcafd18 --- /dev/null +++ b/packages/webgal-test/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "resolveJsonModule": true, + "isolatedModules": true, + "types": ["vitest/globals"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/webgal-test/vitest.config.ts b/packages/webgal-test/vitest.config.ts new file mode 100644 index 000000000..e237c8b94 --- /dev/null +++ b/packages/webgal-test/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + testTimeout: 120_000, + hookTimeout: 60_000, + pool: 'forks', + poolOptions: { + forks: { singleFork: true }, + }, + include: ['src/tests/**/*.test.ts'], + globalSetup: ['src/globalSetup.ts'], + setupFiles: ['src/setup.ts'], + }, +}); diff --git a/packages/webgal/package.json b/packages/webgal/package.json index 101c9fc72..22bba0a84 100644 --- a/packages/webgal/package.json +++ b/packages/webgal/package.json @@ -3,7 +3,9 @@ "version": "4.5.19", "scripts": { "dev": "vite --host --port 3000", + "dev:test": "cross-env WEBGAL_TEST=true vite --host --port 3000", "build": "node scripts/update-engine-version.js && cross-env NODE_ENV=production tsc && vite build --base=./", + "build:test": "node scripts/update-engine-version.js && cross-env NODE_ENV=production WEBGAL_TEST=true tsc && cross-env WEBGAL_TEST=true vite build --base=./", "preview": "vite preview", "lint": "eslint src/** --fix", "prepublishOnly": "npm run build" diff --git a/packages/webgal/src/main.tsx b/packages/webgal/src/main.tsx index 3a18f4117..6624ef75e 100644 --- a/packages/webgal/src/main.tsx +++ b/packages/webgal/src/main.tsx @@ -39,3 +39,10 @@ ReactDOM.render( , document.querySelector('#root'), ); + +// 测试框架初始化(仅在 WEBGAL_TEST=true 编译时启用) +if (__WEBGAL_TEST__) { + import('./test').then(({ initTestFramework }) => { + initTestFramework(); + }); +} diff --git a/packages/webgal/src/test/exposeTestAPI.ts b/packages/webgal/src/test/exposeTestAPI.ts new file mode 100644 index 000000000..57317960c --- /dev/null +++ b/packages/webgal/src/test/exposeTestAPI.ts @@ -0,0 +1,396 @@ +/** + * 暴露 WebGAL 内核到 window,供外部测试框架(vitest)调用 + * + * 设计原则:尽可能把所有内部模块暴露出来,让外部测试框架能完全访问, + * 以实现测试复杂流程、动画和舞台的目的。 + */ +import { WebGAL, Live2D } from '@/Core/WebGAL'; +import { webgalStore } from '@/store/store'; + +// ─── 游戏控制器 ─── +import { nextSentence } from '@/Core/controller/gamePlay/nextSentence'; +import { switchAuto, stopAuto } from '@/Core/controller/gamePlay/autoPlay'; +import { switchFast, stopFast, stopAll } from '@/Core/controller/gamePlay/fastSkip'; +import { startGame, continueGame } from '@/Core/controller/gamePlay/startContinueGame'; +import { scriptExecutor } from '@/Core/controller/gamePlay/scriptExecutor'; + +// ─── 存储控制器 ─── +import { saveGame, generateCurrentStageData } from '@/Core/controller/storage/saveGame'; +import { loadGame, loadGameFromStageData } from '@/Core/controller/storage/loadGame'; +import { jumpFromBacklog } from '@/Core/controller/storage/jumpFromBacklog'; + +// ─── 舞台控制器 ─── +import { resetStage } from '@/Core/controller/stage/resetStage'; +import { playBgm } from '@/Core/controller/stage/playBgm'; + +// ─── 场景管理 ─── +import { changeScene } from '@/Core/controller/scene/changeScene'; +import { callScene } from '@/Core/controller/scene/callScene'; +import { restoreScene } from '@/Core/controller/scene/restoreScene'; +import { sceneFetcher } from '@/Core/controller/scene/sceneFetcher'; +import { sceneParser, WebgalParser } from '@/Core/parser/sceneParser'; +import { syncWithOrigine } from '@/Core/util/syncWithEditor/syncWithOrigine'; + +// ─── Redux actions ─── +import { initState, setStage, resetStageState } from '@/store/stageReducer'; +import { setVisibility } from '@/store/GUIReducer'; +import { setOptionData } from '@/store/userDataReducer'; + +// ─── 配置 & 类型 ─── +import { SYSTEM_CONFIG, PERFORM_CONFIG } from '@/config'; + +import cloneDeep from 'lodash/cloneDeep'; +import type { + IBackgroundObjectSnapshot, + IGameStateSnapshot, + IRuntimeAnimationSnapshot, + IStageObjectSnapshot, + ITestMetadata, + ITextRuntimeSnapshot, + IWebGALTestAPI, +} from '@/test/types'; + +const TEST_API_VERSION = 2; + +const metadata: ITestMetadata = { + testMode: true, + apiVersion: TEST_API_VERSION, + exposedAt: Date.now(), + locationHref: window.location.href, +}; + +function serializeTransform(obj: { pixiContainer: any } | undefined | null): IStageObjectSnapshot['transform'] { + const container = obj?.pixiContainer; + if (!container) return null; + return { + x: container.x, + y: container.y, + scaleX: container.scale.x, + scaleY: container.scale.y, + rotation: container.rotation, + alpha: container.alphaFilterVal ?? container.alpha, + visible: container.visible, + zIndex: container.zIndex, + }; +} + +function serializeStageObject(obj: any): IStageObjectSnapshot | IBackgroundObjectSnapshot { + return { + uuid: obj.uuid, + key: obj.key, + sourceUrl: obj.sourceUrl, + sourceExt: obj.sourceExt, + sourceType: obj.sourceType, + isExiting: obj.isExiting ?? false, + transform: serializeTransform(obj), + }; +} + +function getActiveAnimations(): IRuntimeAnimationSnapshot[] { + const pixiStage = WebGAL.gameplay.pixiStage as any; + const stageAnimations = pixiStage?.stageAnimations ?? []; + return stageAnimations.map((animation: any) => ({ + key: animation.key, + targetKey: animation.targetKey ?? 'default', + type: animation.type, + })); +} + +function getTextState(): ITextRuntimeSnapshot { + const state = webgalStore.getState(); + const textRoot = document.querySelector('#textBoxMain'); + const allElements = Array.from(textRoot?.querySelectorAll('[class*="TextBox_textElement"]') ?? []); + const pendingElements = Array.from(document.querySelectorAll('.Textelement_start')); + const settledElements = allElements.filter((element) => !element.classList.contains('Textelement_start')); + + return { + shownText: state.stage.showText, + shownName: state.stage.showName, + currentDialogKey: state.stage.currentDialogKey, + totalElements: allElements.length, + pendingElements: pendingElements.length, + settledElements: settledElements.length, + visibleText: textRoot?.textContent ?? '', + }; +} + +function settleAnimations(): void { + const pixiStage = WebGAL.gameplay.pixiStage as any; + const stageAnimations = [...(pixiStage?.stageAnimations ?? [])]; + for (const animation of stageAnimations) { + pixiStage?.removeAnimationWithSetEffects(animation.key); + } +} + +function clearPixiObjects(): void { + const pixiStage = WebGAL.gameplay.pixiStage; + if (!pixiStage) return; + for (const obj of [...pixiStage.figureObjects, ...pixiStage.backgroundObjects]) { + pixiStage.removeStageObjectByKey(obj.key); + } +} + +function resetRuntime(): void { + stopAuto(); + stopFast(); + WebGAL.gameplay.pixiStage?.removeAllAnimations(); + WebGAL.gameplay.performController.removeAllPerform(); + resetStage(true, true); + clearPixiObjects(); + WebGAL.sceneManager.resetScene(); + WebGAL.backlogManager.makeBacklogEmpty(); + webgalStore.dispatch(resetStageState(cloneDeep(initState))); + webgalStore.dispatch(setVisibility({ component: 'showTitle', visibility: false })); + webgalStore.dispatch(setVisibility({ component: 'showTextBox', visibility: true })); + webgalStore.dispatch(setVisibility({ component: 'showBacklog', visibility: false })); + webgalStore.dispatch(setVisibility({ component: 'showMenuPanel', visibility: false })); +} + +/** + * 拍摄当前状态快照(包含完整的舞台、视觉、演出状态) + */ +function takeSnapshot(): IGameStateSnapshot { + const state = webgalStore.getState(); + const pixiStage = WebGAL.gameplay.pixiStage; + + return { + metadata, + + // Redux 状态 + stageState: cloneDeep(state.stage), + guiState: cloneDeep(state.GUI), + userData: cloneDeep(state.userData), + + // 场景状态 + sceneState: { + currentSentenceId: WebGAL.sceneManager.sceneData.currentSentenceId, + sceneName: WebGAL.sceneManager.sceneData.currentScene.sceneName, + sceneUrl: WebGAL.sceneManager.sceneData.currentScene.sceneUrl, + sentenceCount: WebGAL.sceneManager.sceneData.currentScene.sentenceList.length, + sceneStackLength: WebGAL.sceneManager.sceneData.sceneStack.length, + sceneStack: cloneDeep(WebGAL.sceneManager.sceneData.sceneStack), + }, + + // Backlog + backlog: cloneDeep(WebGAL.backlogManager.getBacklog()), + backlogLength: WebGAL.backlogManager.getBacklog().length, + + // 演出管理器 + performs: WebGAL.gameplay.performController.performList.map((p) => ({ + performName: p.performName, + duration: p.duration, + isHoldOn: p.isHoldOn, + blockingNext: p.blockingNext(), + blockingAuto: p.blockingAuto(), + goNextWhenOver: p.goNextWhenOver ?? false, + skipNextCollect: p.skipNextCollect ?? false, + })), + performListLength: WebGAL.gameplay.performController.performList.length, + + // Pixi 舞台视觉状态 + pixiState: pixiStage + ? { + figureObjects: pixiStage.figureObjects.map((obj) => serializeStageObject(obj) as IStageObjectSnapshot), + backgroundObjects: pixiStage.backgroundObjects.map( + (obj) => serializeStageObject(obj) as IBackgroundObjectSnapshot, + ), + allObjects: pixiStage.getAllStageObj().map((obj) => serializeStageObject(obj)), + activeAnimations: getActiveAnimations(), + lockedTargets: cloneDeep(pixiStage.getAllLockedObject()), + stageWidth: pixiStage.stageWidth, + stageHeight: pixiStage.stageHeight, + } + : null, + + textState: getTextState(), + + // 游戏播放状态 + gameplayState: { + isAuto: WebGAL.gameplay.isAuto, + isFast: WebGAL.gameplay.isFast, + }, + + // 用户自定义动画 + animations: WebGAL.animationManager.getAnimations().map((a) => ({ + name: a.name, + frameCount: a.effects.length, + })), + + timestamp: Date.now(), + }; +} + +/** + * 注入测试场景:直接用原始脚本文本创建场景并执行 + */ +function injectScene(rawSceneText: string, sceneName = '__test__', sceneUrl = `memory://${sceneName}`): void { + const parsed = sceneParser(rawSceneText, sceneName, sceneUrl); + WebGAL.sceneManager.sceneData.currentScene = parsed; + WebGAL.sceneManager.sceneData.currentSentenceId = 0; +} + +/** + * 注入测试场景并开始执行 + */ +function injectSceneAndRun( + rawSceneText: string, + sceneName = '__test__', + sceneUrl = `memory://${sceneName}`, +): void { + resetRuntime(); + injectScene(rawSceneText, sceneName, sceneUrl); + nextSentence(); +} + +/** + * 注入测试场景(使用 ISentence 数组直接注入,跳过解析) + */ +function injectParsedScene(sentenceList: unknown[], sceneName = '__test__'): void { + WebGAL.sceneManager.sceneData.currentScene = { + sceneName, + sceneUrl: `memory://${sceneName}`, + sentenceList: sentenceList as any[], + assetsList: [], + subSceneList: [], + }; + WebGAL.sceneManager.sceneData.currentSentenceId = 0; +} + +export function exposeTestAPI(): void { + const api: IWebGALTestAPI = { + metadata, + + // ═══ 核心实例(完整的内部访问) ═══ + core: WebGAL, + live2d: Live2D, + store: webgalStore, + + // ═══ Pixi 舞台(直接引用,可深度操控) ═══ + get pixiStage() { + return WebGAL.gameplay.pixiStage; + }, + get pixiApp() { + return WebGAL.gameplay.pixiStage?.currentApp ?? null; + }, + + // ═══ 子模块快捷访问 ═══ + get sceneManager() { + return WebGAL.sceneManager; + }, + get backlogManager() { + return WebGAL.backlogManager; + }, + get animationManager() { + return WebGAL.animationManager; + }, + get performController() { + return WebGAL.gameplay.performController; + }, + get gameplay() { + return WebGAL.gameplay; + }, + get events() { + return WebGAL.events; + }, + + // ═══ 控制器函数 ═══ + controllers: { + // 游戏流程 + nextSentence, + scriptExecutor, + switchAuto, + stopAuto, + switchFast, + stopFast, + stopAll, + startGame, + continueGame, + + // 存档/读档 + saveGame, + loadGame, + loadGameFromStageData, + jumpFromBacklog, + generateCurrentStageData, + + // 舞台 + resetStage, + playBgm, + + // 场景 + changeScene, + callScene, + restoreScene, + syncWithOrigine, + }, + + // ═══ 场景解析 & 注入 ═══ + sceneTools: { + sceneParser, + sceneFetcher, + webgalParser: WebgalParser, + injectScene, + injectSceneAndRun, + injectParsedScene, + }, + + // ═══ Redux dispatch helpers ═══ + dispatch: { + setStage: (key: string, value: unknown) => webgalStore.dispatch(setStage({ key, value } as any)), + resetStageState: (state: unknown) => webgalStore.dispatch(resetStageState(state as any)), + setVisibility: (component: string, visibility: boolean) => + webgalStore.dispatch(setVisibility({ component, visibility } as any)), + raw: (action: unknown) => webgalStore.dispatch(action as any), + }, + + // ═══ 配置 ═══ + config: { + SYSTEM_CONFIG, + PERFORM_CONFIG, + }, + + // ═══ 状态快照 ═══ + takeSnapshot, + + // ═══ 测试专用运行时工具 ═══ + testTools: { + resetRuntime, + settleText: () => WebGAL.events.textSettle.emit(), + settleAnimations, + getTextState, + getActiveAnimations, + getLockedTargets: () => cloneDeep(WebGAL.gameplay.pixiStage?.getAllLockedObject() ?? []), + getStageObjectByKey: (key: string) => { + const obj = WebGAL.gameplay.pixiStage?.getStageObjByKey(key); + return obj ? serializeStageObject(obj) : null; + }, + getEffectByTarget: (target: string) => { + const effect = webgalStore.getState().stage.effects.find((item) => item.target === target); + return effect ? cloneDeep(effect) : null; + }, + setOptionData: (key: string, value: unknown) => webgalStore.dispatch(setOptionData({ key: key as any, value })), + flushBrowserTasks: (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)), + }, + + // ═══ 工具 ═══ + utils: { + cloneDeep, + }, + }; + + window.webgalTest = api; + + console.log( + '%c🧪 WebGAL Test Mode Active', + 'color: #E91E63; font-weight: bold; font-size: 1.5em;', + ); + console.log( + '%cFull API exposed at window.webgalTest', + 'color: #9C27B0; font-style: italic;', + ); + console.log( + '%cModules: core, live2d, pixiStage, pixiApp, sceneManager, backlogManager, ' + + 'animationManager, performController, gameplay, events, controllers, sceneTools, dispatch, config', + 'color: #607D8B; font-size: 0.9em;', + ); +} diff --git a/packages/webgal/src/test/index.ts b/packages/webgal/src/test/index.ts new file mode 100644 index 000000000..2c452868f --- /dev/null +++ b/packages/webgal/src/test/index.ts @@ -0,0 +1,16 @@ +/** + * WebGAL 测试模式入口 + * + * 当 __WEBGAL_TEST__ 编译标志启用时,暴露内核 API 到 window 供 vitest 调用 + */ +import { exposeTestAPI } from '@/test/exposeTestAPI'; + +export function initTestFramework(): void { + if (document.readyState === 'complete') { + setTimeout(exposeTestAPI, 100); + } else { + window.addEventListener('load', () => { + setTimeout(exposeTestAPI, 100); + }); + } +} diff --git a/packages/webgal/src/test/types.ts b/packages/webgal/src/test/types.ts new file mode 100644 index 000000000..ec8f9999f --- /dev/null +++ b/packages/webgal/src/test/types.ts @@ -0,0 +1,267 @@ +import { IEffect, IStageState, ITransform } from '@/store/stageInterface'; +import { IUserData } from '@/store/userDataInterface'; +import { IGuiState } from '@/store/guiInterface'; + +/** + * 测试模式元数据 + */ +export interface ITestMetadata { + testMode: true; + apiVersion: number; + exposedAt: number; + locationHref: string; +} + +/** + * 演出项快照 + */ +export interface IPerformSnapshot { + performName: string; + duration: number; + isHoldOn: boolean; + blockingNext: boolean; + blockingAuto: boolean; + goNextWhenOver: boolean; + skipNextCollect: boolean; +} + +/** + * Pixi 舞台对象快照 + */ +export interface IStageObjectSnapshot { + uuid: string; + key: string; + sourceUrl: string; + sourceExt: string; + sourceType: string; + isExiting: boolean; + transform: { + x: number; + y: number; + scaleX: number; + scaleY: number; + rotation: number; + alpha: number; + visible: boolean; + zIndex: number; + } | null; +} + +/** + * 背景对象快照 + */ +export interface IBackgroundObjectSnapshot { + uuid: string; + key: string; + sourceUrl: string; + sourceExt: string; + sourceType: string; + isExiting: boolean; + transform: IStageObjectSnapshot['transform']; +} + +/** + * Pixi 舞台状态快照 + */ +export interface IPixiStateSnapshot { + figureObjects: IStageObjectSnapshot[]; + backgroundObjects: IBackgroundObjectSnapshot[]; + allObjects: Array; + activeAnimations: IRuntimeAnimationSnapshot[]; + lockedTargets: string[]; + stageWidth: number; + stageHeight: number; +} + +/** + * Pixi 动画运行时快照 + */ +export interface IRuntimeAnimationSnapshot { + key: string; + targetKey: string; + type: 'common' | 'preset'; +} + +/** + * 文本渐显运行时快照 + */ +export interface ITextRuntimeSnapshot { + shownText: string; + shownName: string; + currentDialogKey: string; + totalElements: number; + pendingElements: number; + settledElements: number; + visibleText: string; +} + +/** + * 游戏完整状态快照(包含视觉、演出、舞台全部状态) + */ +export interface IGameStateSnapshot { + metadata: ITestMetadata; + + // Redux 状态 + stageState: IStageState; + guiState: IGuiState; + userData: IUserData; + + // 场景状态 + sceneState: { + currentSentenceId: number; + sceneName: string; + sceneUrl: string; + sentenceCount: number; + sceneStackLength: number; + sceneStack: Array<{ sceneName: string; sceneUrl: string; continueLine: number }>; + }; + + // Backlog 完整数据 + backlog: unknown[]; + backlogLength: number; + + // 演出管理器 + performs: IPerformSnapshot[]; + performListLength: number; + + // Pixi 舞台视觉状态 + pixiState: IPixiStateSnapshot | null; + + // 文本渐显状态 + textState: ITextRuntimeSnapshot; + + // 游戏播放状态 + gameplayState: { + isAuto: boolean; + isFast: boolean; + }; + + // 用户自定义动画 + animations: Array<{ name: string; frameCount: number }>; + + timestamp: number; +} + +/** + * 暴露到 window 的完整测试 API + */ +export interface IWebGALTestAPI { + // ═══ 测试模式元数据 ═══ + readonly metadata: ITestMetadata; + + // ═══ 核心实例(完整的内部访问) ═══ + /** WebGAL 核心单例 — 可访问 sceneManager, backlogManager, gameplay, events 等全部子模块 */ + core: typeof import('@/Core/WebGAL').WebGAL; + /** Live2D 核心单例 */ + live2d: typeof import('@/Core/WebGAL').Live2D; + /** Redux store — getState(), dispatch(), subscribe() */ + store: typeof import('@/store/store').webgalStore; + + // ═══ Pixi 舞台(直接引用) ═══ + /** PixiStage 实例 — 可操作 figureObjects, backgroundObjects, containers, 动画注册等 */ + readonly pixiStage: any; + /** PIXI.Application 实例 — 可访问 renderer, stage, ticker, view (canvas) */ + readonly pixiApp: any; + + // ═══ 子模块快捷访问 ═══ + readonly sceneManager: typeof import('@/Core/WebGAL').WebGAL.sceneManager; + readonly backlogManager: typeof import('@/Core/WebGAL').WebGAL.backlogManager; + readonly animationManager: typeof import('@/Core/WebGAL').WebGAL.animationManager; + readonly performController: typeof import('@/Core/WebGAL').WebGAL.gameplay.performController; + readonly gameplay: typeof import('@/Core/WebGAL').WebGAL.gameplay; + readonly events: typeof import('@/Core/WebGAL').WebGAL.events; + + // ═══ 控制器函数 ═══ + controllers: { + // 游戏流程 + nextSentence: typeof import('@/Core/controller/gamePlay/nextSentence').nextSentence; + scriptExecutor: typeof import('@/Core/controller/gamePlay/scriptExecutor').scriptExecutor; + switchAuto: typeof import('@/Core/controller/gamePlay/autoPlay').switchAuto; + stopAuto: typeof import('@/Core/controller/gamePlay/autoPlay').stopAuto; + switchFast: typeof import('@/Core/controller/gamePlay/fastSkip').switchFast; + stopFast: typeof import('@/Core/controller/gamePlay/fastSkip').stopFast; + stopAll: typeof import('@/Core/controller/gamePlay/fastSkip').stopAll; + startGame: typeof import('@/Core/controller/gamePlay/startContinueGame').startGame; + continueGame: typeof import('@/Core/controller/gamePlay/startContinueGame').continueGame; + + // 存档/读档 + saveGame: typeof import('@/Core/controller/storage/saveGame').saveGame; + loadGame: typeof import('@/Core/controller/storage/loadGame').loadGame; + loadGameFromStageData: typeof import('@/Core/controller/storage/loadGame').loadGameFromStageData; + jumpFromBacklog: typeof import('@/Core/controller/storage/jumpFromBacklog').jumpFromBacklog; + generateCurrentStageData: typeof import('@/Core/controller/storage/saveGame').generateCurrentStageData; + + // 舞台 + resetStage: typeof import('@/Core/controller/stage/resetStage').resetStage; + playBgm: typeof import('@/Core/controller/stage/playBgm').playBgm; + + // 场景 + changeScene: typeof import('@/Core/controller/scene/changeScene').changeScene; + callScene: typeof import('@/Core/controller/scene/callScene').callScene; + restoreScene: typeof import('@/Core/controller/scene/restoreScene').restoreScene; + syncWithOrigine: typeof import('@/Core/util/syncWithEditor/syncWithOrigine').syncWithOrigine; + }; + + // ═══ 场景解析 & 注入 ═══ + sceneTools: { + /** 解析原始脚本文本为 IScene */ + sceneParser: typeof import('@/Core/parser/sceneParser').sceneParser; + /** HTTP 获取远程场景文本 */ + sceneFetcher: typeof import('@/Core/controller/scene/sceneFetcher').sceneFetcher; + /** WebGAL Parser 实例 */ + webgalParser: typeof import('@/Core/parser/sceneParser').WebgalParser; + /** 注入场景(仅设置,不执行) */ + injectScene: (rawSceneText: string, sceneName?: string, sceneUrl?: string) => void; + /** 注入场景并开始执行 */ + injectSceneAndRun: (rawSceneText: string, sceneName?: string, sceneUrl?: string) => void; + /** 注入已解析的 ISentence 数组 */ + injectParsedScene: (sentenceList: unknown[], sceneName?: string) => void; + }; + + // ═══ Redux dispatch helpers ═══ + dispatch: { + setStage: (key: string, value: unknown) => void; + resetStageState: (state: unknown) => void; + setVisibility: (component: string, visibility: boolean) => void; + /** 直接 dispatch 任意 action */ + raw: (action: unknown) => void; + }; + + // ═══ 配置 ═══ + config: { + SYSTEM_CONFIG: typeof import('@/config').SYSTEM_CONFIG; + PERFORM_CONFIG: typeof import('@/config').PERFORM_CONFIG; + }; + + // ═══ 状态快照 ═══ + takeSnapshot: () => IGameStateSnapshot; + + // ═══ 测试专用运行时工具 ═══ + testTools: { + /** 清理演出、动画、backlog、场景和舞台状态,进入可注入场景的空测试态 */ + resetRuntime: () => void; + /** 触发文本直接进入终态 */ + settleText: () => void; + /** 将所有 Pixi 动画直接写入终态并清除动画队列 */ + settleAnimations: () => void; + /** 读取文本渐显 DOM 状态 */ + getTextState: () => ITextRuntimeSnapshot; + /** 读取当前 Pixi 动画运行时队列 */ + getActiveAnimations: () => IRuntimeAnimationSnapshot[]; + /** 读取当前被动画锁定、不能应用内部变换的目标 */ + getLockedTargets: () => string[]; + /** 按 key 读取舞台对象快照 */ + getStageObjectByKey: (key: string) => IStageObjectSnapshot | IBackgroundObjectSnapshot | null; + /** 按 target 读取舞台内部变换记录 */ + getEffectByTarget: (target: string) => IEffect | null; + /** 设置用户选项,便于控制文字速度、自动速度等测试条件 */ + setOptionData: (key: string, value: unknown) => void; + /** 等待浏览器宏任务推进 */ + flushBrowserTasks: (ms?: number) => Promise; + }; + + // ═══ 工具 ═══ + utils: { + cloneDeep: typeof import('lodash/cloneDeep'); + }; +} diff --git a/packages/webgal/src/vite-env.d.ts b/packages/webgal/src/vite-env.d.ts index c2100dd05..1ecac6c99 100644 --- a/packages/webgal/src/vite-env.d.ts +++ b/packages/webgal/src/vite-env.d.ts @@ -1,2 +1,14 @@ /// /// + +declare const __WEBGAL_TEST__: boolean; + +interface Window { + __WEBGAL_DEVICE_INFO__?: { + isIOS?: boolean; + }; + webgalTest?: import('@/test/types').IWebGALTestAPI; + /** Pixi 调试引用(由 PixiController 设置) */ + __PIXI_APP__?: unknown; + PIXIapp?: unknown; +} diff --git a/packages/webgal/vite.config.ts b/packages/webgal/vite.config.ts index 703e6cd27..e70f98d38 100644 --- a/packages/webgal/vite.config.ts +++ b/packages/webgal/vite.config.ts @@ -9,7 +9,11 @@ import viteCompression from 'vite-plugin-compression'; // @ts-ignore const env = process.env.NODE_ENV; +const isTestMode = process.env.WEBGAL_TEST === 'true'; console.log(env); +if (isTestMode) { + console.log('[WebGAL] Test mode enabled'); +} export default defineConfig({ plugins: [ @@ -27,6 +31,9 @@ export default defineConfig({ '@': resolve('src'), }, }, + define: { + __WEBGAL_TEST__: JSON.stringify(isTestMode), + }, build: { // sourcemap: true, }, diff --git a/yarn.lock b/yarn.lock index 9ef0a5ec1..c5ff9ca65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -368,6 +368,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== +"@esbuild/aix-ppc64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz#82b74f92aa78d720b714162939fb248c90addf53" + integrity sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg== + "@esbuild/android-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" @@ -378,6 +383,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== +"@esbuild/android-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz#f78cb8a3121fc205a53285adb24972db385d185d" + integrity sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ== + "@esbuild/android-arm@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" @@ -388,6 +398,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== +"@esbuild/android-arm@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz#593e10a1450bbfcac6cb321f61f468453bac209d" + integrity sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ== + "@esbuild/android-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" @@ -398,6 +413,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== +"@esbuild/android-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz#453143d073326033d2d22caf9e48de4bae274b07" + integrity sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg== + "@esbuild/darwin-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" @@ -408,6 +428,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== +"@esbuild/darwin-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz#6f23000fb9b40b7e04b7d0606c0693bd0632f322" + integrity sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw== + "@esbuild/darwin-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" @@ -418,6 +443,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== +"@esbuild/darwin-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz#27393dd18bb1263c663979c5f1576e00c2d024be" + integrity sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ== + "@esbuild/freebsd-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" @@ -428,6 +458,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== +"@esbuild/freebsd-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz#22e4638fa502d1c0027077324c97640e3adf3a62" + integrity sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w== + "@esbuild/freebsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" @@ -438,6 +473,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== +"@esbuild/freebsd-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz#9224b8e4fea924ce2194e3efc3e9aebf822192d6" + integrity sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ== + "@esbuild/linux-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" @@ -448,6 +488,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== +"@esbuild/linux-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz#4f5d1c27527d817b35684ae21419e57c2bda0966" + integrity sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A== + "@esbuild/linux-arm@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" @@ -458,6 +503,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== +"@esbuild/linux-arm@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz#b9e9d070c8c1c0449cf12b20eac37d70a4595921" + integrity sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA== + "@esbuild/linux-ia32@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" @@ -468,6 +518,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== +"@esbuild/linux-ia32@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz#3f80fb696aa96051a94047f35c85b08b21c36f9e" + integrity sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg== + "@esbuild/linux-loong64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" @@ -478,6 +533,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== +"@esbuild/linux-loong64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz#9be1f2c28210b13ebb4156221bba356fe1675205" + integrity sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q== + "@esbuild/linux-mips64el@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" @@ -488,6 +548,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== +"@esbuild/linux-mips64el@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz#4ab5ee67a3dfcbcb5e8fd7883dae6e735b1163b8" + integrity sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw== + "@esbuild/linux-ppc64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" @@ -498,6 +563,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== +"@esbuild/linux-ppc64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz#dac78c689f6499459c4321e5c15032c12307e7ea" + integrity sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ== + "@esbuild/linux-riscv64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" @@ -508,6 +578,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== +"@esbuild/linux-riscv64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz#050f7d3b355c3a98308e935bc4d6325da91b0027" + integrity sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ== + "@esbuild/linux-s390x@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" @@ -518,6 +593,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== +"@esbuild/linux-s390x@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz#d61f715ce61d43fe5844ad0d8f463f88cbe4fef6" + integrity sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw== + "@esbuild/linux-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" @@ -528,6 +608,16 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== +"@esbuild/linux-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz#ca8e1aa478fc8209257bf3ac8f79c4dc2982f32a" + integrity sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA== + +"@esbuild/netbsd-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz#1650f2c1b948deeb3ef948f2fc30614723c09690" + integrity sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w== + "@esbuild/netbsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" @@ -538,6 +628,16 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== +"@esbuild/netbsd-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz#65772ab342c4b3319bf0705a211050aac1b6e320" + integrity sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw== + +"@esbuild/openbsd-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz#37ed7cfa66549d7955852fce37d0c3de4e715ea1" + integrity sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A== + "@esbuild/openbsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" @@ -548,6 +648,16 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== +"@esbuild/openbsd-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz#01bf3d385855ef50cb33db7c4b52f957c34cd179" + integrity sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg== + +"@esbuild/openharmony-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz#6c1f94b34086599aabda4eac8f638294b9877410" + integrity sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw== + "@esbuild/sunos-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" @@ -558,6 +668,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== +"@esbuild/sunos-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz#4b0dd17ae0a6941d2d0fd35a906392517071a90d" + integrity sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA== + "@esbuild/win32-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" @@ -568,6 +683,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== +"@esbuild/win32-arm64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz#34193ab5565d6ff68ca928ac04be75102ccb2e77" + integrity sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA== + "@esbuild/win32-ia32@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" @@ -578,6 +698,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== +"@esbuild/win32-ia32@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz#eb67f0e4482515d8c1894ede631c327a4da9fc4d" + integrity sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw== + "@esbuild/win32-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" @@ -588,6 +713,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== +"@esbuild/win32-x64@0.27.7": + version "0.27.7" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz#8fe30b3088b89b4873c3a6cc87597ae3920c0a8b" + integrity sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg== + "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" @@ -721,6 +851,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -1195,126 +1330,251 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz#7e158ddfc16f78da99c0d5ccbae6cae403ef3284" integrity sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A== +"@rollup/rollup-android-arm-eabi@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz#043f145716234529052ef9e1ce1d847ffbe9e674" + integrity sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA== + "@rollup/rollup-android-arm64@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz#49f4ae0e22b6f9ffbcd3818b9a0758fa2d10b1cd" integrity sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw== +"@rollup/rollup-android-arm64@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz#023e1bd146e7519087dfd9e8b29e4cf9f8ecd35c" + integrity sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA== + "@rollup/rollup-darwin-arm64@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz#bb200269069acf5c1c4d79ad142524f77e8b8236" integrity sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA== +"@rollup/rollup-darwin-arm64@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz#55ccb5487c02419954c57a7a80602885d616e1ee" + integrity sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw== + "@rollup/rollup-darwin-x64@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz#1bf7a92b27ebdd5e0d1d48503c7811160773be1a" integrity sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw== +"@rollup/rollup-darwin-x64@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz#254b65404b14488c83225e88b8819376ad71a784" + integrity sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew== + "@rollup/rollup-freebsd-arm64@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz#5ccf537b99c5175008444702193ad0b1c36f7f16" integrity sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw== +"@rollup/rollup-freebsd-arm64@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz#6377ff38c052c76fcaffb7b2728d3172fe676fe6" + integrity sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w== + "@rollup/rollup-freebsd-x64@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz#1196ecd7bf4e128624ef83cd1f9d785114474a77" integrity sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA== +"@rollup/rollup-freebsd-x64@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz#ba3902309d088eaf7139b916f09b7140b28b406d" + integrity sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g== + "@rollup/rollup-linux-arm-gnueabihf@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz#cc147633a4af229fee83a737bf2334fbac3dc28e" integrity sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g== +"@rollup/rollup-linux-arm-gnueabihf@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz#e011b9a14638267e53b446286e838dbdaf53f167" + integrity sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g== + "@rollup/rollup-linux-arm-musleabihf@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz#3559f9f060153ea54594a42c3b87a297bedcc26e" integrity sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ== +"@rollup/rollup-linux-arm-musleabihf@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz#0bce9ce9a009490abd28fd922dd97ed521311afe" + integrity sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg== + "@rollup/rollup-linux-arm64-gnu@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz#e91f887b154123485cfc4b59befe2080fcd8f2df" integrity sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A== +"@rollup/rollup-linux-arm64-gnu@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz#6f6cfbbf324fbb4ceff213abdf7f322fd45d25ff" + integrity sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ== + "@rollup/rollup-linux-arm64-musl@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz#660752f040df9ba44a24765df698928917c0bf21" integrity sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ== +"@rollup/rollup-linux-arm64-musl@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz#f7cb3eecaea9c151ef77342af05f38ae924bf795" + integrity sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA== + "@rollup/rollup-linux-loong64-gnu@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz#cb0e939a5fa479ccef264f3f45b31971695f869c" integrity sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw== +"@rollup/rollup-linux-loong64-gnu@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz#499bfac6bb669fd88bb664357bf6be996a28b92f" + integrity sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ== + "@rollup/rollup-linux-loong64-musl@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz#42f86fbc82cd1a81be2d346476dd3231cf5ee442" integrity sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog== +"@rollup/rollup-linux-loong64-musl@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz#127dfac08764764396bbe04453c545d38a3ab518" + integrity sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw== + "@rollup/rollup-linux-ppc64-gnu@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz#39776a647a789dc95ea049277c5ef8f098df77f9" integrity sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ== +"@rollup/rollup-linux-ppc64-gnu@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz#6a72f4d95852aac18326c5bf708393e8f3a41b70" + integrity sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw== + "@rollup/rollup-linux-ppc64-musl@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz#466f20029a8e8b3bb2954c7ddebc9586420cac2c" integrity sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg== +"@rollup/rollup-linux-ppc64-musl@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz#ba8674666b00d6f9066cb9a5771a8430c34d2de6" + integrity sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg== + "@rollup/rollup-linux-riscv64-gnu@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz#cff9877c78f12e7aa6246f6902ad913e99edb2b7" integrity sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA== +"@rollup/rollup-linux-riscv64-gnu@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz#17cc38b2a71e302547cad29bcf78d0db2618c922" + integrity sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg== + "@rollup/rollup-linux-riscv64-musl@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz#9a762fb99b5a82a921017f56491b7e892b9fb17d" integrity sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ== +"@rollup/rollup-linux-riscv64-musl@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz#e36a41e2d8bd247331bd5cfc13b8c951d33454a2" + integrity sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg== + "@rollup/rollup-linux-s390x-gnu@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz#9d25ad8ac7dab681935baf78ac5ea92d14629cdf" integrity sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ== +"@rollup/rollup-linux-s390x-gnu@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz#1687265f1f4bdea0726c761a58c2db9933609d68" + integrity sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ== + "@rollup/rollup-linux-x64-gnu@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz#5e5139e11819fa38a052368da79422cb4afcf466" integrity sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg== +"@rollup/rollup-linux-x64-gnu@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz#56a6a0d9076f2a05a976031493b24a20ddcc0e77" + integrity sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg== + "@rollup/rollup-linux-x64-musl@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz#b6211d46e11b1f945f5504cc794fce839331ed08" integrity sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw== +"@rollup/rollup-linux-x64-musl@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz#bc240ebb5b9fd8d41ca8a80cb458452e8c187e0f" + integrity sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w== + "@rollup/rollup-openbsd-x64@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz#e6e09eebaa7012bb9c7331b437a9e992bd94ca35" integrity sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw== +"@rollup/rollup-openbsd-x64@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz#6f80d48a006c4b2ffa7724e95a3e33f6975872af" + integrity sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw== + "@rollup/rollup-openharmony-arm64@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz#f7d99ae857032498e57a5e7259fb7100fd24a87e" integrity sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA== +"@rollup/rollup-openharmony-arm64@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz#8f6db6f70d0a48abd833b263cd6dd3e7199c4c0e" + integrity sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA== + "@rollup/rollup-win32-arm64-msvc@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz#41e392f5d9f3bf1253fdaf2f6d6f6b1bfc452856" integrity sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ== +"@rollup/rollup-win32-arm64-msvc@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz#b68989bfa815d0b3d4e302ecd90bda744438b177" + integrity sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g== + "@rollup/rollup-win32-ia32-msvc@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz#f41b0490be0e5d3cf459b4dc076a192b532adea9" integrity sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w== +"@rollup/rollup-win32-ia32-msvc@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz#c098e45338c50f22f1b288476354f025b746285b" + integrity sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg== + "@rollup/rollup-win32-x64-gnu@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz#0fcf9f1fcb750f0317b13aac3b3231687e6397a5" integrity sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA== +"@rollup/rollup-win32-x64-gnu@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz#2c9e15be155b79d05999953b1737b2903842e903" + integrity sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg== + "@rollup/rollup-win32-x64-msvc@4.60.0": version "4.60.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz#3afdb30405f6d4248df5e72e1ca86c5eab55fab8" integrity sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w== +"@rollup/rollup-win32-x64-msvc@4.60.1": + version "4.60.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz#23b860113e9f87eea015d1fa3a4240a52b42fcd4" + integrity sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ== + "@tsconfig/node10@^1.0.7": version "1.0.10" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.10.tgz#b7ebd3adfa7750628d100594f6726b054d2c33b2" @@ -1380,6 +1640,19 @@ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.14.tgz#ae3055ea2be43c91c9fd700a36d67820026d96e6" integrity sha512-Wj71sXE4Q4AkGdG9Tvq1u/fquNz9EdG4LIJMwVVII7ashjD/8cf8fyIfJAjRr6YcsXnSE8cOGQPq1gqeR8z+3w== +"@types/chai@^5.2.2": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" + integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + dependencies: + "@types/deep-eql" "*" + assertion-error "^2.0.1" + +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + "@types/earcut@^2.1.0": version "2.1.4" resolved "https://registry.yarnpkg.com/@types/earcut/-/earcut-2.1.4.tgz#5811d7d333048f5a7573b22ddc84923e69596da6" @@ -1633,6 +1906,33 @@ "@vitest/utils" "0.28.5" chai "^4.3.7" +"@vitest/expect@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.2.4.tgz#8362124cd811a5ee11c5768207b9df53d34f2433" + integrity sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig== + dependencies: + "@types/chai" "^5.2.2" + "@vitest/spy" "3.2.4" + "@vitest/utils" "3.2.4" + chai "^5.2.0" + tinyrainbow "^2.0.0" + +"@vitest/mocker@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.2.4.tgz#4471c4efbd62db0d4fa203e65cc6b058a85cabd3" + integrity sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ== + dependencies: + "@vitest/spy" "3.2.4" + estree-walker "^3.0.3" + magic-string "^0.30.17" + +"@vitest/pretty-format@3.2.4", "@vitest/pretty-format@^3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.2.4.tgz#3c102f79e82b204a26c7a5921bf47d534919d3b4" + integrity sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA== + dependencies: + tinyrainbow "^2.0.0" + "@vitest/runner@0.28.5": version "0.28.5" resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.28.5.tgz#4a18fe0e40b25569763f9f1f64b799d1629b3026" @@ -1642,6 +1942,24 @@ p-limit "^4.0.0" pathe "^1.1.0" +"@vitest/runner@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.2.4.tgz#5ce0274f24a971f6500f6fc166d53d8382430766" + integrity sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ== + dependencies: + "@vitest/utils" "3.2.4" + pathe "^2.0.3" + strip-literal "^3.0.0" + +"@vitest/snapshot@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.2.4.tgz#40a8bc0346ac0aee923c0eefc2dc005d90bc987c" + integrity sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ== + dependencies: + "@vitest/pretty-format" "3.2.4" + magic-string "^0.30.17" + pathe "^2.0.3" + "@vitest/spy@0.28.5": version "0.28.5" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.28.5.tgz#b69affa0786200251b9e5aac5c58bbfb1b3273c9" @@ -1649,6 +1967,13 @@ dependencies: tinyspy "^1.0.2" +"@vitest/spy@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.4.tgz#cc18f26f40f3f028da6620046881f4e4518c2599" + integrity sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw== + dependencies: + tinyspy "^4.0.3" + "@vitest/utils@0.28.5": version "0.28.5" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.28.5.tgz#7b82b528df86adfbd4a1f6a3b72c39790e81de0d" @@ -1660,6 +1985,15 @@ picocolors "^1.0.0" pretty-format "^27.5.1" +"@vitest/utils@3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.2.4.tgz#c0813bc42d99527fb8c5b138c7a88516bca46fea" + integrity sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA== + dependencies: + "@vitest/pretty-format" "3.2.4" + loupe "^3.1.4" + tinyrainbow "^2.0.0" + accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -1896,6 +2230,11 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -2131,6 +2470,17 @@ chai@^4.3.7: pathval "^1.1.1" type-detect "^4.0.8" +chai@^5.2.0: + version "5.3.3" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06" + integrity sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw== + dependencies: + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" + chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2155,6 +2505,11 @@ check-error@^1.0.3: dependencies: get-func-name "^2.0.2" +check-error@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.3.tgz#2427361117b70cca8dc89680ead32b157019caf5" + integrity sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA== + chevrotain@^10.5.0: version "10.5.0" resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-10.5.0.tgz#9c1dc62ef0753bb562dbe521b5f72d041bad624e" @@ -2430,7 +2785,7 @@ debug@^4.3.3: dependencies: ms "^2.1.3" -debug@^4.4.3: +debug@^4.4.1, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -2451,6 +2806,11 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -2690,6 +3050,11 @@ es-iterator-helpers@^1.0.17: iterator.prototype "^1.1.2" safe-array-concat "^1.1.2" +es-module-lexer@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + es-object-atoms@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" @@ -2796,6 +3161,38 @@ esbuild@^0.21.3: "@esbuild/win32-ia32" "0.21.5" "@esbuild/win32-x64" "0.21.5" +esbuild@^0.27.0: + version "0.27.7" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.7.tgz#bcadce22b2f3fd76f257e3a64f83a64986fea11f" + integrity sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w== + optionalDependencies: + "@esbuild/aix-ppc64" "0.27.7" + "@esbuild/android-arm" "0.27.7" + "@esbuild/android-arm64" "0.27.7" + "@esbuild/android-x64" "0.27.7" + "@esbuild/darwin-arm64" "0.27.7" + "@esbuild/darwin-x64" "0.27.7" + "@esbuild/freebsd-arm64" "0.27.7" + "@esbuild/freebsd-x64" "0.27.7" + "@esbuild/linux-arm" "0.27.7" + "@esbuild/linux-arm64" "0.27.7" + "@esbuild/linux-ia32" "0.27.7" + "@esbuild/linux-loong64" "0.27.7" + "@esbuild/linux-mips64el" "0.27.7" + "@esbuild/linux-ppc64" "0.27.7" + "@esbuild/linux-riscv64" "0.27.7" + "@esbuild/linux-s390x" "0.27.7" + "@esbuild/linux-x64" "0.27.7" + "@esbuild/netbsd-arm64" "0.27.7" + "@esbuild/netbsd-x64" "0.27.7" + "@esbuild/openbsd-arm64" "0.27.7" + "@esbuild/openbsd-x64" "0.27.7" + "@esbuild/openharmony-arm64" "0.27.7" + "@esbuild/sunos-x64" "0.27.7" + "@esbuild/win32-arm64" "0.27.7" + "@esbuild/win32-ia32" "0.27.7" + "@esbuild/win32-x64" "0.27.7" + escalade@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" @@ -2958,6 +3355,13 @@ estree-walker@^2.0.1, estree-walker@^2.0.2: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -2993,6 +3397,11 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +expect-type@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68" + integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== + express@^4.20.0: version "4.21.0" resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915" @@ -3258,6 +3667,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -3998,6 +4412,11 @@ js-binary-schema-parser@^2.0.3: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-tokens@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.1.tgz#2ec43964658435296f6761b34e10671c2d9527f4" + integrity sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== + js-yaml@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" @@ -4197,6 +4616,11 @@ loupe@^2.3.6: dependencies: get-func-name "^2.0.1" +loupe@^3.1.0, loupe@^3.1.4: + version "3.2.1" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76" + integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -4218,6 +4642,13 @@ magic-string@^0.27.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" +magic-string@^0.30.17: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -4667,11 +5098,21 @@ pathe@^1.1.0, pathe@^1.1.2: resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + pathval@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== +pathval@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.1.tgz#8855c5a2899af072d6ac05d11e46045ad0dc605d" + integrity sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -4692,7 +5133,7 @@ picomatch@^4.0.2: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== -picomatch@^4.0.3: +picomatch@^4.0.3, picomatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== @@ -4856,6 +5297,20 @@ pkg@^5.8.0: resolve "^1.22.0" stream-meter "^1.0.4" +playwright-core@1.59.1: + version "1.59.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.59.1.tgz#d8a2b28bcb8f2bd08ef3df93b02ae83c813244b2" + integrity sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg== + +playwright@^1.52.0: + version "1.59.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.59.1.tgz#f7b0ca61637ae25264cec370df671bbe1f368a4a" + integrity sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw== + dependencies: + playwright-core "1.59.1" + optionalDependencies: + fsevents "2.3.2" + popmotion@^11.0.5: version "11.0.5" resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-11.0.5.tgz#8e3e014421a0ffa30ecd722564fd2558954e1f7d" @@ -4889,6 +5344,15 @@ postcss@^8.4.43: picocolors "^1.1.1" source-map-js "^1.2.1" +postcss@^8.5.6: + version "8.5.9" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.9.tgz#f6ee9e0b94f0f19c97d2f172bfbd7fc71fe1cca4" + integrity sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + prebuild-install@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" @@ -5325,6 +5789,40 @@ rollup@^4.20.0: "@rollup/rollup-win32-x64-msvc" "4.60.0" fsevents "~2.3.2" +rollup@^4.43.0: + version "4.60.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.60.1.tgz#b4aa2bcb3a5e1437b5fad40d43fe42d4bde7a42d" + integrity sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w== + dependencies: + "@types/estree" "1.0.8" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.60.1" + "@rollup/rollup-android-arm64" "4.60.1" + "@rollup/rollup-darwin-arm64" "4.60.1" + "@rollup/rollup-darwin-x64" "4.60.1" + "@rollup/rollup-freebsd-arm64" "4.60.1" + "@rollup/rollup-freebsd-x64" "4.60.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.60.1" + "@rollup/rollup-linux-arm-musleabihf" "4.60.1" + "@rollup/rollup-linux-arm64-gnu" "4.60.1" + "@rollup/rollup-linux-arm64-musl" "4.60.1" + "@rollup/rollup-linux-loong64-gnu" "4.60.1" + "@rollup/rollup-linux-loong64-musl" "4.60.1" + "@rollup/rollup-linux-ppc64-gnu" "4.60.1" + "@rollup/rollup-linux-ppc64-musl" "4.60.1" + "@rollup/rollup-linux-riscv64-gnu" "4.60.1" + "@rollup/rollup-linux-riscv64-musl" "4.60.1" + "@rollup/rollup-linux-s390x-gnu" "4.60.1" + "@rollup/rollup-linux-x64-gnu" "4.60.1" + "@rollup/rollup-linux-x64-musl" "4.60.1" + "@rollup/rollup-openbsd-x64" "4.60.1" + "@rollup/rollup-openharmony-arm64" "4.60.1" + "@rollup/rollup-win32-arm64-msvc" "4.60.1" + "@rollup/rollup-win32-ia32-msvc" "4.60.1" + "@rollup/rollup-win32-x64-gnu" "4.60.1" + "@rollup/rollup-win32-x64-msvc" "4.60.1" + fsevents "~2.3.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -5604,6 +6102,11 @@ std-env@^3.3.1: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== +std-env@^3.9.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b" + integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== + stream-meter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/stream-meter/-/stream-meter-1.0.4.tgz#52af95aa5ea760a2491716704dbff90f73afdd1d" @@ -5730,6 +6233,13 @@ strip-literal@^1.0.0: dependencies: acorn "^8.10.0" +strip-literal@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-3.1.0.tgz#222b243dd2d49c0bcd0de8906adbd84177196032" + integrity sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg== + dependencies: + js-tokens "^9.0.1" + strip-outer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" @@ -5824,6 +6334,24 @@ tinybench@^2.3.1: resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.6.0.tgz#1423284ee22de07c91b3752c048d2764714b341b" integrity sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA== +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + +tinyglobby@^0.2.14: + version "0.2.16" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6" + integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + tinyglobby@^0.2.15: version "0.2.15" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" @@ -5837,11 +6365,26 @@ tinypool@^0.3.1: resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.3.1.tgz#a99c2e446aba9be05d3e1cb756d6aed7af4723b6" integrity sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ== +tinypool@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" + integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== + +tinyrainbow@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" + integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== + tinyspy@^1.0.2: version "1.1.1" resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-1.1.1.tgz#0cb91d5157892af38cb2d217f5c7e8507a5bf092" integrity sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g== +tinyspy@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-4.0.4.tgz#d77a002fb53a88aa1429b419c1c92492e0c81f78" + integrity sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -5997,6 +6540,11 @@ typescript@^4.5.4, typescript@^4.9.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.8.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + ufo@^1.3.2: version "1.5.3" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" @@ -6127,6 +6675,17 @@ vite-node@0.28.5: source-map-support "^0.5.21" vite "^3.0.0 || ^4.0.0" +vite-node@3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07" + integrity sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg== + dependencies: + cac "^6.7.14" + debug "^4.4.1" + es-module-lexer "^1.7.0" + pathe "^2.0.3" + vite "^5.0.0 || ^6.0.0 || ^7.0.0-0" + vite-plugin-compression@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz#a75b0d8f48357ebb377b65016da9f20885ef39b6" @@ -6152,6 +6711,20 @@ vite-plugin-package-version@^1.0.2: optionalDependencies: fsevents "~2.3.2" +"vite@^5.0.0 || ^6.0.0 || ^7.0.0-0": + version "7.3.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.2.tgz#cb041794d4c1395e28baea98198fd6e8f4b96b5c" + integrity sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg== + dependencies: + esbuild "^0.27.0" + fdir "^6.5.0" + picomatch "^4.0.3" + postcss "^8.5.6" + rollup "^4.43.0" + tinyglobby "^0.2.15" + optionalDependencies: + fsevents "~2.3.3" + vite@^5.4.21: version "5.4.21" resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027" @@ -6193,6 +6766,35 @@ vitest@0.28.5, vitest@^0.28.5: vite-node "0.28.5" why-is-node-running "^2.2.2" +vitest@^3.2.1: + version "3.2.4" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.2.4.tgz#0637b903ad79d1539a25bc34c0ed54b5c67702ea" + integrity sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A== + dependencies: + "@types/chai" "^5.2.2" + "@vitest/expect" "3.2.4" + "@vitest/mocker" "3.2.4" + "@vitest/pretty-format" "^3.2.4" + "@vitest/runner" "3.2.4" + "@vitest/snapshot" "3.2.4" + "@vitest/spy" "3.2.4" + "@vitest/utils" "3.2.4" + chai "^5.2.0" + debug "^4.4.1" + expect-type "^1.2.1" + magic-string "^0.30.17" + pathe "^2.0.3" + picomatch "^4.0.2" + std-env "^3.9.0" + tinybench "^2.9.0" + tinyexec "^0.3.2" + tinyglobby "^0.2.14" + tinypool "^1.1.1" + tinyrainbow "^2.0.0" + vite "^5.0.0 || ^6.0.0 || ^7.0.0-0" + vite-node "3.2.4" + why-is-node-running "^2.3.0" + void-elements@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" @@ -6281,6 +6883,14 @@ why-is-node-running@^2.2.2: siginfo "^2.0.0" stackback "0.0.2" +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"