Initial TrueGrowth source import

This commit is contained in:
2026-07-07 09:36:36 +08:00
commit 3b6781d695
2283 changed files with 691996 additions and 0 deletions

View File

@@ -0,0 +1,670 @@
# Opentu Manual 测试覆盖度分析与扩展计划
> 生成时间2026-01-27
> 分析范围:整个项目的 Manual 测试覆盖度
> 重点模块AI 输入栏(底部对话模块)及其配套功能
---
## 📊 目前已完成的测试覆盖
### ✅ 已测试模块100% 完成)
| 模块 | 静态截图 | 动态 GIF | 测试代码 | 状态 |
|------|---------|---------|---------|------|
| **工具箱操作** | ✅ | ✅ | ✅ | 🎉 完成 |
| **素材库管理** | ✅ | ✅ | ✅ | 🎉 完成 |
| **项目管理** | ✅ | ✅ | ✅ | 🎉 完成 |
| **备份/恢复** | ✅ | ✅ | ✅ | 🎉 完成 |
### ⚠️ 部分完成的测试代码已有GIF 未生成)
| 模块 | 静态截图 | 动态 GIF | 测试代码 | GIF 生成 | 状态 |
|------|---------|---------|---------|---------|------|
| **思维导图** | ⚠️ | ✅ | ✅ | ⏳ | 67% |
| **画笔绘制** | ✅ | ✅ | ✅ | ⏳ | 67% |
| **AI 图片生成** | ✅ | ✅ | ✅ | ⏳ | 67% |
| **画布导航** | ✅ | ✅ | ✅ | ⏳ | 67% |
| **形状工具** | ✅ | ✅ | ✅ | ⏳ | 67% |
---
## 🚨 **缺失的核心测试AI 输入栏模块**
### 🎯 模块概述
**AI 输入栏**AIInputBar是 Opentu 最核心的交互组件,位于画布底部中央,类似 Google Mixboard 的交互模式。它是用户与 AI 功能交互的主入口。
**相关文件**
```
packages/drawnix/src/components/ai-input-bar/
├── AIInputBar.tsx (主组件1872 行)
├── ModelDropdown.tsx (模型选择器)
├── ParametersDropdown.tsx (参数配置器)
├── GenerationTypeDropdown.tsx (生成类型选择)
├── CountDropdown.tsx (数量选择)
├── PromptHistoryPopover.tsx (历史提示词)
├── PromptSuggestionPanel.tsx (智能提示面板)
├── SizeDropdown.tsx (尺寸选择)
├── KeyboardDropdown.tsx (键盘导航)
└── ModelSelector.tsx (模型选择器)
```
---
## 🔍 AI 输入栏功能详细分解
### 1⃣ **核心功能组(基础交互)**
#### 📋 功能清单
| 功能点 | 优先级 | 测试难度 | 预计时长 | 依赖 API |
|--------|--------|---------|---------|---------|
| **输入框聚焦/失焦** | P0 | 简单 | 5 秒 | ❌ |
| **文本输入** | P0 | 简单 | 10 秒 | ❌ |
| **输入框展开/收缩** | P0 | 简单 | 8 秒 | ❌ |
| **发送按钮交互** | P0 | 简单 | 5 秒 | ❌ |
| **清空输入** | P1 | 简单 | 3 秒 | ❌ |
#### 🎬 测试思路
```typescript
test('AI 输入栏基础交互演示', async ({ page }) => {
// 1. 等待页面加载
await page.goto('http://localhost:7200/');
await page.waitForTimeout(1500);
// 2. 显示提示:找到 AI 输入栏
await showKeyHint(page, '底部 AI 输入栏', 1500);
const aiInputBar = page.getByTestId('ai-input-bar');
// 3. 点击输入框聚焦
await clickWithEffect(page, aiInputBar.locator('input'), '点击输入框', 1000);
// 4. 输入文本
await showKeyHint(page, '输入提示词', 1000);
await page.keyboard.type('一只可爱的猫咪', { delay: 100 });
await page.waitForTimeout(1000);
// 5. 展示输入框展开效果
await showKeyHint(page, '输入框自动展开', 1500);
// 6. 清空输入Ctrl+A + Backspace
await showKeyHint(page, '清空输入', 1000);
await page.keyboard.press('Control+A');
await page.keyboard.press('Backspace');
await page.waitForTimeout(1000);
// 7. 失焦收缩
await page.keyboard.press('Escape');
await showKeyHint(page, '输入框收缩', 1500);
});
```
---
### 2⃣ **模型选择组(关键配置)**
#### 📋 功能清单
| 功能点 | 优先级 | 测试难度 | 预计时长 | 子功能 |
|--------|--------|---------|---------|--------|
| **模型下拉菜单** | P0 | 中等 | 15 秒 | 展开、搜索、选择 |
| **模型健康状态** | P1 | 简单 | 5 秒 | 显示模型可用性 |
| **模型快捷代码** | P2 | 简单 | 5 秒 | `#imagen3` 显示 |
| **模型分类筛选** | P1 | 中等 | 10 秒 | 图片/视频/Agent |
#### 🎬 测试思路
```typescript
test('模型选择器演示', async ({ page }) => {
await page.goto('http://localhost:7200/');
const aiInputBar = page.getByTestId('ai-input-bar');
// 1. 打开模型下拉菜单
await showKeyHint(page, '选择生成模型', 1500);
const modelDropdown = aiInputBar.locator('.model-dropdown__trigger');
await clickWithEffect(page, modelDropdown, '打开模型列表', 1500);
// 2. 展示模型列表(滚动浏览)
await showKeyHint(page, '浏览可用模型', 1500);
const modelList = page.locator('.model-dropdown__menu');
await modelList.evaluate(el => {
el.scrollBy({ top: 100, behavior: 'smooth' });
});
await page.waitForTimeout(1000);
// 3. 健康状态展示
await showKeyHint(page, '模型健康状态', 1500);
// 4. 选择模型
const imagen3 = page.locator('.model-dropdown__item').filter({ hasText: 'imagen3' });
await clickWithEffect(page, imagen3, '选择 Imagen 3', 1500);
// 5. 显示快捷代码
await showKeyHint(page, '模型快捷代码 #imagen3', 1500);
});
```
---
### 3⃣ **参数配置组(高级功能)**
#### 📋 功能清单
| 功能点 | 优先级 | 测试难度 | 预计时长 | 配置项 |
|--------|--------|---------|---------|--------|
| **参数下拉菜单** | P0 | 中等 | 15 秒 | 展开、平铺展示 |
| **尺寸选择** | P0 | 简单 | 8 秒 | 1:1, 16:9, 4:3 等 |
| **数量选择** | P0 | 简单 | 8 秒 | 1-20 张 |
| **生成类型** | P0 | 简单 | 8 秒 | 图片/视频/Agent |
| **键盘导航** | P2 | 复杂 | 10 秒 | 上下左右键 |
#### 🎬 测试思路
```typescript
test('参数配置演示', async ({ page }) => {
await page.goto('http://localhost:7200/');
const aiInputBar = page.getByTestId('ai-input-bar');
// 1. 打开参数配置
await showKeyHint(page, '配置生成参数', 1500);
const paramsBtn = aiInputBar.locator('.parameters-dropdown__trigger');
await clickWithEffect(page, paramsBtn, '打开参数配置', 1500);
// 2. 展示参数分组(平铺显示)
await showKeyHint(page, '所有参数平铺展示', 1500);
const paramsPanel = page.locator('.parameters-dropdown__menu');
// 3. 选择尺寸
await showKeyHint(page, '选择图片尺寸', 1000);
const size16_9 = paramsPanel.locator('[data-param-value="16:9"]');
await clickWithEffect(page, size16_9, '选择 16:9', 1000);
// 4. 选择数量
await showKeyHint(page, '选择生成数量', 1000);
const count4 = paramsPanel.locator('[data-param-value="4"]');
await clickWithEffect(page, count4, '选择 4 张', 1000);
// 5. 关闭参数面板
await page.keyboard.press('Escape');
await showKeyHint(page, '配置已保存', 1500);
});
```
---
### 4⃣ **内容选择组(多模态输入)**
#### 📋 功能清单
| 功能点 | 优先级 | 测试难度 | 预计时长 | 子功能 |
|--------|--------|---------|---------|--------|
| **上传图片** | P0 | 中等 | 10 秒 | 文件选择、预览 |
| **从素材库选择** | P0 | 中等 | 12 秒 | 打开模态框、选择 |
| **画布元素选择** | P0 | 复杂 | 15 秒 | 自动捕获选中内容 |
| **已选内容预览** | P0 | 简单 | 8 秒 | 缩略图展示 |
| **移除选中内容** | P1 | 简单 | 5 秒 | 点击 X 移除 |
#### 🎬 测试思路
```typescript
test('多模态内容选择演示', async ({ page }) => {
await page.goto('http://localhost:7200/');
const aiInputBar = page.getByTestId('ai-input-bar');
// 1. 上传图片(模拟)
await showKeyHint(page, '上传参考图片', 1500);
const uploadBtn = aiInputBar.locator('.ai-input-bar__upload-btn');
await clickWithEffect(page, uploadBtn, '上传图片', 1000);
// 注意:实际文件上传需要特殊处理
await showKeyHint(page, '(演示:文件选择器)', 1500);
// 2. 从素材库选择
await showKeyHint(page, '从素材库选择', 1500);
const libraryBtn = aiInputBar.locator('.ai-input-bar__library-btn');
await clickWithEffect(page, libraryBtn, '打开素材库', 1500);
// 假设素材库已打开
const firstAsset = page.getByTestId('media-library-grid')
.locator('.asset-item').first();
if (await firstAsset.isVisible()) {
await clickWithEffect(page, firstAsset, '选择素材', 1000);
const confirmBtn = page.getByRole('button', { name: '确认' });
await clickWithEffect(page, confirmBtn, '确认', 1000);
}
// 3. 展示已选内容预览
await showKeyHint(page, '已选内容预览', 1500);
const preview = aiInputBar.locator('.selected-content-preview');
// 4. 移除内容
const removeBtn = preview.locator('.remove-btn').first();
if (await removeBtn.isVisible()) {
await clickWithEffect(page, removeBtn, '移除', 1000);
}
});
```
---
### 5⃣ **智能提示组(体验优化)**
#### 📋 功能清单
| 功能点 | 优先级 | 测试难度 | 预计时长 | 特性 |
|--------|--------|---------|---------|------|
| **历史提示词** | P1 | 中等 | 12 秒 | 悬浮展示、选择 |
| **预设提示词** | P1 | 简单 | 10 秒 | 分类展示 |
| **置顶提示词** | P2 | 简单 | 5 秒 | Pin 功能 |
| **删除历史** | P2 | 简单 | 5 秒 | 删除操作 |
| **灵感面板** | P1 | 中等 | 15 秒 | 空画布时显示 |
#### 🎬 测试思路
```typescript
test('智能提示面板演示', async ({ page }) => {
await page.goto('http://localhost:7200/');
const aiInputBar = page.getByTestId('ai-input-bar');
// 1. 显示灵感面板(空画布)
await showKeyHint(page, '灵感提示面板', 1500);
const inspirationBoard = page.locator('.inspiration-board');
if (await inspirationBoard.isVisible()) {
await showKeyHint(page, '选择预设提示词', 1500);
const card = inspirationBoard.locator('.inspiration-card').first();
await clickWithEffect(page, card, '选择灵感', 1500);
}
// 2. 打开历史提示词
await showKeyHint(page, '查看历史提示词', 1500);
const historyBtn = aiInputBar.locator('.prompt-history-popover__trigger');
await page.hover(historyBtn.locator('button'));
await page.waitForTimeout(800); // 悬浮延迟
// 3. 展示历史列表
await showKeyHint(page, '历史记录与预设', 1500);
const historyPanel = page.locator('.prompt-list-panel');
// 4. 置顶操作
const pinBtn = historyPanel.locator('.pin-btn').first();
if (await pinBtn.isVisible()) {
await clickWithEffect(page, pinBtn, '置顶', 1000);
}
// 5. 选择提示词
const promptItem = historyPanel.locator('.prompt-item').first();
await clickWithEffect(page, promptItem, '使用提示词', 1000);
});
```
---
### 6⃣ **工作流集成组(高级场景)**
#### 📋 功能清单
| 功能点 | 优先级 | 测试难度 | 预计时长 | 说明 |
|--------|--------|---------|---------|------|
| **发送后打开 ChatDrawer** | P0 | 中等 | 10 秒 | 自动打开对话抽屉 |
| **Agent 模式** | P1 | 复杂 | 20 秒 | AI 自动选择工具 |
| **工作流显示** | P1 | 中等 | 15 秒 | 显示执行步骤 |
| **任务状态同步** | P1 | 复杂 | 15 秒 | 实时更新状态 |
| **错误处理** | P2 | 中等 | 10 秒 | API 失败提示 |
#### 🎬 测试思路
```typescript
test('AI 工作流完整演示', async ({ page }) => {
await page.goto('http://localhost:7200/');
const aiInputBar = page.getByTestId('ai-input-bar');
// 1. 输入提示词
await showKeyHint(page, '输入生成请求', 1500);
await clickWithEffect(page, aiInputBar.locator('input'), '输入', 1000);
await page.keyboard.type('生成一只猫咪', { delay: 100 });
// 2. 配置参数(快速)
await showKeyHint(page, '配置生成参数', 1000);
// ... 选择模型、尺寸等
// 3. 发送请求
await showKeyHint(page, '发送生成请求', 1500);
const sendBtn = aiInputBar.locator('.ai-input-bar__send-btn');
await clickWithEffect(page, sendBtn, '发送', 1500);
// 4. 自动打开 ChatDrawer
await showKeyHint(page, '对话抽屉自动打开', 1500);
const chatDrawer = page.getByTestId('chat-drawer');
await page.waitForSelector('[data-testid="chat-drawer"]', {
state: 'visible',
timeout: 3000
});
// 5. 展示工作流执行
await showKeyHint(page, '工作流执行中', 2000);
// 注意:实际 AI 生成会很慢,测试中可能需要 mock
// 6. 关闭抽屉
await page.keyboard.press('Escape');
await showKeyHint(page, '关闭对话抽屉', 1000);
});
```
---
## 🎯 ChatDrawer对话抽屉功能分解
### 1⃣ **基础交互**
| 功能点 | 优先级 | 测试难度 | 预计时长 |
|--------|--------|---------|---------|
| **打开/关闭抽屉** | P0 | 简单 | 8 秒 |
| **拖动调整宽度** | P1 | 中等 | 10 秒 |
| **会话列表** | P0 | 中等 | 15 秒 |
| **新建会话** | P0 | 简单 | 5 秒 |
| **切换会话** | P0 | 简单 | 8 秒 |
| **删除会话** | P1 | 简单 | 5 秒 |
### 2⃣ **消息交互**
| 功能点 | 优先级 | 测试难度 | 预计时长 |
|--------|--------|---------|---------|
| **显示用户消息** | P0 | 简单 | 5 秒 |
| **显示 AI 响应** | P0 | 中等 | 10 秒 |
| **工作流消息** | P1 | 复杂 | 20 秒 |
| **重试失败任务** | P1 | 中等 | 12 秒 |
| **插入到画布** | P0 | 中等 | 10 秒 |
| **下载结果** | P1 | 简单 | 5 秒 |
### 3⃣ **高级功能**
| 功能点 | 优先级 | 测试难度 | 预计时长 |
|--------|--------|---------|---------|
| **模型切换** | P1 | 简单 | 8 秒 |
| **Mermaid 渲染** | P2 | 中等 | 10 秒 |
| **代理日志** | P2 | 中等 | 10 秒 |
| **会话搜索** | P2 | 中等 | 10 秒 |
---
## 📈 测试优先级矩阵
### 🔥 P0 级(必须测试,核心功能)
```
优先执行顺序:
1. AI 输入栏基础交互 ⏱️ 8 秒
2. 模型选择器 ⏱️ 15 秒
3. 参数配置 ⏱️ 15 秒
4. 多模态内容选择 ⏱️ 12 秒
5. 发送 + ChatDrawer 打开 ⏱️ 10 秒
6. ChatDrawer 基础交互 ⏱️ 15 秒
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总计时长:~75 秒1 分 15 秒)
```
### ⚡ P1 级(重要功能,增强体验)
```
1. 智能提示面板 ⏱️ 15 秒
2. 工作流完整演示 ⏱️ 20 秒
3. 会话管理 ⏱️ 15 秒
4. ChatDrawer 拖动调整 ⏱️ 10 秒
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总计时长:~60 秒1 分钟)
```
### 🎨 P2 级(锦上添花,可选)
```
1. 键盘快捷键导航 ⏱️ 10 秒
2. 历史提示词置顶/删除 ⏱️ 8 秒
3. Mermaid 流程图渲染 ⏱️ 10 秒
4. 代理日志查看 ⏱️ 10 秒
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总计时长:~38 秒
```
---
## 🎬 推荐测试计划
### 📅 阶段 1核心功能覆盖Week 1
#### Day 1-2: AI 输入栏基础
```
✅ test('AI 输入栏基础交互演示') - 8 秒
✅ test('模型选择器演示') - 15 秒
✅ test('参数配置演示') - 15 秒
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
合计 GIF: ~38 秒
```
#### Day 3-4: 内容选择与工作流
```
✅ test('多模态内容选择演示') - 12 秒
✅ test('AI 工作流完整演示') - 20 秒
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
合计 GIF: ~32 秒
```
#### Day 5: ChatDrawer 基础
```
✅ test('ChatDrawer 基础交互演示') - 15 秒
✅ test('会话管理演示') - 15 秒
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
合计 GIF: ~30 秒
```
### 📅 阶段 2体验优化Week 2
#### Day 1-2: 智能提示
```
✅ test('智能提示面板演示') - 15 秒
✅ test('灵感面板演示') - 12 秒
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
合计 GIF: ~27 秒
```
#### Day 3-4: 高级功能
```
✅ test('ChatDrawer 高级功能演示') - 20 秒
✅ test('工作流重试与插入演示') - 15 秒
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
合计 GIF: ~35 秒
```
---
## 🛠️ 技术实现建议
### 1. Mock API 响应
由于 AI 生成需要较长时间10-60 秒),测试中建议 mock
```typescript
// 在测试开始前注入 mock
await page.route('**/api/generation/**', route => {
route.fulfill({
status: 200,
body: JSON.stringify({
id: 'mock-task-id',
status: 'completed',
result_url: 'https://example.com/mock-image.png',
}),
});
});
```
### 2. Service Worker 通信
AI 输入栏通过 Service Worker 处理任务,测试时需要:
```typescript
// 等待 SW 就绪
await page.waitForFunction(() => {
return navigator.serviceWorker.controller !== null;
});
// 监听 SW 消息
await page.evaluate(() => {
window.__swMessages = [];
navigator.serviceWorker.addEventListener('message', (event) => {
window.__swMessages.push(event.data);
});
});
```
### 3. 文件上传测试
对于图片上传功能:
```typescript
// 方式 1: 使用测试图片文件
await page.setInputFiles(
'input[type="file"]',
'apps/web-e2e/fixtures/test-image.png'
);
// 方式 2: 创建临时 Blob仅演示界面
await page.evaluate(() => {
const input = document.querySelector('input[type="file"]');
const dataTransfer = new DataTransfer();
const file = new File(['mock'], 'test.png', { type: 'image/png' });
dataTransfer.items.add(file);
input.files = dataTransfer.files;
input.dispatchEvent(new Event('change', { bubbles: true }));
});
```
---
## 📊 最终测试覆盖度预期
### 完成阶段 1 后
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
静态截图: ████████████████████████████████ 100% (21/21)
动态 GIF: ████████████████████████████░░░░ 80% (15/19)
测试代码: ████████████████████████████░░░░ 80% (13/16)
已生成 GIF: ██████████████░░░░░░░░░░░░░░░░░░ 50% (10/19)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总进度: ███████████████████████████████░ 90%
```
### 完成阶段 2 后
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
静态截图: ████████████████████████████████ 100% (21/21)
动态 GIF: ████████████████████████████████ 100% (19/19)
测试代码: ████████████████████████████████ 100% (16/16)
已生成 GIF: ████████████████████████████████ 100% (19/19)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总进度: ████████████████████████████████ 100%
🎊 完整测试体系达成!
```
---
## 📦 其他需要补充的测试模块
### 🎨 绘图工具详细测试
| 模块 | 当前状态 | 缺失测试 |
|------|---------|---------|
| **铅笔工具** | 有基础 GIF | 压感、橡皮擦、设置面板 |
| **形状工具** | 有基础 GIF | 圆角调整、布尔运算、组合 |
| **文本工具** | ❌ | 字体、大小、颜色、对齐 |
| **渐变编辑器** | ❌ | 多色渐变、角度调整 |
### 🖼️ 画布操作测试
| 模块 | 当前状态 | 缺失测试 |
|------|---------|---------|
| **缩放平移** | 有基础 GIF | 快捷键、手势、小地图 |
| **多选操作** | ❌ | 框选、Shift 多选、群组 |
| **图层管理** | ❌ | 上移、下移、置顶、置底 |
| **对齐分布** | ❌ | 左对齐、居中、等间距 |
### 🔧 工具箱详细测试
| 模块 | 当前状态 | 缺失测试 |
|------|---------|---------|
| **工具列表** | ✅ 完成 | - |
| **工具窗口** | ✅ 完成 | - |
| **自定义工具** | ❌ | 添加、配置、删除 |
| **工具设置** | ❌ | iframe 配置、权限设置 |
### 📚 其他功能模块
| 模块 | 优先级 | 预计时长 |
|------|--------|---------|
| **应用设置** | P1 | 15 秒 |
| **语言切换** | P2 | 5 秒 |
| **快捷键面板** | P2 | 10 秒 |
| **版本更新提示** | P2 | 5 秒 |
| **性能面板** | P2 | 8 秒 |
| **小地图** | P1 | 10 秒 |
| **导出功能** | P1 | 12 秒 |
---
## 🎯 总结与建议
### ✅ 已完成的优势
1.**核心项目管理功能**:工具箱、素材库、项目管理、备份恢复全覆盖
2.**测试框架完善**DSL 配置 + 手动录制双轨系统
3.**视觉效果统一**:品牌色、动画、标注完全一致
4.**代码质量高**:清晰注释、容错处理、可维护性强
### 🚨 最大缺口AI 交互模块
**AI 输入栏 + ChatDrawer** 是 Opentu 的核心竞争力,但目前 **0% 测试覆盖**
**重要性评分**:⭐⭐⭐⭐⭐ (5/5)
**紧急程度**:🔥🔥🔥🔥🔥 (5/5)
**用户影响**:🎯🎯🎯🎯🎯 (5/5)
### 📋 行动建议
#### 短期(本周)
1. ✅ 完成 AI 输入栏基础交互测试3 个 GIF~38 秒)
2. ✅ 完成 ChatDrawer 基础交互测试2 个 GIF~30 秒)
3. ✅ 添加 Mock API 支持,避免长时间等待
#### 中期(下周)
1. ✅ 完成智能提示面板测试2 个 GIF~27 秒)
2. ✅ 完成工作流高级功能测试2 个 GIF~35 秒)
3. ✅ 补充文本工具、多选、图层管理测试
#### 长期(本月内)
1. ✅ 达成 100% Manual 测试覆盖
2. ✅ 建立 CI/CD 自动化测试流程
3. ✅ 完善国际化(英文)测试
---
## 🔗 相关文档
- `docs/CODING_RULES.md` - 编码规则
- `docs/FEATURE_FLOWS.md` - 功能流程
- `apps/web-e2e/src/manual-gen/gif-recordings.manual.spec.ts` - 现有 GIF 测试
- `packages/drawnix/src/components/ai-input-bar/` - AI 输入栏源码
- `packages/drawnix/src/components/chat-drawer/` - ChatDrawer 源码
---
**生成时间**2026-01-27
**分析师**Claude Sonnet 4.5
**版本**v1.0